From f0808196c8a0bb579111f61c96052507c559efca Mon Sep 17 00:00:00 2001 From: Josh Kneubuhl Date: Thu, 17 Mar 2022 18:09:42 -0400 Subject: [PATCH 1/8] Addresses Issue #548 by providing a simple guide for running Java chaincode as a service with a local debugger. Signed-off-by: Josh Kneubuhl --- .../chaincode-java/.gitignore | 2 + asset-transfer-basic/chaincode-java/README.md | 164 ++++++++ .../chaincode-java/SCRATCH.md | 49 +++ .../chaincode-java/ccpackage/ccaas.json | 4 + .../chaincode-java/ccpackage/connection.json | 5 + .../chaincode-java/ccpackage/metadata.json | 4 + .../docker/docker-entrypoint.sh | 6 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- asset-transfer-basic/chaincode-java/gradlew | 270 +++++++------ .../chaincode-java/gradlew.bat | 25 +- .../samples/assettransfer/ContractMain.java | 23 ++ ci/azure-pipelines.yml | 4 +- ci/scripts/run-k8s-test-network-basic.sh | 27 +- test-network-k8s/README.md | 11 +- .../asset-transfer-basic-debug/metadata.json | 4 +- .../kube/org1/org1-cc-template.yaml | 3 +- test-network-k8s/network | 92 +++-- test-network-k8s/scripts/chaincode.sh | 374 ++++++++++++------ test-network-k8s/scripts/kind.sh | 11 +- test-network-k8s/scripts/utils.sh | 1 + 21 files changed, 735 insertions(+), 346 deletions(-) create mode 100644 asset-transfer-basic/chaincode-java/.gitignore create mode 100644 asset-transfer-basic/chaincode-java/README.md create mode 100644 asset-transfer-basic/chaincode-java/SCRATCH.md create mode 100644 asset-transfer-basic/chaincode-java/ccpackage/ccaas.json create mode 100644 asset-transfer-basic/chaincode-java/ccpackage/connection.json create mode 100644 asset-transfer-basic/chaincode-java/ccpackage/metadata.json create mode 100644 asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/ContractMain.java diff --git a/asset-transfer-basic/chaincode-java/.gitignore b/asset-transfer-basic/chaincode-java/.gitignore new file mode 100644 index 00000000..bd6fd304 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/.gitignore @@ -0,0 +1,2 @@ +.idea/ +.gradle/ diff --git a/asset-transfer-basic/chaincode-java/README.md b/asset-transfer-basic/chaincode-java/README.md new file mode 100644 index 00000000..ca41f306 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/README.md @@ -0,0 +1,164 @@ + +## Basic asset transfer + +This project demonstrates the use of the Java SDK, running a basic asset transfer contract using the "chaincode as a service" +pattern. + + +## Setup + +In this sample we will employ the [Kubernetes Test Network](../../test-network-k8s) to illustrate a scenario of +building, running, and debugging chaincode on a development workstation. + +This project is also compatible with the legacy chaincode builder pipeline and the compose based test-network. +For additional details, see the [End-to-end with the test-network](../../test-network/CHAINCODE_AS_A_SERVICE_TUTORIAL.md#end-to-end-with-the-the-test-network) +documentation. + +## [Quickstart](../../test-network-k8s#quickstart) + +```shell +export PATH=${PWD}/../../test-network-k8s:$PATH + +network kind + +network up +network channel create +``` + +```shell +network chaincode deploy ${PWD} + +network chaincode invoke asset-transfer-basic '{"Args":["InitLedger"]}' +network chaincode query asset-transfer-basic '{"Args":["ReadAsset","asset1"]}' | jq +``` + +## Detailed Guide + +```shell +network down +network up +network channel create +``` + +```shell +# Build the chaincode docker image +docker build -t hyperledger/fabric-samples/asset-transfer-basic/chaincode-java . + +# Load the docker image directly to the KIND control plane. +# (Alternately, build/tag/push the image to a remote container registry, e.g. localhost:5000) +kind load docker-image hyperledger/fabric-samples/asset-transfer-basic/chaincode-java +``` + +```shell +# Assemble the chaincode package archive +network chaincode package $PWD/ccpackage/ $PWD/build/asset-transfer.tgz + +# Determine the ID for the chaincode package +CORE_CHAINCODE_ID_NAME=$(network chaincode id $PWD/build/asset-transfer.tgz) + +# Launch the chaincode in k8s as Deployment + Service +network chaincode launch $PWD/build/asset-transfer.tgz + +# Complete the chaincode lifecycle +network chaincode install $PWD/build/asset-transfer.tgz +network chaincode approve asset-transfer-basic $CORE_CHAINCODE_ID_NAME +network chaincode commit asset-transfer-basic +``` + +```shell +# execute the smart contract by name +network chaincode invoke asset-transfer-basic '{"Args":["InitLedger"]}' +network chaincode query asset-transfer-basic '{"Args":["ReadAsset","asset1"]}' +``` + +```shell +kubectl -n test-network logs -f deployment/org1peer1-cc-asset-transfer-basic +``` + +## Debugging + +### Build + +```shell +./gradlew shadowJar +``` +or +```shell +docker build -t fabric-samples/asset-transfer-basic/chaincode-java . +``` + + +### Package + +By instructing the peer to connect to chaincode at the Docker host alias `host.docker.internal`, pods running in +Kubernetes will access the local process via a special loopback interface established by KIND. + +Set the "address" attribute in the project's [ccpackage/connection.json](ccpackage/connection.json) descriptor and assemble the chaincode package: +```json +{ + "address": "host.docker.internal:9999", +} +``` + +```shell +network chaincode package $PWD/ccpackage/ $PWD/build/asset-transfer-debug.tgz +``` + +### Launch + +When chaincode is launched locally, it must declare the package ID in the enviroment as if the process had been managed +by the peer's chaincode lifecycle manager. Calculate the package ID and start the chaincode, binding to port 9999 +on the local system: + +```shell +export CHAINCODE_SERVER_ADDRESS=0.0.0.0:9999 +export CORE_CHAINCODE_ID_NAME=$(network chaincode id $PWD/build/asset-transfer-debug.tgz) + +java -jar build/libs/chaincode.jar +``` + +Or using the editor/debugger/IDE of your choice, create a launch target for `ContractMain.main()`, specifying the +environment as above. + +Or launch the chaincode in a Docker container, binding to port 9999 on the host system: + +```shell +docker run \ + --rm \ + --name basic_1.0 \ + -p 9999:9999 \ + -e CHAINCODE_SERVER_ADDRESS \ + -e CORE_CHAINCODE_ID_NAME \ + fabric-samples/asset-transfer-basic/chaincode-java +``` + +### Approve, Invoke, and Query + +After the contract main has launched, install, approve, commit, and invoke the chaincode: + +```shell +# Complete the chaincode lifecycle +export CORE_CHAINCODE_ID_NAME=$(network chaincode id $PWD/build/asset-transfer-debug.tgz) + +network chaincode install $PWD/build/asset-transfer-debug.tgz +network chaincode approve asset-transfer-debug $CORE_CHAINCODE_ID_NAME +network chaincode commit asset-transfer-debug +``` + +```shell +# execute the smart contract by name +network chaincode invoke asset-transfer-debug '{"Args":["InitLedger"]}' +network chaincode query asset-transfer-debug '{"Args":["ReadAsset","asset1"]}' +``` + +## Tear Down + +```shell +network down +``` +or +```shell +network unkind +``` + + diff --git a/asset-transfer-basic/chaincode-java/SCRATCH.md b/asset-transfer-basic/chaincode-java/SCRATCH.md new file mode 100644 index 00000000..02ac7910 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/SCRATCH.md @@ -0,0 +1,49 @@ + + + +# Scratch notes - Ignore - ... `fabric-cli` redux + +`fabric [options] peer [parameters]` + +``` +fabric => network +peer => implicit (from env/context) +channel => implicit (from env/context) +group => chaincode +[params] => --param=value or NETWORK_$GROUP_$COMMAND_$PARAM=value from env +``` + +```shell +network chaincode package +network chaincode id +network chaincode install +network chaincode approve +network chainocde commit +``` + +```shell +network chaincode list +network chaincode delete +network chaincode describe +network chaincode invoke +network chaincode query +``` + +meta / fictitious targets: +``` +network chaincode launch +network chaincode deploy # package, install, LAUNCH, approve, commit +``` + + +ordinal position args vs. named parameters vs. env overrides +```shell +network chaincode package asset-transfer my-chaincode.tar.gz + +network cc package --name=asset-transfer (or NETWORK_CHAINCODE_PACKAGE_NAME=asset-transfer) +network cc package --name= (or NETWORK_${GROUP}_${COMMAND}_${PARAM}=) + + +``` + + diff --git a/asset-transfer-basic/chaincode-java/ccpackage/ccaas.json b/asset-transfer-basic/chaincode-java/ccpackage/ccaas.json new file mode 100644 index 00000000..6f1a9468 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/ccpackage/ccaas.json @@ -0,0 +1,4 @@ +{ + "name": "asset-transfer-basic", + "image": "hyperledger/fabric-samples/asset-transfer-basic/chaincode-java:latest" +} \ No newline at end of file diff --git a/asset-transfer-basic/chaincode-java/ccpackage/connection.json b/asset-transfer-basic/chaincode-java/ccpackage/connection.json new file mode 100644 index 00000000..604b6c5e --- /dev/null +++ b/asset-transfer-basic/chaincode-java/ccpackage/connection.json @@ -0,0 +1,5 @@ +{ + "address": "{{.peername}}-cc-asset-transfer-basic:9999", + "dial_timeout": "10s", + "tls_required": false +} diff --git a/asset-transfer-basic/chaincode-java/ccpackage/metadata.json b/asset-transfer-basic/chaincode-java/ccpackage/metadata.json new file mode 100644 index 00000000..c52d2a5e --- /dev/null +++ b/asset-transfer-basic/chaincode-java/ccpackage/metadata.json @@ -0,0 +1,4 @@ +{ + "type": "ccaas", + "label": "basic_1.0" +} diff --git a/asset-transfer-basic/chaincode-java/docker/docker-entrypoint.sh b/asset-transfer-basic/chaincode-java/docker/docker-entrypoint.sh index 7192c8da..1ff8e07e 100755 --- a/asset-transfer-basic/chaincode-java/docker/docker-entrypoint.sh +++ b/asset-transfer-basic/chaincode-java/docker/docker-entrypoint.sh @@ -7,10 +7,10 @@ set -euo pipefail : ${DEBUG:="false"} if [ "${DEBUG,,}" = "true" ]; then - java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000 -jar /chaincode.jar + exec java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000 -jar /chaincode.jar elif [ "${CORE_PEER_TLS_ENABLED,,}" = "true" ]; then - java -jar /chaincode.jar # todo + exec java -jar /chaincode.jar # todo else - java -jar /chaincode.jar + exec java -jar /chaincode.jar fi diff --git a/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.jar b/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 26482 zcmY(qQ()h3usoc`Y@EinZQHhO+n?CBZQG5}SdG&p-#CrY81L`@o^x|d2#cvn@ISZqiy@{J!yy~>$vM`3ga+e27 zMc9LcPnxiijE&t8XB3o1vM?jPsz>m;`~^w&6pqvZ+&cyyCvo#0#5471Gddisfjf&E zk=xu#_tV_G(Jlby9rF|HzNtH7r8k6W;AZ2;pQ!AUh*7mi)& zPFy4)U@>pbWLANC5Q+{A(sKh3N&qq~l^#{wjTNxE7_z8{FJgNJm;kV`%ak>#+xga@z1? zKI^i!^02KwUY!1IB}2c7TxtgM>fY?JN3tG10QT>myN|uOi6qxfCE7wf1HbS_GyfX# z?EO@xKz7de;{LY4xTwnZSM3gNvAtzm#O#XBw$SQSMZ!suSk3qBk7%jyQ*&H;&8Y2| z{s?b<+$^Gd(MS)Po1UUKttFzQLJ|*4=+k23%i)A0I41rOz^wVCbxdBT#TZ#In+uDa>%M zr*1^jnaNBvB@r{t^~e2KkCOn*iM}`#EOY%K4VOM5QAOp3aA$*I7&KK@(k>D+d@c(A z^=LzXauEa*mG!CEQsVE7CNkrJ--shh!YrUIrr5jlS=wB)GjT#H-PODl*`CoR=@38T zH1-g;H2xg6rZ16pp0rDZQk$$y*^Oh)u8#S|pL%6@v?QxD^ky+`>J9;WXT2RAEyI@& zJkzdI-+#_n_hfspZ-E}U#fs<~5Fw(^|1w-AWN@;#X3g<-IATo*#5k5SokA0N-CBDl z^IzDL%@F%BQq`lGoH=S*p5AmV=O~=sXPn(=Od#pCw}Dfc{$6J8PBqL@!_yY+7Oyfu z8%coPWt8c{ddIn<*MkBgv_petGZ72CmIxEcy|}0x9m)&(q-!_SMk;0cV_nk+1WN0> zOwrt3Ih%7%=n@>WE#NwfP!D7GJqim9 ze)0O2^)dM;Au`dnTGMIFMaE#E@QMcnQ^T6vZkWr9a__)8B07LmYl^X7aWslF6*+P+ z$C_y5PKVKG7OPa4AA4?@ufH%^*{_X?wJ#QWRE!}upOGpT+X-7Awgr!v28njpdcs3d z4u;1APhThftR0LrRb5&X!Uum9V+K}>wu<2&qJ+6U5-`Qs?lpB@p-x~o1wn&Wvd8AH zSWl}PqPnH30RSMTHREkc}+R(K0^>HUc(x9ok4o7dH8ly5w>h220j0N|XipE91BS{%Sfc9!Kx2 zNNUvu2bnR?A_wgw$mHehm}7BmJU}-;cqOi%s1(qwfCW}coBQn zI@XPQe|K!yleYQZ&i9q26)4_GoHJGBdev?9l{451+@y3=C0AAXurMLQ=&kC%S;F0E zY~ouyr2?Mu#cE7k^c~+ry+w8WJ9B2Ub!F)JZ}Vk4E~M~_kiEh7=V2%_2=g14^>Pa0 z3W7mMO-5%eNMi2L2BsKryqHR0GKkBmCXl7%5>pqD(^fp z!Ff*XZ?Hp1(tsmr8kwrpoL3cmcZ{MbwT8Px5#TrT1-n)HN$Xqkd6PgpY!+-y+?QHL z#$)8I)e-Z@Y{)vv2TSTSV1A|Qi|L_3Ea4RpOA`rt>@89|EpuZM`4jp5p|~)RJB};? zQKC`xoy!GUP-=VFqp&K5@@1Ke+>DDBMBXgoRnDj=4qT60<`EiYYv9 zMW8SD@IWE2DD0<(T>1o`Odm^$c+(}^E5&HRenePOSd_S@Vs{qKPDP>k!)5i#zE#P~ zsgFN;^R~11HS}XOnZ`rCSrFmJ`6=GU=;t={z6O34&Ig`BY2wF#Fz!EsfM0|?<(M{6 zsMyNf`RdiwE4E3(m2i(N;IyI%soTUoE>zG~H2kh7(C{fl6=2Bd*x&deA0a1TxhKuH z`?;0QAk6o#z0|_Lz*6m?Z;^c3^aj}>!NAsG{=?J%A3*^aI2dsYOA`-!_tbN2LZDn@ zR}D`C)w-#KU4tl0XsuZ^=f zt$6So_W$+6G*YjH>cxfC3MVvt9E-VJsoC-iGeUD59J~tyRy{BG_(zU@1K%JE5L%ov zNcjJzN87m#d5)!TE`{7!J%@~0fH@WRO#LCNSsIFLC*_N)fs3lEN2lx%UCHSYH{5TvTy2N) zoa@w+jhx9n1@Eot!^&}|%u!ckKgMS3%2b9G{J$wy!|H~#UTS+#+Z!b~yTx7Y=2$;* z#}4f$S$op0l)-gRhcyr!xb2Gx2(S{0mWOE&(P~8?uC=oy7l+~UfVR_apOwM5aJwuH zW-5W}rRFPv=4gi+=^UQ-{P0_sBM3sD>xM+^5m3(;zur?|*v6pX z&Hp^Hc7kT0p!?l?y)25nV(EdSxP~WFtm5|~aqFVcMUi~Db<|bmIX(VMP@)n0xkImb zTo0tEApfUms`9QEfZ*a%RyJ)kA7$a_VN!f^v}KF!@2tUB)wQ2snY2)|mV58GDE2pe zOzJRK+u%j!W_wt0$O5|!kHFesasO!lRZ+}B{uVLL{L<^3cER%6DSVC_AN|0q-2)&M zKMdlwwRJ??Kwz#YbRh9=T+=+lum9K)EHQ@UM&lWor_QM9;=(-SMU-IkavIGE z|(QL&It$0In(lnlAr({mjv#%@t-*V0u&~7z# zD^6+^Uu5zDS4PK*Uca?PY_9w<5`pU75gAh~#lMB1s>8hBT*hdZ&(V?A@FtA;zt1qP zOMS$@pch}h2M{z*AY(kEptk*GH57)}08q%Jev}?tkRUe0M(AMcRG8)A$WGyq{vf-< zL8~E?yKZ0h*1@FgWMHZkXW>XKVW+71wz)t7p^1K`Rf1R2V3(slc`Dvl_iyK{|A1n_ z|2!25e^Yq+$5wgbFZ-G8%HLLht>OAA7NWXi{0W$9xR?wCsgp$Sjs7oND+sCID*lg= z2mXgO?Ei~3q=3dBH#G^2uSt=#;F z{eL^&Zf0jfhitq#B{STDXNbr%+(Ep8&sxuW=p!3jZK-AX7<^|}JiP;Mk9oU1y-$nx zg@Mq{xD*glEV$*MwqU`&+TUy{eUVS;55d96E~|ssw6F7+FgOvNQ{ib>%>k}h=76I{s!v34$$7e)g;JV*_STJfIqA>svR=@7 z19d$5P2Q3Ar%%L?x|F$ZG>N{mTuO1JHIHl0<5A87)@Y6bU37^Zyq}DB#u8qv2`2dE ztnby&Ss*%RfRSLAHT>Ea@xA6iy3sh+Rs)T4_w4l05C+2w(0Oe&?+5voR|rgdU@KNE zjrH!^CA*asZV`zSnqnS#S?N)ruCEW%oE!(tyG}vIp#1VuShI|RejckSdp5i8XCBBx zODEPMC}hC*41vuu@wCKm&?XBo)S@*n=CU_ExjbtPe zXl72cj-C3k(8_~n>bcrO{ zas1`Xy){lpT?kP#MAB4I!oj0RI+^qVd<;6fKZSL~2Peq0hN;H9a(gW2z1L9InzuFS z04il2XVu3Qwmjq;A!YmIM$i+^)}D^XWlHj|trEK~=>{#R0S6&rd zU$JY{jnQbtr;zUF5X{y6Nw3)5Z)z6D2c8{G47||`s5s9|WXrYSe?Pp1wW>-pnI!Fi z1bu6D_!}HiWk{_5s;W>8C~o2`vFF6kEFdqi>cIxcQ$9!(?NUW0%c4ybD_3YLfn2BgSOeANdHD92v#vG=yDK$~RDm08kql zFtsPr?!)91xWnr~ld2#hZRY1!ouJb@{i!t6?<*uN4doZpPd#{zv5)>5#9!{Tz?L^g zq&-&H6*^$N@1%aClq4SkZYaUDLs^Sb>;xi6!QO}?lSm;gTb1Te_>w9Th^#KPdV3=B z#vcVKhX%{A!qW7!is>1@-E(!ti&}GQEHP_@LM_3zh+w(~TM#;l`XH+ujsH?Cbv|Pv zEWUYV4ru;@`oFE5=&UqDg9ZjxMF|E*^1rPN3;d6~9@ByI)>}yzoY38AeZb$6-4~aE z1#jUtl!1j?H;y8Jp}S4d_6$K6^=>Y3plBg4tE5k(NUQMEhHs-UcP|brUsyon84@mH zfb%=EcYi!<%co(R>G!lg|9tAF(rF2oImx}w^}h{xD)77NJ}I~_bh~Od`kHlPLIW0Q zKUHW}76I>5p|b)0(~nN;!0LrB?_Ux`-ls0F>6w8F1Gh_N?7pCSn=f$qniz!&LJXH^&J?-z3W zwntX_MAZEy2(> zj%6~T&X&`1MK_8`+iE_u7M~rJ*r3j0mUOGKpNlWk=5U6u(QP-}K_feWEP>)T4O%a6 z>S2*|u9`lXBSn!*G|S7!1&{60J4@t`w9cyST4(8`SM@6`pIT8W%$;LUt&i~0rRezg z@J1Ey%Bi)QKDncF^;L}>26udn-bf8jbwX7i?IYB5GTLhupGIwL4R3W2b}ClfSe&_@ zx>9)@#&Y+0Jc$4S$J)fx1W@84(8q&Aq=;LsZbfc^YixcO?7p|x)5c>uI`gZ@aa@G{ z`g-U0pVoT6wbjQR<)%tJ`+UEL0ADlpL?o_=8FO;Z?HTn|ti*D80ZYe~QX6Y5tGkXz zr}c%MUOJ5Jow-pF7!?kd+0E@OHw`C0>bBZ?h!%ojvxSEG%HH0e^#C&8#a{%^_NRxZ z%fIlRp3q{KSvo|+`$w4apF58p=drBJ-KDo66PJYW{M$q417}rasrc~^8G&Txl=ysC zv>aM&uk%vqRG6RjlB}4~Bf6N;HjhO1 zvt-4RL0OI!smgo%!@$OL1)e8wITY)5ri3YKYj}0LZ$xQ9d>f`-cAmp+6N!5KXx!Dq zsUz7&<#ht?@ZznAK#zu!uu2liWEdaB~m4S4VN=!+&-vhgoF??0(tYqRX|@puwTvB^O^;E_V^3~ zl-80|jeIr6OQQe^8(yPB#P-Pl6Fk59#x;NXunW~<)p~%@2s_As_f<8hqbpO@4U2*w zgGjbiam;~plVCek7UA$GDa2lA46(I&fXjM1eiV;Oyd)y6>J-$pKqOO_ggMD*Up6B> zlOa&+%s{*u2pBI&N6FAP{JO%4%a&xVkZLB*XH7YeK;i?y>wA~Q#7N=NFRaJLn;)zj zNzxv2T+|Xhahpxt$dyu1=Tf}MM0s+jH}`Ftp-Qkif(-BPSFXgT62ARZhR0>)6mW9o z#UR?Y%L+(VAuHf?n+lx|c43@y9xQ0fmd<7FD^4`+0m#v&p=d2bo8M(9%8_wCCQmWm zJWBQ|FgP(4>H3sFAMixKSfsMBlw4+Fs>g*YgHU*}+5SO5@m7Xs(Rw#(C`mw9lA1`U zOek@s1O-_!b3uEDt%Wp`VDLF~7O<|?IVK`anfKK7NH;KOa?wQ~E$gOTu+AiN>-P~S zGT9(X0L^-(958{iNi#ZzW4M$EwJrJS#+D+zT~r$lRHdjx99)TXyP_#B4uqw8CGhz^ zK6Hl$C2ER;7QcR_OP7V7RXO5*@W|yEZR>?kWMsrn($-NDy94~u%g^wmlg`FDY6j{d zQVSip%}rfp-u1x-Q*>07uIyg?7MLGP-n4~X03#1KsUvu3C~PJL9aCRYb+NVY^h(E> zN6jIarjO{YyXykVZGvvnkt0eU?jH#wZzT2Mb^6Qol19Jrz0C$O7D8c9z?= zajx50_}!9Ql75YY#Cr!^AOx9hmQl5k{ga$%^;zwqV7w6P=lqQo$18q-VJVrrbm`U^ zfI_>42%~$4`XpL4TDW}ry+^II^3=xIVw4>n2`>3Ry~F;=Pb4yj`ujIIj%S|D@|l)G z^ZX_JZt-otf9AN+UN1*Kj8r8e!SZS3L&Ese-H+te*6wl*OKGya!r-L+J2t1!w4^VJ z#qG-5kM|(S0VSYlk?B<6S&>?w>9*n)2z|HEC$U;EIEZ|v!xf^>X}nuw|KtV44y2xQ z;e~2$RRZM?CSE$~I>pg0Cc|+Zn{-%$KYVFXP|tX>6|*F}L{1;WhLvpJ83JVwDqptb zy>kebpK$_353XUi3g1xSaBq1zG8u|g(fJJ_vH?LykIPY9L{}vL;q+&y{hdq69fe0La?^Oz`OYoC-7Md zCl1Zgn4xe551PMX<(f}hfyIIYS~6ZR!-D~=Eqn~#xZt^^&}hBd88L$~!S0SD$`Jed z_CJ&)9h>N)w^T(-z0M|Z2S%Cf-UU7dws$2L>C#yi+%bP}wOKZT)lR z*MsJaddWLYGtxia_A@Fk1McjCh8u0)aRs3P00S;rDq4!(-uYci`Lo*%ft&? zWk)0_DjHe-6=O^qWM3%2++a06Tuv=sPA&Lvd9~#WAYrI=Ky=3^ttG)K;~a3c-XfHaaHK<7;+zT;(y_x&zLAEMCU~)TINcL zyY4hpbBo=7$HEvf8@hp6yJv!5{Fp@a#4O4v@HLNegO8SJt>9#({!?eFA6+xPXC`S(!1Oe1mR z^eW`1AAMe9rmbzFgX$6E7&tbgRhUhgS(k>VNNO{6oy<=&(&mI4mD|T>`mHdH1XM&N z(k85z{BQKoZ%P2@;lh^zr)6YgWXU?16r_O^tQgpPPi+f{jLpi0${M>H8ZeHT33*CE zJ~`uPP&(MKf_T_Fyyg^q&16j1cARdt;||gyNb{AR^yQW+g!01}-RURz{Gm=c2@^)EJ=fYkTiEVsHpcKD_?$6mOz*FhR6-KwE z+$M^~YU~vdQ6|VwyDJmjI=tg9djoXJTVFzd{J4VLe{A-*NaoHpfhEu@q!NXtUZ;_MqJwGW$+PeNOWG06f@-g9< zJ{}LCAQ+xN`>+mKPh2UQs7Dswr%u|N3`b9!=d6wX{3SP|Bj_#ayC*Zv%pH#)NL*8k zQyP=Ad7!SN2ITp31fIC$7#w&JP&MFG@<6nEn6W=`YXmzKI46n`zWPg3*apKj6alI| zXpXv4ZdI7nHl;^x1-8`&xJ6@|WG5Xxd0T(U6=lt@qug^(;b3?zl!_)OW=l8Ty@se6 z-{^OT=hYtC;wd5QAm?LptfB+%l~M*E8*0-^pH zqp^Xn%ctu*FIHwU<@Q_vV;x+EV>i7QR-?O6(?@lt%Jry4;3miR+ueO%L|)@Hhs`~E z*CR)LfKDuPt+1lt6|o(^97v#)u;*6PBDG(8(PjHhIoG+fmS7mc`JB$u^H*KX$Au&Y zWuHmtA0oYA>`ui~u$q52+G9YW)pR3N5Dbd?QL*a0dwXxNKBqQg%qA?6yqD66zL9{3^vkE16LpctMUcKcJ21ZCCm_5TY;><^fNQ_ufE)Ta3x00s5QO-^23JQ{4zq+7m4)~sjmh11P*Wb;qj>BkA>+4DOBkxQxpNb z&{G|3dJw|d+XIBFFaXoB*3(sBD1y*PxgS z!&j}i(;OTgoey;p9qdb9pT3*1DktbpRz)x^4KAVgg~D9sMK+N0_7UWea@ep%RZmM3sTrf#fgeXHR=5bk|+c9#FM%qu2bGGxr#I z#H!|UNROxI4ML$UXXlOKia49EU^cA89VEnm{k5!0t-x5+)Py9r>eoehvu1)~p@@~S zx2fj{FZRMU?*XtxFkvsAzo+K^9409mGW7Rti-jPCWt(2nUdgh@!*`fh8-F3p&rNQ) zab)e36X`u3QgdRagJ5Sm$i&v%S6)0VPjz=0ClK5XA|9@IcO6*>-A(!TZoiAaDc?0nm;YgL z5Bw8EQm9B02^iRu&Ru7R&F6bQm{u4GA6>^S&`)|GU=CI+~Ao%^z?Zg9B z(n8SSo7zKO(To(@vzxawBxi?pXUWbx)CYy{Pib}_ykPKGa$bF+bnkvdwA`lBX=8C| zlQToQyfNSx8O8#BT1g81C=VtuhYtrSGnP_Fa-uDbm9EC!s-QWMRhQ5to{2x+I&#&s z?9O<1DvBpkuT~{vuOFFviYL$D_X_W zrC4?{@p=!v0mq9+f+}SSWo8fp671(j@wY(+_PxhmQHYpb0$gP~F$l~nUUu1lQ%C~- z47xW>_Maik`I29V@3*uiryPGp88>B|q+{K+Cz=>k6MlbqEsew`Z+{u;mJ#y&3$RI^ zw$Y;d2<^l0ScERS5n~=!)D`;*E9ez12P%MOf~}~=C~2P(negqDF7|8FgAyPeFLPmF z@^M!se>8qhh6w#Lc!8(NpX=Kl!tGBSd_!JY%F;Vb#!2--~j1+2K zy%}m=wRnHS)jI*dpmxuPR+hXQa$d_hGvvqft*8)SNDjxOd5hNfJJBt$Pkv}xLtSQ^ zwojC?Uv;e3*>I>x0aaTPAWH=m>%A_ zF}i&QLwxG=>EKsme3;uO6TRwTH;$OsLmzkNKV7{G@prB#VBWimN-&Ury})Sr!y?x_ zGpo`9k}MVbq=c%>4SQs?oS>8#KIe@@I@b}Y4{z-uvb%dT*V_YH!C<8fklJ%w$h`EN zRJnADQ0)99=$^Jeh^giot-o%Mj@EO(7TnG2!p;Vvlj!Wyao4M?)K2Ryxj{f6-=G%? z9CYwX^OL1!A`-XPfewwx7AtGRb{xHfu=5ExUo0a*p=7%tMEN7|n^7%~umdJYdV4)Tj)1L>@I(7|l4Q<@uls+oV}JbTU3{T2 zeL{5Wqdh*deMFomgfy~nzYYouB8t-NBmjuZA`Tbp=OvO@hptrdd-eAJA|{e*mi8?sG&l07~k_`!y&Tnq=x~bvj24K z0H&FO`{V;5jRxqG9M3fp!LRRt;;`RqSp?$qCPnzc_LK##4(miXbmsQ=XF*$CEZ~|??_m_ft zp9y^w;V}y3B@y+E|9rW8vik!>LJK!y@6tnw9Pf~cEU}cStzt@uY*Dy@F@-eG-4RB6 znYg6hIT5INzn$@wv~K8-2F9HrkNvt} z9W$B!-AT|B__RP)XR|ehy&}1xJoj3$&|V_At|#Flh1bd7$sVe)PZ?mSkmDT+Sh?ZC z*&aBki2(L!&qpt35bUCbb>=dFeIwW10Zje|kdU|N5WvApj=>^^E-_pgWvYK*_ zR{i)2qmSO|tby9f!6G$dAsOQnQ?)qiFQZRVw@&i942}{NdiC7M+dNv)CGGC?&2{@mpbR1OsOk9bi~hX0;r>td^htl^*ZL z%$nAgl_!T_t6$94^8SOb-lu2B`*E($LD9vyn2`12!jr|R!~nxnjpEM*REI(-t(A-k zlug5YF2c;6Dj5^8Os|AyoG`T!BuiCp57IIWVHCtm{sIH!IfCwK+pBNGh>t)8GxpAp z4m?pcj^~jz4d6h-kjgjh#8#2T1A{u^06m!dFkQTWb{qbx`&c80Kp2rqNX$HN z`LD5{#8G^GPPDFf`qV@l^K2gE8Uvvs7lbUETg9Gu%ZXtDX3NFuQGiNoTyhno6S8K= zd#>nmOnc_c(ejq8IxS_j;*p0z&xlD6Za4TzmnP25s<0NR9 ztdQl3A)2WOygIcujq6(1t}ZkUV@{u*ZB4N=Ei%_l&M)g|hBF_naDs-Z_`YT6FHXnI z@*UP$5!uMvA0d10PM-sF)kL(Sjg5ClFS`aIX=tpFthvEI`@x?ii2{bnxtGmay8Dlf z+-yo6Ho!^KVp<8w$QgJyqk?zVto&lIC~bVl>_h3yq`J)1wJ6j|MpqMpf;+iVEZl@8 z(`#Bf|8i@FLSpJQ0&YQIw4cG2=OO>paD@*k&+xS3sqshPHtb6wRS+@oz+})*^G(zH3@IvJ~#P50`K?gJCZETUVtRu z$O7?D_Gve~j~>CwLMJ=)J2h!C>SD@qF5P8Wmd3jtfi+^C45&h-ZuUnfmE87r9JiqA z@;)J15Zm(>y0V&f=I|>9XPNW8!;^`nH-7G`X54-To%$k%wW7(rKo(VWuwFoBQ+Z=m zu^*zI)NL7ESj`b>2b?}^<*Y591n>qWO6~VE4{D-54&*}PqmJRB2oyw6ISG}d0uEGnqr>ZZMM*M!q`>A;AXnN%s9RCQY=N?EkXLv^9vB%ig z{4c&4@t=K2`ajkoBMPAH36y`ooiX*?(4GSHcK+rp;Z1*MM<>q2=CBy+(Wfb_ysKZfVrL5*< zKCW?6osl=BS4J~*OrYcPZTyqt?>KOM@{(MJbF=yP&OZ!?N_#E(&>Brsw5Gk0uHZoJ z@~=H8hVv~u#0T--=#72d_)q%`^3R$D{q^`NeLwM8`a*LTQLYY-yr#T=>@b)m)l2UQ zZ1-smzlnMSutWeXD3rusS`uAw29La z$}Y~il}1L*axsPF74x+nlMOy9Z)?k?k>WtC*ec8IX_eS($k~8J$XB+_;5(-vvaMKY z_k((`*Np4yZxZ8bZ*VtNZhfi>#gCl&{(ahTW!0y^;NhMID$R45{5B#-)`tD3ZANOH zb%ofQ3>qL&T~g7(-4%0{-2UBW(FCs0-9o8RpWK>f4OfvdQ_h)z| z-b=jX?<(KwXX3h((RB@B0;~Zqw&3sbZGRGUBX>=P;4ac1vP%5!DSJ z+T_y{&;gUUK6qAGl_s6TXpO>jwPJ4P`J}CEk?ll@B0F4I4+n=uHUs&; zM*qD5&^3I)aM|FJ&9z6Rf|QcAn&^g>dszO(;ZZ9Eoek4?++ccZh~(a@ocyBx0?=9> zJLPG@tGt+PXzj&LtDlj7pJJy{keOkqPgBfkYgcn{+%OoClHOq+pQlpj&!JC#GQnY$ zIaT?1CtTu_nX?en>io)Syg`|sJf4GQ;We&`bv%mgO7A4Ix0$1AG0+v5d0yRtzo73{ z?FjWR&FVz7#qHd9ighWWL(U`O7rQrqAR1N;XkLP3}QJ}&Th5M)o~cHw?*K2 zVdvS=n9<|SDEEmq%Z|J#Fr$$wt=+EN@ce{oj#WHfcwy&Aw)$tg+UGNugFw<;74}_J zHN^@V`DGS92qF%nexliZ z(tc>6m!0;HYVh-Hjm@$WBouvB2i4Y0?#^S1mn290R?qZIv(4Rg@6G$h)$}dijm5ag z{e@A3;HU3@3+5@mrP;7GU!cR{iDEq^@mY?>hX)EzduaUY4d*hZ1i~YQor1Aj8)^g~t%QJ<;#|L^>KVPmzu9Cz+z8n92 z$I3;pDPk4Cz~BR0++YNI>M!oZUM<4g#9ku;%s5IF0?Y(T9bV)E7@(2?va}5yMXnoV z_^r8!D};p9`;=m&ap5Rgvyvzm#|fl)bYq9cZZ zG`zMFb~reXK=4jV04famB*MJfjv>C5vMFM}xnn**k2luigTKcVpG#geDwLEY<$>ke zi^YSh!R0GbC3Ge1nj@^T{j?bQEr1=5yl(<-mn`h=AnckC?hh5*ZYA8GM6`Y)vKaxH zszBUJQ#ePgbHF8bJ>q*afx$kpXDsAnN`<3Isnm9&z&^h37}+}2T&WR%@V8rx$5>!L zs4Ga%K%OW>{x#Et>k%bHNmcP*il@+I!-OF&l+04#L$bp+Ne`?yj(wMh<%Yqiu1r@u zpN?FUseN=NL zW{QYSl1#%1lTM-t&=rMoNvdReTgSH@Bd+>K;ZLIfn_&vIvQGC0|L;f$8Vu~)|6Gp% zjIe-=qno>ly}g*Vg_+&|1e(&*43yACFd}wsIt_a45mu2gFk;kcpf>J`;Z)+1N3B&K zz?UJfE4GU1=@~X0R)9mKA34(OIo@aCLQ~$Bo^LWp4mkzct$f#RJFRTJz~|>Plrcdf z;%IWF36F@0L<9`0!HP=KK~6B_s)s$mjIEKVrGMKJNQ$jGM)p~tr})Pg)V0rIz`d8S z^)T9S6JS`5d1f3wZKq+^2;ym{tzR0^Ks>I^+1+dCb)&v8OJ5D^JaFCpAvofBRpMK% z{ZylAJp8(MyQ~J%A)(k(DCON${3$9y8KbDs=U9*y{*;kva8%>y);TI5bqoR|KFTa~ zpK?1C;6^kXI!9D$CJMPZDsR(Fiji51s?o9?@Ojl;(%>TwGQ&>rxl4voW*L5(Nndev z1@uiTjZ`2u@J=>h!Xwgk7lTqTU$jrbRw<0_Acemct%Etqt^LfEui@`H?w+GXrF&cm@=45sg_sbNQE z`7feU&gwA4pDJ*^x1`c?Fzj-2eC(9eR9;2svKy4_Ip6*h&rdp^cn|Zk9_gm~J0@yf zl5T9;;pZ_XzbIi)@9e=$Dk4$3qJKe)4GUVR;8In+YX`pI>joa-FERlR@;S0Wv!Z5h z(oKk}_J$;E!HCOQ-+s#vXXi{>l0B4C5oP!&miA@N0O5~#J&$m@4{?m*y?vIweUxH1 z^dDwXaYAOPW?7{*-{#?cSXv19&d+)Nj76Imx=Q>1K=?6gUhx%-gk|O86vWM&_Z;Xq z!MA@MrBA6ydu!^~+B?R0!Rpd9&x4uCTCX7#Q*Y z5@?wJ&ED1P!1=4MtO!xH_D$Zblae6jlA-;QUC$J5mVre>3`Wa<*8NA}BO_^fKZ}>T zAfbnu*|EC16k}jtpWab|nGUHSQNNhSEzeG(=u|yNac92JTmIua}>3DSy%ww8UGt6;E8Oml_^ate|kH0yJ!67`kg+ z{f>V39kHt%?T%ZU>H`6K%f}h99lss62GxK2N4sRJpmXg1;i&JxjP`1@#NACo33Ty` zL{-TB)}Jv+q~PwAi?tkg9JXSx?0}Q6(ps^`nj*7YW22d>jdLphFkT8*nrhR5XR_Qg z(|3`=eo??1g>9Dzf5PL40CXHK8!x)IHj?EiN-(mmNtIX{NJVDLITc1;Bi99;~qswx`Mt?gT`Pd72y}H&;V03nx1x8(* z5BNL{-embUgKrXC6p$NK|LCrjIoM+CC6HP zAWT)%&A8-HVSjVy4u=@y*2_7TYJj!QXaQ7s~hMtsFcp+^)5aF{E806gQK%t?;yRAOnx7v+DH&Du$p1e z6kT!+;6?M`c1by_YldVsOMs1>?XEJ&zaP+D#F)s?1bE>}Pg3K&f_^7Ojm2^AE5WD? zPl@M`6NSl;Rp%#jC$OCUrJ>t}YDcTa5)0B=w~CA`?5Uwg;`s*E>=8fR~AWSd~ z*)+d-nL))L)GIj<7Q1}=6V=+k!!Ga0Z_sMop;qVw(!!X&UIfc74M)cAMwn4by2(7- zeh%-yVL)9+8d8_SZyA5!UL_0{R#LwpWKR=1X4LOWs^0qNKSNmYRfyA2I{E;>%3+j<;=$c~HrTovx=P7;P2k55lUaEcaStTrd>QM|=tcLB@ZuZk# zo*62epXHa`M0Qg^2AY$Mwe#3;FD%@QBesjfY??o6L+k%)JA>2cbugi@<`)!OdAuTh zXjVp^q2>q5GMztg3luMFxVI}_m(zLR7K!9HoFJPWu+e&0EGqCC$gQ2VB{nQRlkr}k z0EvI!S^9MjAnsga=ZH>}PTyrdA>rrOMBYeE9%=HOj+Dg0{gIN%b-8|Pl)b2;};n*+}0ICb-DgCXe(Gkf$B6F;{!?43aDA4%KH-Zjy z5M$X_(?(<~|0a=PTIsn_(2}`!*X(7cvG&Q^OVk_$f7QK8f9}~O6((t`7rY+%a5EQW z@Er0@C@%8ZyG3A>%|SH>)*4O*^XwU+rw-KI#Is4hpjhYGJFHO6y_T`+)dopBpjswo zaqC<%?O@Y29puJ0PZOUxz6Iu}m6Nqdbj8BRHgZ{kgS;h4Pm?az#bx?LPaR*Y1W$+6 z;fIJ9k5h2{I>aoL*M_ET{S;X&HE?kx+{O#B2$k)F%}#Hj#+?$fnW2r$c%4RJG7@5) z%@6BdeaSjKVxUP1{kE>jOHtqq&~`zUXmbzHelM-6Mary@J=h4t&G;|olLyLU;$t1N zsK0|>4?9;6=e86*nRvFukQjO1%t_cv&dR1UBtPwsQ8nd$Th+-}WTMeNnK+ZkFWhT9 zdoSY;AoyNZC@l}UMRqHXj}Tg^%$rwtfg5~vQ5n`)N|hm;f2h-JpFO7pkgX0L;p7({ zow$ESfCbk0@%+MY<>|}k$sd@UM<{iYxxYk}J(`lpA0WfJjS--j>d&RX(|J657a+;~ zcug-HXF7ck_za#CO54_9<(x2=S6)n#mg3VmEaH?zw2VD)cJ1Q*?nP0z*>qS)Wbg;gJmuuho z@&TrVcZh+M_M5CtnG-FFe;+E_{BCXNoQc%nl}DAJcbev}!chWX^^E92DgSHO3oM*U z=~Mn*sr9>PeSUt1v1ESt^UT0rtDsiHluxS7__;sF$pQI5-pm2eRzD&8z`GlVE~&{z zeK9dQ0V{Q+(JrsHEvxcxO<;izHl5gh6R8?QqGpEm)IX%@?SCiFy>!?95?GatR$%!Ga*nOw0 z&sNtIzGpG}PM6!d96@hZd4si~wv>60%h4?0EkVF0mDj6Ds={{_>R=X>GqO6Qe-(;a zpP(sVx3j`1(~)pjUgq*Bs@BwbfbwS3W}?yx65t4^#A=jKYKB1=5ECm9YlQ=`I5)e{ ziS%X06VM&v`W{!ZrrH#`*348DTz3b+@fk^TCUl2)aNuCA)i^hQ{4B^WUn(%;#0NxM}=v(2!<6GAl`dV*6d`wZ)0Sv4H_CkgkqG)ey*q)ahpWR zVitCV({2_k=EZWVjp*C!sB`A`PE**o=Y@VvsxzcC$vf#Z&N1O&g{E#(7bj?aompX} z{m@YxTEwnp$_|Pzw$r>ZvKdDjGPR$glCmE*m4>ZFj%(SjY=`cleO<1Cc zJYpODnXL(-`VEWy_0p$>siz@oObGg}^9k`|wb1yOrzN3|DNBd-FE?VR0e5z!2XCj= zG*^t{0%wX}D&cF2sW>xuo{Tv+?-Ew{el*Llkg0dXRjNGJyAt2miuwq(0goTf-4S>_ zrzR>0^SdL-Q5x`K(iN0368(ewwbV(-D{_A#-7%KNO$PT^hXHdkmHg{msvztNYWw zGMp44d#33hqIS<2~24U+!S(9i^`p2H~m1qp&+-Ooyz$g;Kbqn^m+<16{-#Pk}Yb8xzXBA%9sDUvZjX&Xhx-EOTBUq54LR_DLP@msjMM?edSb{whMEwZKHf!_=_&=M6|HEdRH80m+R9 zG5(_YJ)INQ>#)`V`@9h7f#jgxki9iT44oP60&Xl|Ma}7TU8fA7BUPWgyJZevAm%Itc z4sfHlqV1gjI%OCm$JwHl_n@8k5Mh|NbjZbzeaouY|Lv`@u+jyD)7mqpKhALn2HiZz z3n1u60YMPlw<+-t*ySM3p_ejS3oC9NQ=9WnVu)4U5lSl&-)+WIo69Q_1WpD40Fnq} z(j~h#WV*bOGBo(Bc^@#1M)Sn$v?NabCT~ITD{e5GS9PG;y`J23MFQ2MD7_ed~=jmnw zO1v6iQZ|-%QkS^GQP)!8l8Lfuo5RkPxBns~dN`daHNsi&-pnSQ0B%HY*>|qOPPBqf z|Bxsb&yIYZtit!!5d#uC{!qO-UM=UA8_wwVgZA0L`1WxLGw}w-GCo9AF{b#Qn-6X$gg+KynU+=Om#E&t z%=Lp{&XWW^l5`Jwq2b2C1jVT`zt;q6AHzqpAZlKB5~a>8R6nKvxa0&}zP{Z~|8dO} z@EbfHayi0W}C-yApe)vTvrv>c8@}=U^?wwEdI>M#F zJ&CW2kfl-1sj01q-~-@MH1M$+j7Xwp91{e=C%pdx@L_v*l?PrVqQ~S7^n=qO-TEq) z=N;o;KJ^wIIV5(*HX0eT^-hvXa#^g##h1_)BF<_a8BWqFtDwlr9;^D$Hp?cOIhHK7 z3*&~jq9lAuKJn$HW`o|zC}4j*%nJh3^epq*w6upjFH{Li-ov*dJ)#4Kf>SU!UEBQ;>}m1SJU8!WF# zc1Hr2AR7MwImvGfUb3PcuyPdmS){r0STdT+;mu-l=!r4TvdM%8@sM~HXxdL9nXSzI zpXgRVQb8vi_hYCWB^mGwOigtvOK-D-7kb!{jMk@6Ar4Hfqk>O9peub1>C4 z!H1t6-3kzik}Ho7dyt%(TLNCv2z+gWRbuum(dU-@~3B_eBQR^o=!9 z;-6yufj$P|o4fRgCxhGfEtK$H)0RC?9pi+A(Q&jL5=hQ(-cbpTm+j zHrZ5U(m6b8^+zXv5B0+)Qw(wZ_C1so*(E01wepk;iSh8a0FanM*v)1@Gq}oOdw_ z`OSj#rwXiN2fC5pcUTH+=#TCr`DZdg!*DVwlN}lcZrWA`j{4Tw4{>S`HXy1HJ&e(B zT1r<+%^|E@4fV|<3^c8A|N1^OYVk$#z-2HNC+`GXBMnP$d zvZ`~45!eHY5;}6zITC5>6TY^k)(p|>FMvCSDr9AuJ2y0-VF(Q z02l>fwO!c|zg-ANuoc5tA_^(@F9f__`^fsS?NVvhPHphcBVaG5hVeIPt-f3f0ObJt zeOl}bk=H2zN~S0RQCXJj5yccr&yT5?n)mErynRU(FEC%Hd+ZJDRcN=L7LVsEWN~QYD<|>1}l4JP* zHLjZd!6O*&RK+S$y~KPh+ZVrx@##0QWiw6m6*V5Ym^+qzq1q1 z?oVHNb{5T8;>2@6<+fx($X+j`TlAqhbCB2M3?o;jcs0IJH%OOTuDTR~tFP<}l0BXS zcL++Xm+JbLdpvYCd6Cdbbbm!EBemTOU*o%n3Z0*MT9e_OvMZW+Q?ULMiCkkB@m=L% z!c8R8js~M_kn(3E5@a?8agw>9JZA}YY{YLlMot@Ft+ex%6{aS1&*~8)eVM~s(cj+J ze0V_4R0~rS*78MUtQ72Gs1)wq+r)-T(%GLx0>aWvZ+(im6pu0? z?OEl5^)ishBQ;)yU?`_=*T3F(1GT`te(O)Yygua7!5GgR4H5_y^ztkHNI#u?2lIDe zI%lIn0RDyBfe`}+hUQOTT6hcxlB{_EA)3PzGG(S5*+S@(t*XZ9L#}2T!k0x(;?0+A zh`?hKxO$rjGcE%ffy!{0r1TyrA={ubKd8#xq?{B|t45;wp03Znq{4ET4skmNvO1jj zxU389WqIt4Kim|N`lJ2e6-L=2)YQe2P6(TOqpptubqfi@2AW}kSPumuCE=ce(cI-{ zkzFaTgZh*OqA@Ff{o*2-m(&O55v`+b>D{^$t{aE(wi9*Vbl(bRA#Gv%cg{m8?9VPr z-rS#a0w=;fHG5si1xfp1{0Y0`7Vi*947Wu^%nxZP3{f`BxQ&oE<}eLWH{7lv620jF z`}FAxXDfX!W{I(-lu{cAAV0d0zF%9GTd@zf>iOvC(|Jk6PDpda($3JQquyUDzirL$ z%L4{y&F)Gw7>0D&xTIn0Hj4wU*$g`iRj+spDS2SXBT-ZS*) z8=3=oqDzZN)a6k^_Dc!x&8Qt>$`E5(oD4+Il1fYFtzci7F1Iy8CPMKMZU-&_QpXvp zTxQ={_Ha!N$CGgjN19oNLx)`)rW`6tleRmP>~YmtYG=75D%Ei*-3%KvC*qOJesP9t zS*TPu$k&5y8cvOGWW~#irIzEDJY4r(uws38nY71$&(i@uhgzY~ktIPD&2-IUBoI&z zKih>?*4CXUY!h%oF3M4@r8*UkYF|B2|FTuZ&9zISF>^^~nx3qWKb|E7Vu- zpUXAT1>76ZstqlNS{o>vHMY|{i=MRfg)BR>F^lGaZ|o!Zt5dWXvV_HE0?C5;^uQ*wd09ov)N_L*in8xqrkgwr~z#dW4}Fb2>!Y$j7;m4 z9iA;Gvqg+da2_5U#>Q-n4V-H1;A#!d-Y`IBcb-LXSCw71F3BOp_8%AS=wy}sloV`q zZj0NIaz@ku@sILA06+gqo?ugoxXlb+-x9)t#K-s5!5F#!4h|QQyTy58+F-pc3+^yB z5N+(KRA5uHs!y9@Dj1wyvj|i9ooV$b(V0bKMwZi9&2M9qqN~}^dPFpHtV&*bpy`@i z%3W%Ww;b8x#?f6Zl2bR-6j2%svX{N3Eiy1|+TSaHC}!GQI3%Bua~Es37RquB?>Mr{ zQ4?s46;~KcUE0L*Kj*HZ?{e_jr!A-Kwr*bcH8EoNcb%q+_lCOT?`ILmupZCqdz4#! zId$Dlq6dafqX0rXCM`}i2zc1!*GIPo!bL2MiJ<)qldnSPB-{S-1Y) zJf_*gnqn0@60^>Tu;Zl1@=0K`8BdVk)tDdyA_dO%nPO{+xtsd<%?SWF`J#i0&Dr5G z;!B8S@A^TyX<dG zS7MMD&D;}i!2pf`Q?}?I+Gw-l?jTc=(IOID^T5C+@dmc<( z$fi~WS1`|HgZf9w+G_@WicHSSTdU&Sve0EEL~cLIW#1dy3%BwQrfQCGtMWpDS?|$( z77m-QC}g(CcJL_YB^H%)V-tdHWcc}%S~3p#);f7<=<+mMvV>(Q8YfwZEQWqft+C%_ zlV$b~3P_;9)5Qk6d_Wk2KG9|2(~N1)K)QIbF4w!;8Lk%`A6zL3QlF~iJH&Vtlz*@Q zgmx#Rz8U|#L?mViGG$XK^bjZndo%x@I74cce(>Cr$=}2Gu7&p;a7~=N=br4?rd54q z&AaH9AGz=C4hJWn(BrBpE$l==n!EgqJaQra6c#iwX&@ZKBIg$n55(cV_?AwE1jz+* z@G9ThQB5td$@>7ILSbIC=?VM~SWLV}1Vb3&gAu_q-4N5bE3@xI)C-y6D>{8s4-ac% zWYt+`R~l@Cpu2{oLq7hdFeEdS0oFP+W0;HjyN8tB^hF&dL;0^Nk zAvPb`B5m#=X_re^kwDr7=;M<-L<5a$@K}Ww`y*;&y zVS09Pv+Y@R>kF5rLnv`DEA}EK{1YAVl&S0cRB-o$eg^-B+Ta2(oY%Dud*&fXEd!-} zGl}*Qk$!B;@{zRGUh^v@3KgEYkNgW8QU@|r*#-|PnurZm@}`8)3so_{dxv=%j`xMR z4gsqwKs-4L7e!3=Y`wwYP<)AcS;`k7yIZSM@}r!|@1pZh(N5#`fhKwlgxJp+BcmA( z?R)|pKK@TnzwkQAG^sc#r)4D6#Zf7+O$aYdkZyQG`A$&Bis56~Vqbx`uv^oT$Tr9a zC}Zgn#c?KZz)&_P*8MYc%9MIyT9zoD8jS*|XF2bW*^b)Bvb8n*gu)QkntnVu|$w~RkN_?9lZGsjl zsB7{*hA^BF&*46D(WGUHs_Pc$iP^}a(TBdogQ|5+YJ`-IheMs&3#}u&Y6;MH+FkH7 zKf^a*?R?4@q6-m!&nc(FuP$MHxn$O2T6%}1kz8B_!2vMrWtgyECb>tedrJ(Zg;~f; zfk$Q=m4dGuRKN0tIWyVu*W;gxEBbxatRo%2@u7ey!?AETJ-e}O)aK+y`R_Ad9$NT! zSZdXkew24*om5V6cd7fDw?u&E7tBbAa+-G{bZlT*RIRhW$n`ytIW~ z2{RuFxyKkHtc3CqT@kmDt#B&Ci*#NgpPX6Demg_YDtJ1z6o4@TCKQqnUQac7z*s%P ztX;iJYiNIxkebPo(wwx!d2fqVVT*js6au4HT?}tWE3HoBh`K5%R&GBtECm~e)k)la zKyC%M$O_LZ=pVSGN0m-d(Yc{{dBp1-7huAnKwWv8z!)nGu$X7C>|@m=ccwjQT<<3A zH2YjMWXB;?%P;HvaW~x_*#jZCBT%XL4dA>zk{siFF%rw|oIT3;`*Am5Ot$gzPtF8% zWeQX~>$e3!j*e2q`Tg>;xj{Pv-a?$0)iIx9E4G_zruhqHiO7B+7RmZXxY(pGTFECG zMi3FKAZ9p2@#c!F3u2jO+5vH$dPy&<((IE@A2*0SaS5+w>`fMww%=rKOf(-Y)jk{- z36(uwWE=Uzy$K6rS28_^N0(<0tr|>#k{j#1#ZT>N3MvDWMvkEjl)n2$BLV4mizOmo z*>0lV;ucP13vi2Vji%$~MFD%fzRz@s_Vx`mW^!0cR_N1gEc1R_2KvNRrl~e|+-m&O zL*rZJGT*czw?a>Ix1-Cn(W4u>R_khRYWnvN@$=<&nHx!%ww%^xtL;T8WmFNn%!(EE zr48iQieIRHu%lJ{1TyP&7%xKHiWcd3?AYlV71bISO0C-}q2F^^2R*eUaILfj-W`i8 zSuml&rRcb-FVhRUqm69e1{+W6`Lyp(lI!U~@1%b=S{IgnPF9$zMz2jgOj5X$IIU77 z3@SCinsr;ubFgT>mrQIt!?4t|TsHT1YjyCf3Mmzz8Bkf;K0xY5s&9a}Nc$c6>&H^` zj1vtpK4Y4%uAmBLV)c|0?h-~MJeg>1x!F1={;&75Sk5emeo+Wf7)5RB!tZu|c1@x#g(E@R; zx>kzJU7C|**nE!Wa-o74P`9O}22@9!-}iD#Y?JAr?nt-nI{H?KO6hpc60+Qd*7?hb z_N)Sf4H)nVp7ose5#slpcOtA0j{`6S(gdDtille5E2U{-i+Zcp)TDl1sSK4C9$b0* zI)#!f_ySM8hA-wEqpd?D1L@H0059{iRb$?Dl$ntvEe}1ZS4fZ)_@Hw`Oj{?+>G<8B08Z8gl}3(lwT>oI4hOts70g{u<(+C$7cNxNPaqN-9GeW*Ijo6K$@RBisgw1 z7))jb&{anID6mvot?(p8XPw`)Dt^toOS=M{>1CH4gD#UTd^GA;#Q${7Rk-53zjpofi57;0qQX zlllPElZEE}?@hrsR)$jH?e9~%QOT4pnyBu#nG$C)CXmz;FeS5F5vLF=oL=Q-k8hgN z2e4W`sU|+dBGkf%bO6wOk5tDeUy}c5uwY<>pi_QS(919|WK8?O14je*xs7`)e+E`Q zfkDbiazD{EFi1etMyk9{ssf*p1#KuEBcH>~d^(8e8jEbX#6M>*=`48_E;B)BS zEsrPf&o&Ndz8%2oZaej6ZN=l}iW>6!;;Iow_dF!pdxbg=)250N>G@Wmt=RI?<1j+h+!aYl|eUD;XWz2h+2>`M_Z!O_)}Y@ zU79TN1lEURcqBjf&xQ6L`J4k&#DOU+BT5ZacFYOW5WP+Myn-GOcy?4W)%YimRYTr# z6z#MTJ?1qv5)(gqiL%5Z-Z{7mCW4lNEihhY7_8;uhCG2&|5cn74*ql0AwO)k2JyVA zRYOgUO)YnMeZB$Z+_V`l%WCosNLI~dhMUb$aL2pgIdgjSgZz6BqFOzxw9sW5#~lAq zL&lR5$Y$)PPv_`$G+xiYHc@bCs3(R}mh5R~KDHd7=<8*lu7!RI<<2#`)7YuSNs|gcXI}mO?)|E5 zY>4?cp?l3#bu4XN5z|k#&=#MMyW33k6%I{39FSPLFjan^l2LiSVf_)}$>Vyl5;_S( zUXE<911P~U86#CInQ>S!0fPU-^Paxbk(r@kimk!$2)m)Km(KVIN@b6gBU#vuu9ZNg zk%Qy=vJ1!0e8jl=5y`DbxM}HSC+^Gh4NegkbMh!GeP$@RmYS161t=+BG`gJibAP-o z$AW}s>SiinmwyK>d@N=GR;H`Ee!VwPtJl^^QlU0VEQ#Z;t$xe(|FU zg2BFe6@d+IYb+P`8fH^|apO1ifiL>MEd&UyZ+IH*E$Mx;Tc*f0^ zz10_G))Yj+*r@6(uvo-F&@Iv#gNE`>HsZ*_CWVbzAGD_AKozG=po zq6$1oJ5ZqCA1ZmfV}>NuFlhBju=yJ#U@K~(wAI+JG(i^ldY~!JOqK3F=*CFHPvQx+ ziiW66!Q~TXZFs`i#!uPaPb)Nt(*e6@K0%V7*WBIxB>Z)&W^f`_^2Eri%j_71>c)R? zrgKF2d>x0crGX9aoqtuaVOzk=XRyfr^z)-!2QO%UK;d<%XYW)=WKl-rOwNTnjUlMEqz`aH$!YLl{Z?!qcj$OP>tmgQukCpWqhupG-kv& z?fX09IBB8^RB~7o*5O_f^~eP_<<>w@HIN|uQJZ3|cP_ZLdoS9?k@-iaw%SIf_5${4 z@AnjM+Ib$y8{#IG$rI+pZf&6{2-~1f^$$XZ`?91-p0qh5&?EN7+SfM^bmR9d+@Zsx zR~f%;_*47(RWyR!dM!`LH?@E2s7R~@coHP?ihk~h({;kM+mvvPj1xpH^*hi(^{zr$ zfZCTGfW?OpP!D|J_~Gn$Jio)q%Vcqe)=INwQ^!GF-bCsQyjz_x6yBTs0HHRoz1wtA zkCJ%TN09Zt&jJ_8@OT3yIpES2Ilq2-TqZ&EJMGe9@N1=j zm4mbjkC?`K!XCaWRTL^@N9V1oO^a_0!ethy$7{`4Vqdf$Vb3C{rpobM|Ip#SJ(%{J z?2#b9KHZrR{#Ebie#faU2;}+K5)(;L#_&dFdRN)8aR^fV$&U`mbhY5#tY!^(lXo}L z0TX9kO&!uHv?(1Oo?RvXJ6h!&wJp)V$LUQ_Vhrheqsb~DMa z`uGG+AaJ<~&c;bC9sw0IHglVHVT@LVWh13S#RWUkgSP`)-LesJ!{EE50{s%1v}ugu zg@iOYhBERZV-~2*WOfM=C^lX?4{2ww3d>9ypzj}Yo`T}Bt>W?r0(D?qiXO579|0;a^ z3xECpKoAxn^!ErR_lIXT^bLSr_m3JQhxTVQ|GUHj1H=25- z0IxlvoD%{78BeIwgaKg2`{nW)YHLFUMW18<1Vf-xVPGhkBLUR*MFj=^QI9NvFRhr+ znMn}9JLr!Ib&4C%8U7+tLd~ZL!2gZ7z`zLn_3&j8e?Us!An4v8CUkcS1W=CoW1^m> z0RQLb5fuhT@Gn2{_&=ci0HQyB)zctATGAiW*)$jUuleO)ezK`hu^B6XYt|p^#EcZc zj`;03jtWBG;c=B+P*Tf#okMD|BU!AK>`)PY=pD zSkQnOOsM`m2+&*o$CN(L0{+)~j{j0-YfJw!$^GjAp6Xse6wp@-Zvff@Q1VU+sNRcc vHt`~|ybLNPbYuYph?s`%EtmrgW}(W9LbwPoGvycF?w3>UrOP+-|I+>sx~Cb5 delta 22605 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 diff --git a/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.properties b/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.properties index bb8b2fc2..84d1f85f 100644 --- a/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.properties +++ b/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/asset-transfer-basic/chaincode-java/gradlew b/asset-transfer-basic/chaincode-java/gradlew index 83f2acfd..1b6c7873 100755 --- a/asset-transfer-basic/chaincode-java/gradlew +++ b/asset-transfer-basic/chaincode-java/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,78 +17,113 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -105,84 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done fi +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/asset-transfer-basic/chaincode-java/gradlew.bat b/asset-transfer-basic/chaincode-java/gradlew.bat index 9618d8d9..107acd32 100644 --- a/asset-transfer-basic/chaincode-java/gradlew.bat +++ b/asset-transfer-basic/chaincode-java/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/ContractMain.java b/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/ContractMain.java new file mode 100644 index 00000000..18b07e27 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/ContractMain.java @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.fabric.samples.assettransfer; + +import org.hyperledger.fabric.contract.ContractRouter; + +public final class ContractMain { + + private ContractMain() { + } + + public static void main(final String[] args) throws Exception { + if (!System.getenv().containsKey("CHAINCODE_SERVER_ADDRESS")) { + throw new IllegalArgumentException("Missing required 'CHAINCODE_SERVER_ADDRESS' parameter from env"); + + } else if (!System.getenv().containsKey("CORE_CHAINCODE_ID_NAME")) { + throw new IllegalArgumentException("Missing required 'CORE_CHAINCODE_ID_NAME' parameter from env"); + } + + ContractRouter.main(args); + } +} diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index c1ace394..b75aef48 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -130,12 +130,12 @@ jobs: displayName: Run Test Network Basic Chaincode - job: KubeTestNetworkBasic - displayName: Kube Test Network Basic + displayName: Kube Test Network pool: vmImage: ubuntu-20.04 strategy: matrix: - Docker-Typescript: + Typescript: CLIENT_LANGUAGE: typescript steps: - template: templates/install-k8s-deps.yml diff --git a/ci/scripts/run-k8s-test-network-basic.sh b/ci/scripts/run-k8s-test-network-basic.sh index 6ce21f47..e4404e24 100755 --- a/ci/scripts/run-k8s-test-network-basic.sh +++ b/ci/scripts/run-k8s-test-network-basic.sh @@ -19,9 +19,10 @@ export TEST_NETWORK_FABRIC_CA_VERSION=amd64-${FABRIC_VERSION}-stable # test-network-k8s parameters export TEST_TAG=$(git describe) export TEST_NETWORK_KIND_CLUSTER_NAME=${TEST_NETWORK_KIND_CLUSTER_NAME:-kind} + +# asset-transfer-basic chaincode target export TEST_NETWORK_CHAINCODE_NAME=${TEST_NETWORK_CHAINCODE_NAME:-asset-transfer-basic} -export TEST_NETWORK_CHAINCODE_IMAGE=${TEST_NETWORK_CHAINCODE_NAME}:${TEST_TAG} -export TEST_NETWORK_CHAINCODE_PATH=${TEST_NETWORK_CHAINCODE_PATH:-../asset-transfer-basic/chaincode-external} +export TEST_NETWORK_CHAINCODE_PATH=${TEST_NETWORK_CHAINCODE_PATH:-$PWD/../asset-transfer-basic/chaincode-java} # gateway client application parameters export GATEWAY_CLIENT_APPLICATION_PATH=${GATEWAY_CLIENT_APPLICATION_PATH:-../asset-transfer-basic/application-gateway-${CLIENT_LANGUAGE}} @@ -44,12 +45,10 @@ function print() { function touteSuite() { createCluster - buildChaincodeImage } function quitterLaScene() { destroyCluster - scrubCCImages } function createCluster() { @@ -62,19 +61,6 @@ function destroyCluster() { ./network unkind } -function buildChaincodeImage() { - print "Building chaincode image $TEST_NETWORK_CHAINCODE_IMAGE" - ${CONTAINER_CLI} build -t $TEST_NETWORK_CHAINCODE_IMAGE $TEST_NETWORK_CHAINCODE_PATH - - # todo: work with local reg, or k3s, or KIND, or ... - kind load docker-image $TEST_NETWORK_CHAINCODE_IMAGE -} - -function scrubCCImages() { - print "Scrubbing chaincode images" - ${CONTAINER_CLI} rmi $TEST_NETWORK_CHAINCODE_IMAGE -} - function createNetwork() { print "Launching network" ./network up @@ -84,7 +70,7 @@ function createNetwork() { kubectl -n test-network port-forward svc/org1-peer1 7051:7051 & print "Deploying chaincode" - ./network chaincode deploy + ./network chaincode deploy $TEST_NETWORK_CHAINCODE_PATH print "Extracting certificates" kubectl \ @@ -109,13 +95,12 @@ function stopNetwork() { touteSuite trap "quitterLaScene" EXIT -# invoke / query createNetwork print "Inserting and querying assets" -( ./network chaincode invoke '{"Args":["InitLedger"]}' \ +( ./network chaincode invoke $CHAINCODE_NAME '{"Args":["InitLedger"]}' \ && sleep 5 \ - && ./network chaincode query '{"Args":["ReadAsset","asset1"]}' ) + && ./network chaincode query $CHAINCODE_NAME '{"Args":["ReadAsset","asset1"]}' ) print "OK" print "Running rest-easy test" diff --git a/test-network-k8s/README.md b/test-network-k8s/README.md index 6656d8e1..7dcd8894 100644 --- a/test-network-k8s/README.md +++ b/test-network-k8s/README.md @@ -33,17 +33,18 @@ Launch the network, create a channel, and deploy the [basic-asset-transfer](../a ```shell ./network up ./network channel create -./network chaincode deploy + +./network chaincode deploy $PWD/../asset-transfer-basic/chaincode-java ``` Invoke and query chaincode: ```shell -./network chaincode invoke '{"Args":["CreateAsset","1","blue","35","tom","1000"]}' -./network chaincode query '{"Args":["ReadAsset","1"]}' +./network chaincode invoke asset-transfer-basic '{"Args":["CreateAsset","1","blue","35","tom","1000"]}' +./network chaincode query asset-transfer-basic '{"Args":["ReadAsset","1"]}' ``` Access the blockchain with a [REST API](https://github.com/hyperledger/fabric-samples/tree/main/asset-transfer-basic/rest-api-typescript): -``` +```shell ./network rest-easy ``` @@ -60,7 +61,7 @@ Tear down the cluster: ## [Detailed Guides](docs/README.md) -- [`./network`](docs/NETWORK.md) +- [`network`](docs/NETWORK.md) - [Working with Kubernetes](docs/KUBERNETES.md) - [Certificate Authorities](docs/CA.md) - [Launching the Test Network](docs/TEST_NETWORK.md) diff --git a/test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json b/test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json index bb7056c0..c52d2a5e 100644 --- a/test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json +++ b/test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json @@ -1,4 +1,4 @@ { - "type": "external", + "type": "ccaas", "label": "basic_1.0" -} \ No newline at end of file +} diff --git a/test-network-k8s/kube/org1/org1-cc-template.yaml b/test-network-k8s/kube/org1/org1-cc-template.yaml index c86c4208..374289dc 100644 --- a/test-network-k8s/kube/org1/org1-cc-template.yaml +++ b/test-network-k8s/kube/org1/org1-cc-template.yaml @@ -21,12 +21,13 @@ spec: containers: - name: main image: {{CHAINCODE_IMAGE}} + imagePullPolicy: IfNotPresent env: - name: CHAINCODE_SERVER_ADDRESS value: 0.0.0.0:9999 # todo: load with an envFrom and a dynamic config map with the ID. - - name: CHAINCODE_ID + - name: CORE_CHAINCODE_ID_NAME value: {{CHAINCODE_ID}} ports: - containerPort: 9999 diff --git a/test-network-k8s/network b/test-network-k8s/network index b53b0634..3482e8f6 100755 --- a/test-network-k8s/network +++ b/test-network-k8s/network @@ -9,16 +9,17 @@ set -o errexit # todo: better handling for input parameters. # todo: skip storage volume init if deploying to a remote / cloud cluster (ICP IKS ROKS etc...) # todo: for logging, set up a stack and allow multi-line status output codes -# todo: refactor - lots of for-org-in-0-to-2-... # todo: find a better technique for passing input commands to a remote kube exec # todo: register tls csr.hosts w/ kube DNS domain .NS.svc.cluster.local # todo: user:pass auth for tls and ecert bootstrap admins. here and in the server-config.yaml # todo: set tls.certfiles= ... arg in deployment env / yaml -# todo: refactor chaincode install to support other chaincode routines # todo: consider using templates for boilerplate network nodes (orderers, peers, ...) -# todo: allow the user to specify the chaincode name (hardcoded as 'basic') both in install and invoke/query # todo: track down a nasty bug whereby the CA service endpoints (kube services) will occasionally reject TCP connections after network down/up. This is patched by introducing a 10s sleep after the deployments are up... -# todo: refactor query/invoke to specify chaincode name (-n param) + +# todo: allow relative paths for input arguments. +cd "$(dirname "$0")" + + CONTAINER_CLI=${CONTAINER_CLI:-docker} FABRIC_VERSION=${TEST_NETWORK_FABRIC_VERSION:-2.4} @@ -35,15 +36,15 @@ LOCAL_REGISTRY_PORT=${TEST_NETWORK_LOCAL_REGISTRY_PORT:-5000} STAGE_DOCKER_IMAGES=${TEST_NETWORK_STAGE_DOCKER_IMAGES:-false} NGINX_HTTP_PORT=${TEST_NETWORK_INGRESS_HTTP_PORT:-80} NGINX_HTTPS_PORT=${TEST_NETWORK_INGRESS_HTTPS_PORT:-443} -CHAINCODE_NAME=${TEST_NETWORK_CHAINCODE_NAME:-asset-transfer-basic} -CHAINCODE_IMAGE=${TEST_NETWORK_CHAINCODE_IMAGE:-ghcr.io/hyperledgendary/fabric-ccaas-asset-transfer-basic:latest} -CHAINCODE_LABEL=${TEST_NETWORK_CHAINCODE_LABEL:-basic_1.0} + # todo: more complicated config, as these bleed into the yaml descriptors (sed? kustomize? helm (no)? tkn? ansible?...) or other script locations TLSADMIN_AUTH=tlsadmin:tlsadminpw RCAADMIN_AUTH=rcaadmin:rcaadminpw function print_help() { + set +x + log log "--- Fabric Information" log "Fabric Version \t\t: ${FABRIC_VERSION}" @@ -52,11 +53,6 @@ function print_help() { log "Network name \t\t: ${NETWORK_NAME}" log "Channel name \t\t: ${CHANNEL_NAME}" log - log "--- Chaincode Information" - log "Chaincode name \t\t: ${CHAINCODE_NAME}" - log "Chaincode image \t: ${CHAINCODE_IMAGE}" - log "Chaincode label \t: ${CHAINCODE_LABEL}" - log log "--- Cluster Information" log "Cluster name \t\t: ${CLUSTER_NAME}" log "Cluster namespace \t: ${NS}" @@ -71,8 +67,6 @@ function print_help() { log "Debug log file \t\t: ${DEBUG_FILE}" log - - echo todo: help output, parse mode, flags, env, etc. } @@ -102,8 +96,6 @@ else shift fi - - if [ "${MODE}" == "kind" ]; then log "Initializing KIND cluster \"${CLUSTER_NAME}\":" kind_init @@ -130,10 +122,10 @@ elif [ "${MODE}" == "down" ]; then log "🏁 - Fabric network is down." elif [ "${MODE}" == "channel" ]; then - ACTION=$1 + COMMAND=$1 shift - if [ "${ACTION}" == "create" ]; then + if [ "${COMMAND}" == "create" ]; then log "Creating channel \"${CHANNEL_NAME}\":" channel_up log "🏁 - Channel is ready." @@ -143,37 +135,41 @@ elif [ "${MODE}" == "channel" ]; then exit 1 fi -elif [ "${MODE}" == "chaincode" ]; then - ACTION=$1 - shift +elif [[ "${MODE}" == "chaincode" || "${MODE}" == "cc" ]]; then + cc_command_group $@ - if [ "${ACTION}" == "deploy" ]; then - log "Deploying chaincode \"${CHAINCODE_NAME}\":" - deploy_chaincode - log "🏁 - Chaincode is ready." - - elif [ "${ACTION}" == "install" ]; then - log "Installing chaincode \"${CHAINCODE_NAME}\":" - install_chaincode - log "🏁 - Chaincode is installed with CHAINCODE_ID=${CHAINCODE_ID}" - - elif [ "${ACTION}" == "activate" ]; then - log "Activating chaincode \"${CHAINCODE_NAME}\":" - activate_chaincode - log "🏁 - Chaincode is activated with CHAINCODE_ID=${CHAINCODE_ID}" - - elif [ "${ACTION}" == "invoke" ]; then - invoke_chaincode $@ 2>> ${LOG_FILE} - - elif [ "${ACTION}" == "query" ]; then - query_chaincode $@ >> ${LOG_FILE} - - elif [ "${ACTION}" == "metadata" ]; then - query_chaincode_metadata >> ${LOG_FILE} - else - print_help - exit 1 - fi +#elif [ "${MODE}" == "chaincode" ]; then +# COMMAND=$1 +# shift +# +# if [ "${COMMAND}" == "deploy" ]; then +# log "Deploying chaincode \"${CHAINCODE_NAME}\":" +# deploy_chaincode +# log "🏁 - Chaincode is ready." +# +# elif [ "${COMMAND}" == "install" ]; then +# log "Installing chaincode \"${CHAINCODE_NAME}\":" +# install_chaincode +# log "🏁 - Chaincode is installed with CHAINCODE_ID=${CHAINCODE_ID}" +# +# elif [ "${COMMAND}" == "activate" ]; then +# log "Activating chaincode \"${CHAINCODE_NAME}\":" +# activate_chaincode +# log "🏁 - Chaincode is activated with CHAINCODE_ID=${CHAINCODE_ID}" +# +# elif [ "${COMMAND}" == "invoke" ]; then +# invoke_chaincode $@ 2>> ${LOG_FILE} +# +# elif [ "${COMMAND}" == "query" ]; then +# query_chaincode $@ >> ${LOG_FILE} +# +# elif [ "${COMMAND}" == "metadata" ]; then +# query_chaincode_metadata >> ${LOG_FILE} +# +# else +# print_help +# exit 1 +# fi elif [ "${MODE}" == "anchor" ]; then update_anchor_peers $@ diff --git a/test-network-k8s/scripts/chaincode.sh b/test-network-k8s/scripts/chaincode.sh index 5eee57e7..181ee4e5 100755 --- a/test-network-k8s/scripts/chaincode.sh +++ b/test-network-k8s/scripts/chaincode.sh @@ -5,16 +5,99 @@ # SPDX-License-Identifier: Apache-2.0 # -function package_chaincode_for() { - local org=$1 - local cc_folder="chaincode/${CHAINCODE_NAME}" - local build_folder="build/chaincode" - local cc_archive="${build_folder}/${CHAINCODE_NAME}.tgz" - push_fn "Packaging chaincode folder ${cc_folder}" +# Convenience routine to "do everything" required to bring up a sample CC. +function deploy_chaincode() { + local cc_folder=$1 + local build_folder=${cc_folder}/build + local cc_package=${build_folder}/chaincode.tgz + + build_chaincode_image ${cc_folder} mkdir -p ${build_folder} - tar -C ${cc_folder} -zcf ${cc_folder}/code.tar.gz connection.json + package_chaincode ${cc_folder}/ccpackage ${cc_package} + extract_chaincode_image ${cc_package} + extract_chaincode_name ${cc_package} + + launch_chaincode ${cc_package} + install_chaincode ${cc_package} + approve_chaincode ${CHAINCODE_NAME} ${CHAINCODE_ID} + commit_chaincode ${CHAINCODE_NAME} +} + +function query_chaincode() { + local cc_name=$1 + shift + + set -x + # todo: mangle additional $@ parameters with bash escape quotations + echo ' + export CORE_PEER_ADDRESS=org1-peer1:7051 + peer chaincode query -n '${cc_name}' -C '${CHANNEL_NAME}' -c '"'$@'"' + ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash +} + +function query_chaincode_metadata() { + local cc_name=$1 + shift + + set -x + local args='{"Args":["org.hyperledger.fabric:GetMetadata"]}' + # todo: mangle additional $@ parameters with bash escape quotations + log 'Org1-Peer1:' + echo ' + export CORE_PEER_ADDRESS=org1-peer1:7051 + peer chaincode query -n '${cc_name}' -C '${CHANNEL_NAME}' -c '"'$args'"' + ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash + + log '' + log 'Org1-Peer2:' + echo ' + export CORE_PEER_ADDRESS=org1-peer2:7051 + peer chaincode query -n '${cc_name}' -C '${CHANNEL_NAME}' -c '"'$args'"' + ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash +} + +function invoke_chaincode() { + local cc_name=$1 + shift + + # set -x + # todo: mangle additional $@ parameters with bash escape quotations + echo ' + export CORE_PEER_ADDRESS=org1-peer1:7051 + peer chaincode \ + invoke \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem \ + -n '${cc_name}' \ + -C '${CHANNEL_NAME}' \ + -c '"'$@'"' + ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash + + sleep 2 +} + +function build_chaincode_image() { + local cc_folder=$1 + local cc_image=$(jq -r .image ${cc_folder}/ccpackage/ccaas.json) + + push_fn "Building chaincode image ${cc_image}" + + docker build -t ${cc_image} ${cc_folder} + + kind load docker-image ${cc_image} + + pop_fn +} + +function package_chaincode() { + local cc_folder=$1 + local cc_archive=$2 + local archive_name=$(basename $cc_archive) + push_fn "Packaging chaincode ${archive_name}" + + tar -C ${cc_folder} -zcf ${cc_folder}/code.tar.gz connection.json ccaas.json tar -C ${cc_folder} -zcf ${cc_archive} code.tar.gz metadata.json rm ${cc_folder}/code.tar.gz @@ -22,32 +105,6 @@ function package_chaincode_for() { pop_fn } -# Copy the chaincode archive from the local host to the org admin -function transfer_chaincode_archive_for() { - local org=$1 - local cc_archive="build/chaincode/${CHAINCODE_NAME}.tgz" - push_fn "Transferring chaincode archive to ${org}" - - # Like kubectl cp, but targeted to a deployment rather than an individual pod. - tar cf - ${cc_archive} | kubectl -n $NS exec -i deploy/${org}-admin-cli -c main -- tar xvf - - - pop_fn -} - -function install_chaincode_for() { - local org=$1 - local peer=$2 - push_fn "Installing chaincode for org ${org} peer ${peer}" - - # Install the chaincode - echo 'set -x - export CORE_PEER_ADDRESS='${org}'-'${peer}':7051 - peer lifecycle chaincode install build/chaincode/'${CHAINCODE_NAME}'.tgz - ' | exec kubectl -n $NS exec deploy/${org}-admin-cli -c main -i -- /bin/bash - - pop_fn -} - function launch_chaincode_service() { local org=$1 local cc_id=$2 @@ -70,120 +127,177 @@ function launch_chaincode_service() { pop_fn } -function activate_chaincode_for() { +# Copy the chaincode archive from the local host to the org admin +function transfer_chaincode_archive_for() { local org=$1 - local cc_id=$2 - push_fn "Activating chaincode ${CHAINCODE_ID}" + local cc_archive=$2 + local dirname=$(dirname $cc_archive) + local filename=$(basename $cc_archive) - echo 'set -x - export CORE_PEER_ADDRESS='${org}'-peer1:7051 - - peer lifecycle \ - chaincode approveformyorg \ - --channelID '${CHANNEL_NAME}' \ - --name '${CHAINCODE_NAME}' \ - --version 1 \ - --package-id '${cc_id}' \ - --sequence 1 \ - -o org0-orderer1:6050 \ - --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem - - peer lifecycle \ - chaincode commit \ - --channelID '${CHANNEL_NAME}' \ - --name '${CHAINCODE_NAME}' \ - --version 1 \ - --sequence 1 \ - -o org0-orderer1:6050 \ - --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + push_fn "Transferring chaincode archive to ${org}" + + # Like kubectl cp, but targeted to a deployment rather than an individual pod. + tar cf - -C ${dirname} ${filename} | kubectl -n $NS exec -i deploy/${org}-admin-cli -c main -- tar xvf - + + pop_fn +} + +function install_chaincode_for() { + local org=$1 + local package_name=$2 + local peer=$3 + push_fn "Installing chaincode for ${org} ${peer}" + + # Install the chaincode + echo 'set -x + export CORE_PEER_ADDRESS='${org}'-'${peer}':7051 + peer lifecycle chaincode install '${package_name}' ' | exec kubectl -n $NS exec deploy/${org}-admin-cli -c main -i -- /bin/bash pop_fn } -function query_chaincode() { - set -x - # todo: mangle additional $@ parameters with bash escape quotations - echo ' - export CORE_PEER_ADDRESS=org1-peer1:7051 - peer chaincode query -n '${CHAINCODE_NAME}' -C '${CHANNEL_NAME}' -c '"'$@'"' - ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash -} - -function query_chaincode_metadata() { - set -x - local args='{"Args":["org.hyperledger.fabric:GetMetadata"]}' - # todo: mangle additional $@ parameters with bash escape quotations - log 'Org1-Peer1:' - echo ' - export CORE_PEER_ADDRESS=org1-peer1:7051 - peer chaincode query -n '${CHAINCODE_NAME}' -C '${CHANNEL_NAME}' -c '"'$args'"' - ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash - - log '' - log 'Org1-Peer2:' - echo ' - export CORE_PEER_ADDRESS=org1-peer2:7051 - peer chaincode query -n '${CHAINCODE_NAME}' -C '${CHANNEL_NAME}' -c '"'$args'"' - ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash - -} - -function invoke_chaincode() { - # set -x - # todo: mangle additional $@ parameters with bash escape quotations - echo ' - export CORE_PEER_ADDRESS=org1-peer1:7051 - peer chaincode \ - invoke \ - -o org0-orderer1:6050 \ - --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem \ - -n '${CHAINCODE_NAME}' \ - -C '${CHANNEL_NAME}' \ - -c '"'$@'"' - ' | exec kubectl -n $NS exec deploy/org1-admin-cli -c main -i -- /bin/bash - - sleep 2 -} - -# Normally the chaincode ID is emitted by the peer install command. In this case, we'll generate the -# package ID as the sha-256 checksum of the chaincode archive. -function set_chaincode_id() { - local cc_package=build/chaincode/${CHAINCODE_NAME}.tgz - cc_sha256=$(shasum -a 256 ${cc_package} | tr -s ' ' | cut -d ' ' -f 1) - - label=$( jq -r '.label' chaincode/${CHAINCODE_NAME}/metadata.json) - - CHAINCODE_ID=${label}:${cc_sha256} -} - -# Package and install the chaincode, but do not activate. +# Install the chaincode package to an org peer function install_chaincode() { local org=org1 + local cc_package=$1 + local package_name=$(basename $cc_package) - package_chaincode_for ${org} - transfer_chaincode_archive_for ${org} - install_chaincode_for ${org} peer1 - install_chaincode_for ${org} peer2 - - set_chaincode_id + transfer_chaincode_archive_for ${org} ${cc_package} + install_chaincode_for ${org} ${package_name} peer1 + install_chaincode_for ${org} ${package_name} peer2 } -# Activate the installed chaincode but do not package/install a new archive. -function activate_chaincode() { - set -x +# approve the chaincode package for an org and assign a name +function approve_chaincode() { + local org=org1 + local cc_name=$1 + local cc_id=$2 + push_fn "Approving chaincode ${cc_name} with ID ${cc_id}" - set_chaincode_id - activate_chaincode_for org1 $CHAINCODE_ID + echo 'set -x + export CORE_PEER_ADDRESS='${org}'-peer1:7051 + + peer lifecycle \ + chaincode approveformyorg \ + --channelID '${CHANNEL_NAME}' \ + --name '${cc_name}' \ + --version 1 \ + --package-id '${cc_id}' \ + --sequence 1 \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + + ' | exec kubectl -n $NS exec deploy/${org}-admin-cli -c main -i -- /bin/bash + + pop_fn } -# Install, launch, and activate the chaincode -function deploy_chaincode() { - set -x +# commit the named chaincode for an org +function commit_chaincode() { + local org=org1 + local cc_name=$1 + push_fn "Committing chaincode ${cc_name}" + + echo 'set -x + export CORE_PEER_ADDRESS='${org}'-peer1:7051 + + peer lifecycle \ + chaincode commit \ + --channelID '${CHANNEL_NAME}' \ + --name '${cc_name}' \ + --version 1 \ + --sequence 1 \ + -o org0-orderer1:6050 \ + --tls --cafile /var/hyperledger/fabric/organizations/ordererOrganizations/org0.example.com/msp/tlscacerts/org0-tls-ca.pem + ' | exec kubectl -n $NS exec deploy/${org}-admin-cli -c main -i -- /bin/bash + pop_fn +} + +# The chaincode docker image is stored in the code.tar.gz ccaas.json +function extract_chaincode_image() { + CHAINCODE_IMAGE=$(tar zxfO $1 code.tar.gz | tar zxfO - ccaas.json | jq -r .image) +} + +function extract_chaincode_name() { + CHAINCODE_NAME=$(tar zxfO $1 code.tar.gz | tar zxfO - ccaas.json | jq -r .name) +} + +function launch_chaincode() { + local cc_package=$1 + + id_chaincode ${cc_package} + extract_chaincode_image ${cc_package} + extract_chaincode_name ${cc_package} - install_chaincode launch_chaincode_service org1 $CHAINCODE_ID $CHAINCODE_IMAGE peer1 launch_chaincode_service org1 $CHAINCODE_ID $CHAINCODE_IMAGE peer2 - activate_chaincode } +function id_chaincode() { + local cc_package=$1 + + cc_sha256=$(shasum -a 256 ${cc_package} | tr -s ' ' | cut -d ' ' -f 1) + cc_label=$(tar zxfO ${cc_package} metadata.json | jq -r '.label') + + CHAINCODE_ID=${cc_label}:${cc_sha256} +} + +# chaincode "group" commands. Like "main" for chaincode sub-command group. +function cc_command_group() { + #set -x + + COMMAND=$1 + shift + + if [ "${COMMAND}" == "deploy" ]; then + log "Deploying chaincode" + deploy_chaincode $@ + log "🏁 - Chaincode is ready." + + elif [ "${COMMAND}" == "package" ]; then + log "Packaging chaincode" + package_chaincode $@ + log "🏁 - Chaincode package is ready." + + elif [ "${COMMAND}" == "id" ]; then + id_chaincode $@ + log $CHAINCODE_ID + + elif [ "${COMMAND}" == "launch" ]; then + log "Launching chaincode services" + launch_chaincode $@ + log "🏁 - Chaincode services are ready" + + elif [ "${COMMAND}" == "install" ]; then + log "Installing chaincode for org1" + install_chaincode $@ + log "🏁 - Chaincode is installed" + + elif [ "${COMMAND}" == "approve" ]; then + log "Approving chaincode for org1" + approve_chaincode $@ + log "🏁 - Chaincode is approved" + + elif [ "${COMMAND}" == "commit" ]; then + log "Committing chaincode for org1" + commit_chaincode $@ + log "🏁 - Chaincode is committed" + + elif [ "${COMMAND}" == "invoke" ]; then + invoke_chaincode $@ 2>> ${LOG_FILE} + + elif [ "${COMMAND}" == "query" ]; then + query_chaincode $@ >> ${LOG_FILE} + + elif [ "${COMMAND}" == "metadata" ]; then + query_chaincode_metadata $@ >> ${LOG_FILE} + +# todo: maybe... +# elif [ "${COMMAND}" == "activate" ]; then + + else + print_help + exit 1 + fi +} \ No newline at end of file diff --git a/test-network-k8s/scripts/kind.sh b/test-network-k8s/scripts/kind.sh index 02cb9165..2922c7d9 100755 --- a/test-network-k8s/scripts/kind.sh +++ b/test-network-k8s/scripts/kind.sh @@ -87,8 +87,8 @@ nodes: - containerPort: 443 hostPort: ${ingress_https_port} protocol: TCP -networking: - kubeProxyMode: "ipvs" +#networking: +# kubeProxyMode: "ipvs" # create a cluster with the local registry enabled in containerd containerdConfigPatches: @@ -98,6 +98,11 @@ containerdConfigPatches: EOF + for node in $(kind get nodes); + do + docker exec "$node" sysctl net.ipv4.conf.all.route_localnet=1; + done + pop_fn } @@ -162,4 +167,4 @@ function kind_init() { function kind_unkind() { kind_delete -} \ No newline at end of file +} diff --git a/test-network-k8s/scripts/utils.sh b/test-network-k8s/scripts/utils.sh index 14f4697b..a3c8bd8e 100644 --- a/test-network-k8s/scripts/utils.sh +++ b/test-network-k8s/scripts/utils.sh @@ -24,6 +24,7 @@ function logging_init() { function exit_fn() { rc=$? + set +x # Write an error icon to the current logging statement. if [ "0" -ne $rc ]; then From b616efeaaba1b9e9940d8c2ce2abd19fcfff4e51 Mon Sep 17 00:00:00 2001 From: Josh Kneubuhl Date: Sun, 3 Apr 2022 07:34:45 -0400 Subject: [PATCH 2/8] missed a couple of bash syntax errors Signed-off-by: Josh Kneubuhl --- test-network-k8s/scripts/chaincode.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test-network-k8s/scripts/chaincode.sh b/test-network-k8s/scripts/chaincode.sh index bb067cd0..7c6cf9ea 100755 --- a/test-network-k8s/scripts/chaincode.sh +++ b/test-network-k8s/scripts/chaincode.sh @@ -142,7 +142,7 @@ function install_chaincode() { local org=org1 local cc_package=$1 - install_chaincode_for ${org} peer1 ${cc_package + install_chaincode_for ${org} peer1 ${cc_package} install_chaincode_for ${org} peer2 ${cc_package} } @@ -186,6 +186,8 @@ function commit_chaincode() { --sequence 1 \ --orderer org0-orderer1.${DOMAIN}:443 \ --tls --cafile ${TEMP_DIR}/channel-msp/ordererOrganizations/org0/orderers/org0-orderer1/tls/signcerts/tls-cert.pem + + pop_fn } # The chaincode docker image is stored in the code.tar.gz ccaas.json @@ -218,7 +220,7 @@ function id_chaincode() { } # chaincode "group" commands. Like "main" for chaincode sub-command group. -function cc_command_group() { +function chaincode_command_group() { #set -x COMMAND=$1 @@ -274,4 +276,4 @@ function cc_command_group() { print_help exit 1 fi -} \ No newline at end of file +} From 2235c66c24ff3f31e09222c09bb08c9e81083c50 Mon Sep 17 00:00:00 2001 From: Josh Kneubuhl Date: Sun, 3 Apr 2022 10:13:44 -0400 Subject: [PATCH 3/8] Add metadata and activate examples to the CC README Signed-off-by: Josh Kneubuhl --- asset-transfer-basic/chaincode-java/README.md | 36 +++++++++---------- test-network-k8s/scripts/chaincode.sh | 33 ++++++++++++----- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/asset-transfer-basic/chaincode-java/README.md b/asset-transfer-basic/chaincode-java/README.md index ca41f306..96bef4c6 100644 --- a/asset-transfer-basic/chaincode-java/README.md +++ b/asset-transfer-basic/chaincode-java/README.md @@ -16,20 +16,22 @@ documentation. ## [Quickstart](../../test-network-k8s#quickstart) -```shell +``` export PATH=${PWD}/../../test-network-k8s:$PATH network kind - +``` +``` network up network channel create ``` - -```shell -network chaincode deploy ${PWD} - -network chaincode invoke asset-transfer-basic '{"Args":["InitLedger"]}' -network chaincode query asset-transfer-basic '{"Args":["ReadAsset","asset1"]}' | jq +``` +network chaincode deploy ${PWD} +``` +``` +network chaincode metadata asset-transfer-basic +network chaincode invoke asset-transfer-basic '{"Args":["InitLedger"]}' +network chaincode query asset-transfer-basic '{"Args":["ReadAsset","asset1"]}' | jq ``` ## Detailed Guide @@ -67,8 +69,9 @@ network chaincode commit asset-transfer-basic ```shell # execute the smart contract by name -network chaincode invoke asset-transfer-basic '{"Args":["InitLedger"]}' -network chaincode query asset-transfer-basic '{"Args":["ReadAsset","asset1"]}' +network chaincode metadata asset-transfer-basic +network chaincode invoke asset-transfer-basic '{"Args":["InitLedger"]}' +network chaincode query asset-transfer-basic '{"Args":["ReadAsset","asset1"]}' ``` ```shell @@ -101,7 +104,7 @@ Set the "address" attribute in the project's [ccpackage/connection.json](ccpacka ``` ```shell -network chaincode package $PWD/ccpackage/ $PWD/build/asset-transfer-debug.tgz +network cc package $PWD/ccpackage/ $PWD/build/asset-transfer-debug.tgz ``` ### Launch @@ -138,17 +141,14 @@ After the contract main has launched, install, approve, commit, and invoke the c ```shell # Complete the chaincode lifecycle -export CORE_CHAINCODE_ID_NAME=$(network chaincode id $PWD/build/asset-transfer-debug.tgz) - -network chaincode install $PWD/build/asset-transfer-debug.tgz -network chaincode approve asset-transfer-debug $CORE_CHAINCODE_ID_NAME -network chaincode commit asset-transfer-debug +network cc activate asset-transfer-debug $PWD/build/asset-transfer-debug.tgz ``` ```shell # execute the smart contract by name -network chaincode invoke asset-transfer-debug '{"Args":["InitLedger"]}' -network chaincode query asset-transfer-debug '{"Args":["ReadAsset","asset1"]}' +network cc metadata asset-transfer-debug +network cc invoke asset-transfer-debug '{"Args":["InitLedger"]}' +network cc query asset-transfer-debug '{"Args":["ReadAsset","asset1"]}' ``` ## Tear Down diff --git a/test-network-k8s/scripts/chaincode.sh b/test-network-k8s/scripts/chaincode.sh index 7c6cf9ea..49e927cd 100755 --- a/test-network-k8s/scripts/chaincode.sh +++ b/test-network-k8s/scripts/chaincode.sh @@ -20,9 +20,21 @@ function deploy_chaincode() { extract_chaincode_name ${cc_package} launch_chaincode ${cc_package} - install_chaincode ${cc_package} - approve_chaincode ${CHAINCODE_NAME} ${CHAINCODE_ID} - commit_chaincode ${CHAINCODE_NAME} + + activate_chaincode ${CHAINCODE_NAME} ${cc_package} +} + +# Convenience routine to "do everything other than package and launch" a sample CC. +# This is useful in local debugging scenarios, where +function activate_chaincode() { + local cc_name=$1 + local cc_package=$2 + + set_chaincode_id ${cc_package} + + install_chaincode ${cc_package} + approve_chaincode ${cc_name} ${CHAINCODE_ID} + commit_chaincode ${cc_name} } function query_chaincode() { @@ -202,7 +214,8 @@ function extract_chaincode_name() { function launch_chaincode() { local cc_package=$1 - id_chaincode ${cc_package} + set_chaincode_id ${cc_package} + extract_chaincode_image ${cc_package} extract_chaincode_name ${cc_package} @@ -210,7 +223,7 @@ function launch_chaincode() { launch_chaincode_service org1 $CHAINCODE_ID $CHAINCODE_IMAGE peer2 } -function id_chaincode() { +function set_chaincode_id() { local cc_package=$1 cc_sha256=$(shasum -a 256 ${cc_package} | tr -s ' ' | cut -d ' ' -f 1) @@ -231,13 +244,18 @@ function chaincode_command_group() { deploy_chaincode $@ log "🏁 - Chaincode is ready." + elif [ "${COMMAND}" == "activate" ]; then + log "Activating chaincode" + activate_chaincode $@ + log "🏁 - Chaincode is ready." + elif [ "${COMMAND}" == "package" ]; then log "Packaging chaincode" package_chaincode $@ log "🏁 - Chaincode package is ready." elif [ "${COMMAND}" == "id" ]; then - id_chaincode $@ + set_chaincode_id $@ log $CHAINCODE_ID elif [ "${COMMAND}" == "launch" ]; then @@ -269,9 +287,6 @@ function chaincode_command_group() { elif [ "${COMMAND}" == "metadata" ]; then query_chaincode_metadata $@ >> ${LOG_FILE} -# todo: maybe... -# elif [ "${COMMAND}" == "activate" ]; then - else print_help exit 1 From 827e30cf3018a13c9afc3642b1ff6aa71ea46108 Mon Sep 17 00:00:00 2001 From: Josh Kneubuhl Date: Mon, 4 Apr 2022 14:24:16 -0400 Subject: [PATCH 4/8] move ccpackage/ contents into network script Signed-off-by: Josh Kneubuhl --- asset-transfer-basic/chaincode-java/README.md | 28 ++-- .../chaincode-java/SCRATCH.md | 49 ------- .../chaincode-java/ccpackage/ccaas.json | 4 - .../chaincode-java/ccpackage/connection.json | 5 - .../chaincode-java/ccpackage/metadata.json | 4 - ci/scripts/run-k8s-test-network-basic.sh | 5 +- .../kube/org1/org1-cc-template.yaml | 10 +- test-network-k8s/scripts/chaincode.sh | 130 +++++++++++------- test-network-k8s/scripts/utils.sh | 8 ++ 9 files changed, 111 insertions(+), 132 deletions(-) delete mode 100644 asset-transfer-basic/chaincode-java/SCRATCH.md delete mode 100644 asset-transfer-basic/chaincode-java/ccpackage/ccaas.json delete mode 100644 asset-transfer-basic/chaincode-java/ccpackage/connection.json delete mode 100644 asset-transfer-basic/chaincode-java/ccpackage/metadata.json diff --git a/asset-transfer-basic/chaincode-java/README.md b/asset-transfer-basic/chaincode-java/README.md index 96bef4c6..860354a5 100644 --- a/asset-transfer-basic/chaincode-java/README.md +++ b/asset-transfer-basic/chaincode-java/README.md @@ -26,7 +26,7 @@ network up network channel create ``` ``` -network chaincode deploy ${PWD} +network chaincode deploy asset-transfer-basic basic_1.0 ${PWD} ``` ``` network chaincode metadata asset-transfer-basic @@ -44,22 +44,22 @@ network channel create ```shell # Build the chaincode docker image -docker build -t hyperledger/fabric-samples/asset-transfer-basic/chaincode-java . +docker build -t fabric-samples/asset-transfer-basic/chaincode-java . # Load the docker image directly to the KIND control plane. # (Alternately, build/tag/push the image to a remote container registry, e.g. localhost:5000) -kind load docker-image hyperledger/fabric-samples/asset-transfer-basic/chaincode-java +kind load docker-image fabric-samples/asset-transfer-basic/chaincode-java ``` ```shell # Assemble the chaincode package archive -network chaincode package $PWD/ccpackage/ $PWD/build/asset-transfer.tgz +network chaincode package basic_1.0 asset-transfer-basic $PWD/build/asset-transfer.tgz # Determine the ID for the chaincode package CORE_CHAINCODE_ID_NAME=$(network chaincode id $PWD/build/asset-transfer.tgz) # Launch the chaincode in k8s as Deployment + Service -network chaincode launch $PWD/build/asset-transfer.tgz +network chaincode launch asset-transfer-basic $CORE_CHAINCODE_ID_NAME fabric-samples/asset-transfer-basic/chaincode-java # Complete the chaincode lifecycle network chaincode install $PWD/build/asset-transfer.tgz @@ -75,7 +75,7 @@ network chaincode query asset-transfer-basic '{"Args":["ReadAsset","asset1"] ``` ```shell -kubectl -n test-network logs -f deployment/org1peer1-cc-asset-transfer-basic +kubectl -n test-network logs -f deployment/org1peer1-ccaas-asset-transfer-basic ``` ## Debugging @@ -96,20 +96,16 @@ docker build -t fabric-samples/asset-transfer-basic/chaincode-java . By instructing the peer to connect to chaincode at the Docker host alias `host.docker.internal`, pods running in Kubernetes will access the local process via a special loopback interface established by KIND. -Set the "address" attribute in the project's [ccpackage/connection.json](ccpackage/connection.json) descriptor and assemble the chaincode package: -```json -{ - "address": "host.docker.internal:9999", -} -``` - +Set the "address" attribute in the package connection.json descriptor and assemble the chaincode package: ```shell -network cc package $PWD/ccpackage/ $PWD/build/asset-transfer-debug.tgz +export TEST_NETWORK_CHAINCODE_ADDRESS=host.docker.internal:9999 + +network cc package basic_1.0 asset-transfer-debug $PWD/build/asset-transfer-debug.tgz ``` ### Launch -When chaincode is launched locally, it must declare the package ID in the enviroment as if the process had been managed +When chaincode is launched locally, it must declare the package ID in the environment as if the process had been managed by the peer's chaincode lifecycle manager. Calculate the package ID and start the chaincode, binding to port 9999 on the local system: @@ -117,7 +113,7 @@ on the local system: export CHAINCODE_SERVER_ADDRESS=0.0.0.0:9999 export CORE_CHAINCODE_ID_NAME=$(network chaincode id $PWD/build/asset-transfer-debug.tgz) -java -jar build/libs/chaincode.jar +java -jar build/libs/chaincode.jar ``` Or using the editor/debugger/IDE of your choice, create a launch target for `ContractMain.main()`, specifying the diff --git a/asset-transfer-basic/chaincode-java/SCRATCH.md b/asset-transfer-basic/chaincode-java/SCRATCH.md deleted file mode 100644 index 02ac7910..00000000 --- a/asset-transfer-basic/chaincode-java/SCRATCH.md +++ /dev/null @@ -1,49 +0,0 @@ - - - -# Scratch notes - Ignore - ... `fabric-cli` redux - -`fabric [options] peer [parameters]` - -``` -fabric => network -peer => implicit (from env/context) -channel => implicit (from env/context) -group => chaincode -[params] => --param=value or NETWORK_$GROUP_$COMMAND_$PARAM=value from env -``` - -```shell -network chaincode package -network chaincode id -network chaincode install -network chaincode approve -network chainocde commit -``` - -```shell -network chaincode list -network chaincode delete -network chaincode describe -network chaincode invoke -network chaincode query -``` - -meta / fictitious targets: -``` -network chaincode launch -network chaincode deploy # package, install, LAUNCH, approve, commit -``` - - -ordinal position args vs. named parameters vs. env overrides -```shell -network chaincode package asset-transfer my-chaincode.tar.gz - -network cc package --name=asset-transfer (or NETWORK_CHAINCODE_PACKAGE_NAME=asset-transfer) -network cc package --name= (or NETWORK_${GROUP}_${COMMAND}_${PARAM}=) - - -``` - - diff --git a/asset-transfer-basic/chaincode-java/ccpackage/ccaas.json b/asset-transfer-basic/chaincode-java/ccpackage/ccaas.json deleted file mode 100644 index 6f1a9468..00000000 --- a/asset-transfer-basic/chaincode-java/ccpackage/ccaas.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "asset-transfer-basic", - "image": "hyperledger/fabric-samples/asset-transfer-basic/chaincode-java:latest" -} \ No newline at end of file diff --git a/asset-transfer-basic/chaincode-java/ccpackage/connection.json b/asset-transfer-basic/chaincode-java/ccpackage/connection.json deleted file mode 100644 index 604b6c5e..00000000 --- a/asset-transfer-basic/chaincode-java/ccpackage/connection.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "address": "{{.peername}}-cc-asset-transfer-basic:9999", - "dial_timeout": "10s", - "tls_required": false -} diff --git a/asset-transfer-basic/chaincode-java/ccpackage/metadata.json b/asset-transfer-basic/chaincode-java/ccpackage/metadata.json deleted file mode 100644 index c52d2a5e..00000000 --- a/asset-transfer-basic/chaincode-java/ccpackage/metadata.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "ccaas", - "label": "basic_1.0" -} diff --git a/ci/scripts/run-k8s-test-network-basic.sh b/ci/scripts/run-k8s-test-network-basic.sh index f53b71b3..77be6403 100755 --- a/ci/scripts/run-k8s-test-network-basic.sh +++ b/ci/scripts/run-k8s-test-network-basic.sh @@ -67,7 +67,7 @@ function createNetwork() { ./network channel create print "Deploying chaincode" - ./network chaincode deploy $TEST_NETWORK_CHAINCODE_PATH + ./network chaincode deploy asset-transfer-basic basic_1.0 $TEST_NETWORK_CHAINCODE_PATH } function stopNetwork() { @@ -82,7 +82,8 @@ trap "quitterLaScene" EXIT createNetwork print "Inserting and querying assets" -( ./network chaincode invoke $CHAINCODE_NAME '{"Args":["InitLedger"]}' \ +( ./network chaincode metadata $CHAINCODE_NAME \ + && ./network chaincode invoke $CHAINCODE_NAME '{"Args":["InitLedger"]}' \ && sleep 5 \ && ./network chaincode query $CHAINCODE_NAME '{"Args":["ReadAsset","asset1"]}' ) print "OK" diff --git a/test-network-k8s/kube/org1/org1-cc-template.yaml b/test-network-k8s/kube/org1/org1-cc-template.yaml index 374289dc..4a4a701f 100644 --- a/test-network-k8s/kube/org1/org1-cc-template.yaml +++ b/test-network-k8s/kube/org1/org1-cc-template.yaml @@ -7,16 +7,16 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: org1{{PEER_NAME}}-cc-{{CHAINCODE_NAME}} + name: org1{{PEER_NAME}}-ccaas-{{CHAINCODE_NAME}} spec: replicas: 1 selector: matchLabels: - app: org1{{PEER_NAME}}-cc-{{CHAINCODE_NAME}} + app: org1{{PEER_NAME}}-ccaas-{{CHAINCODE_NAME}} template: metadata: labels: - app: org1{{PEER_NAME}}-cc-{{CHAINCODE_NAME}} + app: org1{{PEER_NAME}}-ccaas-{{CHAINCODE_NAME}} spec: containers: - name: main @@ -36,11 +36,11 @@ spec: apiVersion: v1 kind: Service metadata: - name: org1{{PEER_NAME}}-cc-{{CHAINCODE_NAME}} + name: org1{{PEER_NAME}}-ccaas-{{CHAINCODE_NAME}} spec: ports: - name: chaincode port: 9999 protocol: TCP selector: - app: org1{{PEER_NAME}}-cc-{{CHAINCODE_NAME}} \ No newline at end of file + app: org1{{PEER_NAME}}-ccaas-{{CHAINCODE_NAME}} \ No newline at end of file diff --git a/test-network-k8s/scripts/chaincode.sh b/test-network-k8s/scripts/chaincode.sh index 49e927cd..4084ecb7 100755 --- a/test-network-k8s/scripts/chaincode.sh +++ b/test-network-k8s/scripts/chaincode.sh @@ -7,21 +7,35 @@ # Convenience routine to "do everything" required to bring up a sample CC. function deploy_chaincode() { + local cc_name=$1 + local cc_label=$2 + local cc_folder=$(absolute_path $3) + + local temp_folder=$(mktemp -d) + local cc_package=${temp_folder}/${cc_name}.tgz + + package_chaincode ${cc_label} ${cc_name} ${cc_package} + + set_chaincode_id ${cc_package} + set_chaincode_image ${cc_folder} + + build_chaincode_image ${cc_folder} ${CHAINCODE_IMAGE} + kind_load_image ${CHAINCODE_IMAGE} + launch_chaincode ${cc_name} ${CHAINCODE_ID} ${CHAINCODE_IMAGE} + + activate_chaincode ${cc_name} ${cc_package} +} + +# Infer a reasonable name for the chaincode image based on the folder path conventions, or +# allow the user to override with TEST_NETWORK_CHAINCODE_IMAGE. +function set_chaincode_image() { local cc_folder=$1 - local build_folder=${cc_folder}/build - local cc_package=${build_folder}/chaincode.tgz - build_chaincode_image ${cc_folder} - - mkdir -p ${build_folder} - - package_chaincode ${cc_folder}/ccpackage ${cc_package} - extract_chaincode_image ${cc_package} - extract_chaincode_name ${cc_package} - - launch_chaincode ${cc_package} - - activate_chaincode ${CHAINCODE_NAME} ${cc_package} + if [ -z "$TEST_NETWORK_CHAINCODE_IMAGE" ]; then + CHAINCODE_IMAGE=${cc_folder/*fabric-samples/fabric-samples} + else + CHAINCODE_IMAGE=${TEST_NETWORK_CHAINCODE_IMAGE} + fi } # Convenience routine to "do everything other than package and launch" a sample CC. @@ -73,8 +87,6 @@ function invoke_chaincode() { local cc_name=$1 shift - # set -x - export_peer_context org1 peer1 peer chaincode invoke \ @@ -89,24 +101,58 @@ function invoke_chaincode() { function build_chaincode_image() { local cc_folder=$1 - local cc_image=$(jq -r .image ${cc_folder}/ccpackage/ccaas.json) + local cc_image=$2 push_fn "Building chaincode image ${cc_image}" docker build -t ${cc_image} ${cc_folder} + pop_fn +} + +function kind_load_image() { + local cc_image=$1 + + push_fn "Loading chaincode to kind image plane" + kind load docker-image ${cc_image} pop_fn } function package_chaincode() { - local cc_folder=$1 - local cc_archive=$2 - local archive_name=$(basename $cc_archive) - push_fn "Packaging chaincode ${archive_name}" + local cc_label=$1 + local cc_name=$2 + local cc_archive=$3 - tar -C ${cc_folder} -zcf ${cc_folder}/code.tar.gz connection.json ccaas.json + local cc_folder=$(dirname $cc_archive) + local archive_name=$(basename $cc_archive) + + push_fn "Packaging chaincode ${cc_label}" + + mkdir -p ${cc_folder} + + # Allow the user to override the service URL for the endpoint. This allows, for instance, + # local debugging at the 'host.docker.internal' DNS alias. + local cc_default_address="{{.peername}}-ccaas-${cc_name}:9999" + local cc_address=${TEST_NETWORK_CHAINCODE_ADDRESS:-$cc_default_address} + + cat << EOF > ${cc_folder}/connection.json +{ + "address": "${cc_address}", + "dial_timeout": "10s", + "tls_required": false +} +EOF + + cat << EOF > ${cc_folder}/metadata.json +{ + "type": "ccaas", + "label": "${cc_label}" +} +EOF + + tar -C ${cc_folder} -zcf ${cc_folder}/code.tar.gz connection.json tar -C ${cc_folder} -zcf ${cc_archive} code.tar.gz metadata.json rm ${cc_folder}/code.tar.gz @@ -116,26 +162,37 @@ function package_chaincode() { function launch_chaincode_service() { local org=$1 - local cc_id=$2 - local cc_image=$3 - local peer=$4 + local peer=$2 + local cc_name=$3 + local cc_id=$4 + local cc_image=$5 push_fn "Launching chaincode container \"${cc_image}\"" # The chaincode endpoint needs to have the generated chaincode ID available in the environment. # This could be from a config map, a secret, or by directly editing the deployment spec. Here we'll keep # things simple by using sed to substitute script variables into a yaml template. cat kube/${org}/${org}-cc-template.yaml \ - | sed 's,{{CHAINCODE_NAME}},'${CHAINCODE_NAME}',g' \ + | sed 's,{{CHAINCODE_NAME}},'${cc_name}',g' \ | sed 's,{{CHAINCODE_ID}},'${cc_id}',g' \ | sed 's,{{CHAINCODE_IMAGE}},'${cc_image}',g' \ | sed 's,{{PEER_NAME}},'${peer}',g' \ | exec kubectl -n $NS apply -f - - kubectl -n $NS rollout status deploy/${org}${peer}-cc-${CHAINCODE_NAME} + kubectl -n $NS rollout status deploy/${org}${peer}-ccaas-${cc_name} pop_fn } +function launch_chaincode() { + local org=org1 + local cc_name=$1 + local cc_id=$2 + local cc_image=$3 + + launch_chaincode_service ${org} peer1 ${cc_name} ${cc_id} ${cc_image} + launch_chaincode_service ${org} peer2 ${cc_name} ${cc_id} ${cc_image} +} + function install_chaincode_for() { local org=$1 local peer=$2 @@ -202,27 +259,6 @@ function commit_chaincode() { pop_fn } -# The chaincode docker image is stored in the code.tar.gz ccaas.json -function extract_chaincode_image() { - CHAINCODE_IMAGE=$(tar zxfO $1 code.tar.gz | tar zxfO - ccaas.json | jq -r .image) -} - -function extract_chaincode_name() { - CHAINCODE_NAME=$(tar zxfO $1 code.tar.gz | tar zxfO - ccaas.json | jq -r .name) -} - -function launch_chaincode() { - local cc_package=$1 - - set_chaincode_id ${cc_package} - - extract_chaincode_image ${cc_package} - extract_chaincode_name ${cc_package} - - launch_chaincode_service org1 $CHAINCODE_ID $CHAINCODE_IMAGE peer1 - launch_chaincode_service org1 $CHAINCODE_ID $CHAINCODE_IMAGE peer2 -} - function set_chaincode_id() { local cc_package=$1 diff --git a/test-network-k8s/scripts/utils.sh b/test-network-k8s/scripts/utils.sh index f5185961..bc47209c 100644 --- a/test-network-k8s/scripts/utils.sh +++ b/test-network-k8s/scripts/utils.sh @@ -102,3 +102,11 @@ function export_peer_context() { export CORE_PEER_MSPCONFIGPATH=${TEMP_DIR}/enrollments/${org}/users/${org}admin/msp export CORE_PEER_TLS_ROOTCERT_FILE=${TEMP_DIR}/channel-msp/peerOrganizations/${org}/msp/tlscacerts/tlsca-signcert.pem } + +function absolute_path() { + local relative_path=$1 + + local abspath="$( cd "${relative_path}" && pwd )" + + echo $abspath +} From 3ed7be81971632502175ec5c48d1789fbeb4d5ab Mon Sep 17 00:00:00 2001 From: Josh Kneubuhl Date: Tue, 5 Apr 2022 09:14:25 -0400 Subject: [PATCH 5/8] Fix CI test - Azure mounts git checkout at a different folder root path Signed-off-by: Josh Kneubuhl --- ci/scripts/run-k8s-test-network-basic.sh | 1 + test-network-k8s/scripts/chaincode.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/ci/scripts/run-k8s-test-network-basic.sh b/ci/scripts/run-k8s-test-network-basic.sh index 77be6403..afd9a16b 100755 --- a/ci/scripts/run-k8s-test-network-basic.sh +++ b/ci/scripts/run-k8s-test-network-basic.sh @@ -23,6 +23,7 @@ export TEST_NETWORK_KIND_CLUSTER_NAME=${TEST_NETWORK_KIND_CLUSTER_NAME:-kind} # asset-transfer-basic chaincode target export TEST_NETWORK_CHAINCODE_NAME=${TEST_NETWORK_CHAINCODE_NAME:-asset-transfer-basic} export TEST_NETWORK_CHAINCODE_PATH=${TEST_NETWORK_CHAINCODE_PATH:-$PWD/../asset-transfer-basic/chaincode-java} +export TEST_NETWORK_CHAINCODE_IMAGE=${TEST_NETWORK_CHAINCODE_IMAGE:-fabric-samples/asset-transfer-basic/chaincode-java} # gateway client application parameters export GATEWAY_CLIENT_APPLICATION_PATH=${GATEWAY_CLIENT_APPLICATION_PATH:-../asset-transfer-basic/application-gateway-${CLIENT_LANGUAGE}} diff --git a/test-network-k8s/scripts/chaincode.sh b/test-network-k8s/scripts/chaincode.sh index 4084ecb7..cdca638d 100755 --- a/test-network-k8s/scripts/chaincode.sh +++ b/test-network-k8s/scripts/chaincode.sh @@ -32,6 +32,7 @@ function set_chaincode_image() { local cc_folder=$1 if [ -z "$TEST_NETWORK_CHAINCODE_IMAGE" ]; then + # cc_folder path starting with first index of "fabric-samples" CHAINCODE_IMAGE=${cc_folder/*fabric-samples/fabric-samples} else CHAINCODE_IMAGE=${TEST_NETWORK_CHAINCODE_IMAGE} From a02b945faf1e973cce3b0e0463d2038491f64d56 Mon Sep 17 00:00:00 2001 From: Josh Kneubuhl Date: Tue, 5 Apr 2022 10:00:35 -0400 Subject: [PATCH 6/8] Update test-network-k8s README with updated cc deploy commands Signed-off-by: Josh Kneubuhl --- test-network-k8s/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-network-k8s/README.md b/test-network-k8s/README.md index 0bf16c49..ce3f9c43 100644 --- a/test-network-k8s/README.md +++ b/test-network-k8s/README.md @@ -35,7 +35,7 @@ Launch the network, create a channel, and deploy the [basic-asset-transfer](../a ./network up ./network channel create -./network chaincode deploy $PWD/../asset-transfer-basic/chaincode-java +./network chaincode deploy asset-transfer-basic basic_1.0 $PWD/../asset-transfer-basic/chaincode-java ``` Invoke and query chaincode: From bef68f326ac5cf7198d855b71cc59cf3f98dfaea Mon Sep 17 00:00:00 2001 From: Josh Kneubuhl Date: Tue, 5 Apr 2022 10:40:52 -0400 Subject: [PATCH 7/8] Run basic-asset transfer CI tests with Java + golang CC in Azure Signed-off-by: Josh Kneubuhl --- ci/azure-pipelines.yml | 6 +++++- ci/scripts/run-k8s-test-network-basic.sh | 5 +++-- test-network-k8s/kube/org1/org1-cc-template.yaml | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index b75aef48..3399833c 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -135,8 +135,12 @@ jobs: vmImage: ubuntu-20.04 strategy: matrix: - Typescript: + Typescript-Java: CLIENT_LANGUAGE: typescript + CHAINCODE_LANGUAGE: java + Typescript-Golang: + CLIENT_LANGUAGE: typescript + CHAINCODE_LANGUAGE: external steps: - template: templates/install-k8s-deps.yml - script: ../ci/scripts/run-k8s-test-network-basic.sh diff --git a/ci/scripts/run-k8s-test-network-basic.sh b/ci/scripts/run-k8s-test-network-basic.sh index afd9a16b..86093238 100755 --- a/ci/scripts/run-k8s-test-network-basic.sh +++ b/ci/scripts/run-k8s-test-network-basic.sh @@ -9,6 +9,7 @@ set -euo pipefail # Test matrix parameters export CONTAINER_CLI=${CONTAINER_CLI:-docker} export CLIENT_LANGUAGE=${CLIENT_LANGUAGE:-typescript} +export CHAINCODE_LANGUAGE=${CHAINCODE_LANGUAGE:-java} # Fabric version and Docker registry source: use the latest stable tag image from JFrog export FABRIC_VERSION=${FABRIC_VERSION:-2.4} @@ -22,8 +23,8 @@ export TEST_NETWORK_KIND_CLUSTER_NAME=${TEST_NETWORK_KIND_CLUSTER_NAME:-kind} # asset-transfer-basic chaincode target export TEST_NETWORK_CHAINCODE_NAME=${TEST_NETWORK_CHAINCODE_NAME:-asset-transfer-basic} -export TEST_NETWORK_CHAINCODE_PATH=${TEST_NETWORK_CHAINCODE_PATH:-$PWD/../asset-transfer-basic/chaincode-java} -export TEST_NETWORK_CHAINCODE_IMAGE=${TEST_NETWORK_CHAINCODE_IMAGE:-fabric-samples/asset-transfer-basic/chaincode-java} +export TEST_NETWORK_CHAINCODE_PATH=${TEST_NETWORK_CHAINCODE_PATH:-$PWD/../asset-transfer-basic/chaincode-${CHAINCODE_LANGUAGE}} +export TEST_NETWORK_CHAINCODE_IMAGE=${TEST_NETWORK_CHAINCODE_IMAGE:-fabric-samples/asset-transfer-basic/chaincode-${CHAINCODE_LANGUAGE}} # gateway client application parameters export GATEWAY_CLIENT_APPLICATION_PATH=${GATEWAY_CLIENT_APPLICATION_PATH:-../asset-transfer-basic/application-gateway-${CLIENT_LANGUAGE}} diff --git a/test-network-k8s/kube/org1/org1-cc-template.yaml b/test-network-k8s/kube/org1/org1-cc-template.yaml index 4a4a701f..05658f52 100644 --- a/test-network-k8s/kube/org1/org1-cc-template.yaml +++ b/test-network-k8s/kube/org1/org1-cc-template.yaml @@ -25,8 +25,8 @@ spec: env: - name: CHAINCODE_SERVER_ADDRESS value: 0.0.0.0:9999 - - # todo: load with an envFrom and a dynamic config map with the ID. + - name: CHAINCODE_ID + value: {{CHAINCODE_ID}} - name: CORE_CHAINCODE_ID_NAME value: {{CHAINCODE_ID}} ports: From 184f9a7317f7e6d48ef8544c690eb91269dd3a1a Mon Sep 17 00:00:00 2001 From: Josh Kneubuhl Date: Tue, 5 Apr 2022 11:55:37 -0400 Subject: [PATCH 8/8] remove (obsolete) test-net chaincode/ folder Signed-off-by: Josh Kneubuhl --- .../chaincode/asset-transfer-basic-debug/connection.json | 5 ----- .../chaincode/asset-transfer-basic-debug/metadata.json | 4 ---- .../chaincode/asset-transfer-basic/connection.json | 5 ----- .../chaincode/asset-transfer-basic/metadata.json | 4 ---- 4 files changed, 18 deletions(-) delete mode 100644 test-network-k8s/chaincode/asset-transfer-basic-debug/connection.json delete mode 100644 test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json delete mode 100644 test-network-k8s/chaincode/asset-transfer-basic/connection.json delete mode 100644 test-network-k8s/chaincode/asset-transfer-basic/metadata.json diff --git a/test-network-k8s/chaincode/asset-transfer-basic-debug/connection.json b/test-network-k8s/chaincode/asset-transfer-basic-debug/connection.json deleted file mode 100644 index 030c8ee1..00000000 --- a/test-network-k8s/chaincode/asset-transfer-basic-debug/connection.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "address": "host.docker.internal:9999", - "dial_timeout": "10s", - "tls_required": false -} diff --git a/test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json b/test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json deleted file mode 100644 index c52d2a5e..00000000 --- a/test-network-k8s/chaincode/asset-transfer-basic-debug/metadata.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "ccaas", - "label": "basic_1.0" -} diff --git a/test-network-k8s/chaincode/asset-transfer-basic/connection.json b/test-network-k8s/chaincode/asset-transfer-basic/connection.json deleted file mode 100644 index f3a8dd89..00000000 --- a/test-network-k8s/chaincode/asset-transfer-basic/connection.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "address": "{{.peername}}-cc-asset-transfer-basic:9999", - "dial_timeout": "10s", - "tls_required": false -} \ No newline at end of file diff --git a/test-network-k8s/chaincode/asset-transfer-basic/metadata.json b/test-network-k8s/chaincode/asset-transfer-basic/metadata.json deleted file mode 100644 index c52d2a5e..00000000 --- a/test-network-k8s/chaincode/asset-transfer-basic/metadata.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "ccaas", - "label": "basic_1.0" -}