From d9f61236a6b2d456cdae52b63ac1e63178b98884 Mon Sep 17 00:00:00 2001 From: Joshua Beha Date: Mon, 17 Jun 2024 13:11:41 -0400 Subject: [PATCH 1/6] Add multiArch param --- task/skopeo-copy/0.4/README.md | 110 ++++++++++++++++++ .../0.4/samples/docker-secret.yaml | 10 ++ task/skopeo-copy/0.4/samples/quay-secret.yaml | 10 ++ task/skopeo-copy/0.4/samples/run.yaml | 12 ++ .../0.4/samples/serviceaccount.yaml | 7 ++ task/skopeo-copy/0.4/skopeo-copy.yaml | 84 +++++++++++++ .../0.4/tests/pre-apply-task-hook.sh | 5 + task/skopeo-copy/0.4/tests/run.yaml | 19 +++ 8 files changed, 257 insertions(+) create mode 100644 task/skopeo-copy/0.4/README.md create mode 100644 task/skopeo-copy/0.4/samples/docker-secret.yaml create mode 100644 task/skopeo-copy/0.4/samples/quay-secret.yaml create mode 100644 task/skopeo-copy/0.4/samples/run.yaml create mode 100644 task/skopeo-copy/0.4/samples/serviceaccount.yaml create mode 100644 task/skopeo-copy/0.4/skopeo-copy.yaml create mode 100644 task/skopeo-copy/0.4/tests/pre-apply-task-hook.sh create mode 100644 task/skopeo-copy/0.4/tests/run.yaml diff --git a/task/skopeo-copy/0.4/README.md b/task/skopeo-copy/0.4/README.md new file mode 100644 index 0000000000..913cd178ba --- /dev/null +++ b/task/skopeo-copy/0.4/README.md @@ -0,0 +1,110 @@ +# Skopeo + + +[Skopeo](https://github.com/containers/skopeo) is a command line tool for working with remote image registries. Skopeo doesn’t require a daemon to be running while performing its operations. In particular, the handy skopeo command called `copy` will ease the whole image copy operation. Without further ado, you can copy an image from a registry to another simply by running: +``` +skopeo copy docker://internal.registry/myimage:latest / +docker://production.registry/myimage:v1.0 +``` +The copy command will take care of copying the image from `internal.registry` to `production.registry` + +If your production registry requires credentials to login in order to push the image, skopeo can handle that as well. + +``` +skopeo copy --dest-creds prod_user:prod_pass docker://internal.registry/myimage:latest / +docker://production.registry/myimage:v1.0 +``` + +The same goes for credentials for the source registry (internal.registry) by using the `--src-creds` flag. + +It is also useful for copying images between two remote docker registries, such as the registries of two different OpenShift clusters, as shown +``` +skopeo copy docker://busybox:latest oci:busybox_ocilayout:latest +``` +Skopeo copy isn’t limited to remote containers registries. The image prefix `docker://` from the above command define the transport to be used when handling the image. + +There are others also similar to that: + +- atomic +- containers-storage +- dir +- docker +- docker-daemon +- docker-tar +- oci +- ostree + +This `task` can be used to copy one or more than one images to-and fro various storage mechanisms. + +## Install the Task + +``` +kubectl apply -f https://api.hub.tekton.dev/v1/resource/tekton/task/skopeo-copy/0.3/raw +``` + +## Parameters + +- **srcImageURL**: The URL of the image to be copied to the `destination` registry. +- **destImageURL**: The URL of the image where the image from `source` should be copied to. +- **srcTLSverify**: Verify the TLS on the src registry endpoint +- **destTLSverify**: Verify the TLS on the dest registry endpoint +- **multiArch**: How to handle multi-architecture images (system, all, or index-only) + +## Workspace + +- **images-url**: To mount file containing multiple source and destination images registries URL, which is mounted as configMap. + + +## Secrets and ConfigMap +* `Secret` to provide the credentials of the source and destination registry where the image needs to be copied from and to. +* `ConfigMap` to provide support for copying multiple images, this contains file `url.txt` which stores images registry URL's. + + [This](../0.4/samples/quay-secret.yaml) example can help to use secrets for providing credentials of image registries. + +## Platforms + +The Task can be run on `linux/amd64`, `linux/s390x`, `linux/arm64` and `linux/ppc64le` platforms. + +## Usage + +This task will use the `Service Account` with access to the secrets containing source and destination image registry credentials, this will authorize it to the respective image registries. + +In case of multiple source and destination image registries that needs to be copied to and from a file named `url.txt` should be created containing all the source and destination image registries `URL` seperated by a space and each set of images should be written in the new line, as shown below. + +``` +docker://quay.io/temp/kubeconfigwriter:v1 docker://quay.io/skopeotest/kube:v1 +docker://quay.io/temp/kubeconfigwriter:v2 docker://quay.io/skopeotest/kube:v2 +``` + +`ConfigMap` should be created using this file. Following `command` can be used to create configMap from the `file`. +``` +kubectl create configmap image-configmap --from-file=url.txt +``` +In case there is only one source and destination image that needs to be copied then, Source and destination image URL needs to be provided in the input params of the task. + +This will result in the image getting copied from the source registry to the destination registry. + + +[This](../0.4/samples/serviceaccount.yaml) will guide the user to use service account for authorization to image registries. + +See [here](../0.4/samples/run.yaml) for example of `TaskRun`. +### Note + +- `Source credentials` are only required, if the source image registry needs authentication to pull the image, whereas `Destination credentials` are always required. + +- In case of multiple source and destination images, `secret` containing `credentials` of all the image registries must be added to the `service account` and configMap containing `url.txt` should be mounted into the workspace, as shown + ``` + workspaces: + - name: images-url + configmap: + name: image-configmap + ``` + + +- If there is only one source and destination image registry URL, then `emptyDir` needs to be mounted in the `workspace` as shown below: + + ``` + workspaces: + - name: images-url + emptyDir: {} + ``` diff --git a/task/skopeo-copy/0.4/samples/docker-secret.yaml b/task/skopeo-copy/0.4/samples/docker-secret.yaml new file mode 100644 index 0000000000..94c48661af --- /dev/null +++ b/task/skopeo-copy/0.4/samples/docker-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: docker-creds + annotations: + tekton.dev/docker-0: https://docker.io +type: kubernetes.io/basic-auth +stringData: + username: test + password: test@1 diff --git a/task/skopeo-copy/0.4/samples/quay-secret.yaml b/task/skopeo-copy/0.4/samples/quay-secret.yaml new file mode 100644 index 0000000000..4482f8572f --- /dev/null +++ b/task/skopeo-copy/0.4/samples/quay-secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: quay-creds + annotations: + tekton.dev/docker-0: https://quay.io +type: kubernetes.io/basic-auth +stringData: + username: skopeotest + password: Skopeo@1 diff --git a/task/skopeo-copy/0.4/samples/run.yaml b/task/skopeo-copy/0.4/samples/run.yaml new file mode 100644 index 0000000000..5203e43a01 --- /dev/null +++ b/task/skopeo-copy/0.4/samples/run.yaml @@ -0,0 +1,12 @@ +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + name: skopeo-run +spec: + serviceAccountName: secret-service-account + taskRef: + name: skopeo-copy + workspaces: + - name: images-url + configmap: + name: image-configmap diff --git a/task/skopeo-copy/0.4/samples/serviceaccount.yaml b/task/skopeo-copy/0.4/samples/serviceaccount.yaml new file mode 100644 index 0000000000..7dcb46907d --- /dev/null +++ b/task/skopeo-copy/0.4/samples/serviceaccount.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: secret-service-account +secrets: + - name: docker-creds + - name: quay-creds diff --git a/task/skopeo-copy/0.4/skopeo-copy.yaml b/task/skopeo-copy/0.4/skopeo-copy.yaml new file mode 100644 index 0000000000..ba279c2a34 --- /dev/null +++ b/task/skopeo-copy/0.4/skopeo-copy.yaml @@ -0,0 +1,84 @@ +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: skopeo-copy + labels: + app.kubernetes.io/version: "0.3" + annotations: + tekton.dev/pipelines.minVersion: "0.12.1" + tekton.dev/categories: CLI + tekton.dev/tags: cli + tekton.dev/displayName: "skopeo copy" + tekton.dev/platforms: "linux/amd64,linux/s390x,linux/ppc64le,linux/arm64" +spec: + description: >- + Skopeo is a command line tool for working with remote image registries. + + Skopeo doesn’t require a daemon to be running while performing its operations. + In particular, the handy skopeo command called copy will ease the whole image + copy operation. The copy command will take care of copying the image from + internal.registry to production.registry. If your production registry requires + credentials to login in order to push the image, skopeo can handle that as well. + + workspaces: + - name: images-url + params: + - name: srcImageURL + description: URL of the image to be copied to the destination registry + type: string + default: "" + - name: destImageURL + description: URL of the image where the image from source should be copied to + type: string + default: "" + - name: srcTLSverify + description: Verify the TLS on the src registry endpoint + type: string + default: "true" + - name: destTLSverify + description: Verify the TLS on the dest registry endpoint + type: string + default: "true" + - name: multiArch + description: How to handle multi-architecture images (system, all, or index-only) + type: string + default: "system" + steps: + - name: skopeo-copy + env: + - name: HOME + value: /tekton/home + image: quay.io/skopeo/stable:v1 + script: | + # Function to copy multiple images. + # + copyimages() { + filename="$(workspaces.images-url.path)/url.txt" + if ! [[ "$(params.multiArch)" =~ ^(system|all|index-only)$ ]]; then exit 1 ; fi + while IFS= read -r line || [ -n "$line" ] + do + cmd="" + for url in $line + do + # echo $url + cmd="$cmd \ + $url" + done + read -ra sourceDest <<<"${cmd}" + skopeo copy --multi-arch="$(params.multiArch)" "${sourceDest[@]}" --src-tls-verify="$(params.srcTLSverify)" --dest-tls-verify="$(params.destTLSverify)" + echo "$cmd" + done < "$filename" + } + # + # If single image is to be copied then, it can be passed through + # params in the taskrun. + if [ "$(params.srcImageURL)" != "" ] && [ "$(params.destImageURL)" != "" ] ; then + skopeo copy --multi-arch="$(params.multiArch)" "$(params.srcImageURL)" "$(params.destImageURL)" --src-tls-verify="$(params.srcTLSverify)" --dest-tls-verify="$(params.destTLSverify)" + else + # If file is provided as a configmap in the workspace then multiple images can be copied. + # + copyimages + fi + securityContext: + runAsNonRoot: true + runAsUser: 65532 diff --git a/task/skopeo-copy/0.4/tests/pre-apply-task-hook.sh b/task/skopeo-copy/0.4/tests/pre-apply-task-hook.sh new file mode 100644 index 0000000000..08b496c826 --- /dev/null +++ b/task/skopeo-copy/0.4/tests/pre-apply-task-hook.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +# Add an internal registry as sidecar to the task so we can upload it directly +# from our tests without having to go to an external registry. +add_sidecar_registry ${TMPF} diff --git a/task/skopeo-copy/0.4/tests/run.yaml b/task/skopeo-copy/0.4/tests/run.yaml new file mode 100644 index 0000000000..0cafdb784f --- /dev/null +++ b/task/skopeo-copy/0.4/tests/run.yaml @@ -0,0 +1,19 @@ +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + name: skopeo-run +spec: + params: + - name: srcImageURL + value: docker://quay.io/temp/kubeconfigwriter:v1 + - name: destImageURL + value: docker://localhost:5000/kube:latest + - name: destTLSverify + value: "false" + - name: multiArch + value: "system" + taskRef: + name: skopeo-copy + workspaces: + - name: images-url + emptyDir: {} From 6ed160ea2bd9782c4049772232f5324c1994b35d Mon Sep 17 00:00:00 2001 From: Joshua Beha Date: Mon, 17 Jun 2024 13:14:38 -0400 Subject: [PATCH 2/6] Add new version in missed places --- task/skopeo-copy/0.4/README.md | 2 +- task/skopeo-copy/0.4/skopeo-copy.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/task/skopeo-copy/0.4/README.md b/task/skopeo-copy/0.4/README.md index 913cd178ba..d5ee4c9b82 100644 --- a/task/skopeo-copy/0.4/README.md +++ b/task/skopeo-copy/0.4/README.md @@ -39,7 +39,7 @@ This `task` can be used to copy one or more than one images to-and fro various s ## Install the Task ``` -kubectl apply -f https://api.hub.tekton.dev/v1/resource/tekton/task/skopeo-copy/0.3/raw +kubectl apply -f https://api.hub.tekton.dev/v1/resource/tekton/task/skopeo-copy/0.4/raw ``` ## Parameters diff --git a/task/skopeo-copy/0.4/skopeo-copy.yaml b/task/skopeo-copy/0.4/skopeo-copy.yaml index ba279c2a34..cea9c68038 100644 --- a/task/skopeo-copy/0.4/skopeo-copy.yaml +++ b/task/skopeo-copy/0.4/skopeo-copy.yaml @@ -3,7 +3,7 @@ kind: Task metadata: name: skopeo-copy labels: - app.kubernetes.io/version: "0.3" + app.kubernetes.io/version: "0.4" annotations: tekton.dev/pipelines.minVersion: "0.12.1" tekton.dev/categories: CLI From c37d7beb91cf65dc24a349de1bea4e89120af86f Mon Sep 17 00:00:00 2001 From: Joshua Beha Date: Mon, 17 Jun 2024 13:21:52 -0400 Subject: [PATCH 3/6] Make param check more readable --- task/skopeo-copy/0.4/skopeo-copy.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/task/skopeo-copy/0.4/skopeo-copy.yaml b/task/skopeo-copy/0.4/skopeo-copy.yaml index cea9c68038..205a51aed5 100644 --- a/task/skopeo-copy/0.4/skopeo-copy.yaml +++ b/task/skopeo-copy/0.4/skopeo-copy.yaml @@ -54,7 +54,12 @@ spec: # copyimages() { filename="$(workspaces.images-url.path)/url.txt" - if ! [[ "$(params.multiArch)" =~ ^(system|all|index-only)$ ]]; then exit 1 ; fi + case "$(params.multiArch)" in all|system|index-only) + ;; + *) + echo "Unrecognized multiArch choice: $(params.multiArch)" + exit 1;; + esac while IFS= read -r line || [ -n "$line" ] do cmd="" From 159d8b64310e447f187d848b4b76b34d6b82004e Mon Sep 17 00:00:00 2001 From: Joshua Beha Date: Tue, 18 Jun 2024 16:43:19 -0400 Subject: [PATCH 4/6] Move case to cover both situations --- task/skopeo-copy/0.4/skopeo-copy.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/task/skopeo-copy/0.4/skopeo-copy.yaml b/task/skopeo-copy/0.4/skopeo-copy.yaml index 205a51aed5..141179cfdf 100644 --- a/task/skopeo-copy/0.4/skopeo-copy.yaml +++ b/task/skopeo-copy/0.4/skopeo-copy.yaml @@ -54,12 +54,6 @@ spec: # copyimages() { filename="$(workspaces.images-url.path)/url.txt" - case "$(params.multiArch)" in all|system|index-only) - ;; - *) - echo "Unrecognized multiArch choice: $(params.multiArch)" - exit 1;; - esac while IFS= read -r line || [ -n "$line" ] do cmd="" @@ -77,6 +71,13 @@ spec: # # If single image is to be copied then, it can be passed through # params in the taskrun. + # + case "$(params.multiArch)" in all|system|index-only) + ;; + *) + echo "Unrecognized multiArch choice: $(params.multiArch)" + exit 1;; + esac if [ "$(params.srcImageURL)" != "" ] && [ "$(params.destImageURL)" != "" ] ; then skopeo copy --multi-arch="$(params.multiArch)" "$(params.srcImageURL)" "$(params.destImageURL)" --src-tls-verify="$(params.srcTLSverify)" --dest-tls-verify="$(params.destTLSverify)" else From 6bec80a0a8f38084f273c7bde82968f9882ca7ba Mon Sep 17 00:00:00 2001 From: Joshua Beha Date: Wed, 19 Jun 2024 09:31:42 -0400 Subject: [PATCH 5/6] Fix comments --- task/skopeo-copy/0.4/.skopeo-copy.yaml.swp | Bin 0 -> 12288 bytes task/skopeo-copy/0.4/skopeo-copy.yaml | 8 +++++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 task/skopeo-copy/0.4/.skopeo-copy.yaml.swp diff --git a/task/skopeo-copy/0.4/.skopeo-copy.yaml.swp b/task/skopeo-copy/0.4/.skopeo-copy.yaml.swp new file mode 100644 index 0000000000000000000000000000000000000000..0680f88b6343a0b3bbfcd807a22cf07c5dc20136 GIT binary patch literal 12288 zcmeHNON<@G6)lGlAPFP_7DQ2`%bN+Yqv?J#_SlJ^Z4_c-G01i#|B(1d)O6Q-T}*dZ zQ&l}P508NbSwvZ2HV6~e2#F{XiiD6@WfestDY#Gxoc`>+!1xU$}1gE@O`=lazd2B;0j=_EU{B zLAG9mh1v+^dKN}1yK2IdGl9cnDk?LPTQj`x=-Sz#6&qINLBuUj)nsV7*%+D)RR|SC zs+bO!!`1G0IOSP-mHfV;XP{@`^BB09?c2NJLw#XzC*5}Ixz8Ki_w@|)4D<~24D<~2 z4D<~24D<~24E&!lV2fSsDfsk_+OLP}-@C5(U4Qn!dIow1dIow1dIow1dIow1dIow1 zdIow1dIow1{s$T0F=Nl+kNqE92jlVo|IOe3?|qN4cYwEmH-J9?zX6^AP5=RLJMhsh zjJ*Ur4?F|>9QYaV{>_YC0)7en^Sg|_2wVn!3s~SW;52YO@UQPM_Ac;ezyOZ|r-1#y zzrW4c2f$x}KLQ4L6u2K)1^#vuV}Ai&16~0x0lx+`a2{9&2>AF$*b2N2ya2?&1Hduh zC%_%RCpR$mA@COPCh#oqk8fcP@H+5&AO;=;egxbC>;m4rp0SsK%fPe1ap1?m4}m{@ z6Jvmn!S5yD89)Isa1z)HIPUKN`sZuJ0M(B!^LxxWBlL(4?BBm{FPrN)rSj8C*%4V? zin_~)sMOLuCknnF^AI3avI}EMnq@IOgVK zImviIGAEk|+ERMM6g(2{E2peXtt{M3uYe++)4FgfrHI2;he%ChGvTT3+jzq$2)vAd z>nJ%gSlnH3%`+4F#(U7VXF#)7qoXya=-lZObM3J(w$l*=7#P^p1e-$3@w6is9cNFR zshS}o2;9_b4|P4Av8^+5Ob>mgO?Mul!GIp2yY8ABbcKP~CxphQzOa!l)<4Wa%)Yx~LvM7|c&J$r170=@2V#m(d%{UC> zT%_bY6N64C$nv}_Mp-=H{vYr43r3YX634Ln9{R;27u_EHDt9yC(SpYmXihWLbolV$ z!QutDb8+4uN5h=sP5{wjsnhv^NIY4L$iq*1nE65NkA;dZH)*aXR&$?Yr3pja(l4g+ z3cX%>SLNK~t;FTCwc}@wG~SVuKs!5#%OJ-xx=%!EM`5$DE?u;F&V*HJ3t&iCaGUHI zx;MLRXS!z@cK05-&`Rv?$Kz!lS*egjRh9Sr)}Cd~CbCVWWq~ZeQ%mv6NA!5fr=e8! z&BURxd_5JT&6QTyCQnNQUVXiBn5e8-rGVUK9npQiI(ctBT;!Wot!Vl>7a%bN0alo% zn8aetk>p1-s3NcN+D;1yHC7`B)nbw4y!CJs>eQC=HQ{-Th49|`951biY^mFdvAYtx zw6*cPJ zkhU6p1tuWIzrccS>AK2*FY7-lXOs5))}pOMU@=^dtiH!uWpy4Plc+L{yEUbX&n_ox zbv^#?Su|h8RJ%Yj=RSg(b(&iqkkhfGDxULu+`DI77-Qlryhk{-&J z$;^0iYwW#V%iTG{%BW1aUh*THGt<_)T4;peM0xazyaM6swA(aWiE2(DUg8;lm6~ED zO#c4Umt9nO>v5KhxyY&nh!Y|XGdVH`=DR!%e~{}l*B~uwbK{>0tJSHcqHMb(Y2~O- z=T@ndT$ykS>g7qBKxd|Gv!QG5rGPM^xg2$Oz)T^cYM)dW!^2o?4vUmqXOBVFgPL-C z$g}vs>QeP?_8nZ_uHTCyI+8Y&Bp#O*ZiLyG(OsUKd|k8X{?If=)(K|f+3Js#0n98Vc9WUISl+~&$UMz63> E0U!KUH~;_u literal 0 HcmV?d00001 diff --git a/task/skopeo-copy/0.4/skopeo-copy.yaml b/task/skopeo-copy/0.4/skopeo-copy.yaml index 141179cfdf..7a0e5429cf 100644 --- a/task/skopeo-copy/0.4/skopeo-copy.yaml +++ b/task/skopeo-copy/0.4/skopeo-copy.yaml @@ -68,9 +68,7 @@ spec: echo "$cmd" done < "$filename" } - # - # If single image is to be copied then, it can be passed through - # params in the taskrun. + # Check that the multiArch parm is one of the supported methods # case "$(params.multiArch)" in all|system|index-only) ;; @@ -78,6 +76,10 @@ spec: echo "Unrecognized multiArch choice: $(params.multiArch)" exit 1;; esac + # + # If single image is to be copied then, it can be passed through + # params in the taskrun. + # if [ "$(params.srcImageURL)" != "" ] && [ "$(params.destImageURL)" != "" ] ; then skopeo copy --multi-arch="$(params.multiArch)" "$(params.srcImageURL)" "$(params.destImageURL)" --src-tls-verify="$(params.srcTLSverify)" --dest-tls-verify="$(params.destTLSverify)" else From 5364f04ea53cb0d959c864b24d3e9fdd0df35f87 Mon Sep 17 00:00:00 2001 From: Joshua Beha Date: Wed, 3 Jul 2024 08:08:30 -0400 Subject: [PATCH 6/6] Delete skopeo-copy.swp --- task/skopeo-copy/0.4/.skopeo-copy.yaml.swp | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 task/skopeo-copy/0.4/.skopeo-copy.yaml.swp diff --git a/task/skopeo-copy/0.4/.skopeo-copy.yaml.swp b/task/skopeo-copy/0.4/.skopeo-copy.yaml.swp deleted file mode 100644 index 0680f88b6343a0b3bbfcd807a22cf07c5dc20136..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeHNON<@G6)lGlAPFP_7DQ2`%bN+Yqv?J#_SlJ^Z4_c-G01i#|B(1d)O6Q-T}*dZ zQ&l}P508NbSwvZ2HV6~e2#F{XiiD6@WfestDY#Gxoc`>+!1xU$}1gE@O`=lazd2B;0j=_EU{B zLAG9mh1v+^dKN}1yK2IdGl9cnDk?LPTQj`x=-Sz#6&qINLBuUj)nsV7*%+D)RR|SC zs+bO!!`1G0IOSP-mHfV;XP{@`^BB09?c2NJLw#XzC*5}Ixz8Ki_w@|)4D<~24D<~2 z4D<~24D<~24E&!lV2fSsDfsk_+OLP}-@C5(U4Qn!dIow1dIow1dIow1dIow1dIow1 zdIow1dIow1{s$T0F=Nl+kNqE92jlVo|IOe3?|qN4cYwEmH-J9?zX6^AP5=RLJMhsh zjJ*Ur4?F|>9QYaV{>_YC0)7en^Sg|_2wVn!3s~SW;52YO@UQPM_Ac;ezyOZ|r-1#y zzrW4c2f$x}KLQ4L6u2K)1^#vuV}Ai&16~0x0lx+`a2{9&2>AF$*b2N2ya2?&1Hduh zC%_%RCpR$mA@COPCh#oqk8fcP@H+5&AO;=;egxbC>;m4rp0SsK%fPe1ap1?m4}m{@ z6Jvmn!S5yD89)Isa1z)HIPUKN`sZuJ0M(B!^LxxWBlL(4?BBm{FPrN)rSj8C*%4V? zin_~)sMOLuCknnF^AI3avI}EMnq@IOgVK zImviIGAEk|+ERMM6g(2{E2peXtt{M3uYe++)4FgfrHI2;he%ChGvTT3+jzq$2)vAd z>nJ%gSlnH3%`+4F#(U7VXF#)7qoXya=-lZObM3J(w$l*=7#P^p1e-$3@w6is9cNFR zshS}o2;9_b4|P4Av8^+5Ob>mgO?Mul!GIp2yY8ABbcKP~CxphQzOa!l)<4Wa%)Yx~LvM7|c&J$r170=@2V#m(d%{UC> zT%_bY6N64C$nv}_Mp-=H{vYr43r3YX634Ln9{R;27u_EHDt9yC(SpYmXihWLbolV$ z!QutDb8+4uN5h=sP5{wjsnhv^NIY4L$iq*1nE65NkA;dZH)*aXR&$?Yr3pja(l4g+ z3cX%>SLNK~t;FTCwc}@wG~SVuKs!5#%OJ-xx=%!EM`5$DE?u;F&V*HJ3t&iCaGUHI zx;MLRXS!z@cK05-&`Rv?$Kz!lS*egjRh9Sr)}Cd~CbCVWWq~ZeQ%mv6NA!5fr=e8! z&BURxd_5JT&6QTyCQnNQUVXiBn5e8-rGVUK9npQiI(ctBT;!Wot!Vl>7a%bN0alo% zn8aetk>p1-s3NcN+D;1yHC7`B)nbw4y!CJs>eQC=HQ{-Th49|`951biY^mFdvAYtx zw6*cPJ zkhU6p1tuWIzrccS>AK2*FY7-lXOs5))}pOMU@=^dtiH!uWpy4Plc+L{yEUbX&n_ox zbv^#?Su|h8RJ%Yj=RSg(b(&iqkkhfGDxULu+`DI77-Qlryhk{-&J z$;^0iYwW#V%iTG{%BW1aUh*THGt<_)T4;peM0xazyaM6swA(aWiE2(DUg8;lm6~ED zO#c4Umt9nO>v5KhxyY&nh!Y|XGdVH`=DR!%e~{}l*B~uwbK{>0tJSHcqHMb(Y2~O- z=T@ndT$ykS>g7qBKxd|Gv!QG5rGPM^xg2$Oz)T^cYM)dW!^2o?4vUmqXOBVFgPL-C z$g}vs>QeP?_8nZ_uHTCyI+8Y&Bp#O*ZiLyG(OsUKd|k8X{?If=)(K|f+3Js#0n98Vc9WUISl+~&$UMz63> E0U!KUH~;_u