From 0efa30176a863ddb6fe407fc58ce49b0c5ee325a Mon Sep 17 00:00:00 2001 From: Colm Bhandal Date: Thu, 14 Sep 2023 17:29:39 +0100 Subject: [PATCH] Kf 4175 docs site (#196) * Repo setup * Project name * Doc check refactor Using reusable workflow instead * Dispatch event Allows it to be manually called * typo * MFlow word Added exception for spellcheck * Custom conf * quotes Values seem to be in quotes * Starter pack update Pulled in latest starter pack changes * Latest workflow * Proj name Removed "Documentation" - the site alredy appends "documentation" * Skeletal landing page Content as yet unchecked Also updated some links * Charm specific Made the content charm specific * Remove readme Removed starter pack readme * Fixed links * Tentative content Unchecked * Removed V2 Added version tables instead of V2 in heading/text * MLflow syntax Little tweaks * Kf 1022 deploy mlflow kubeflow ck8s (#191) * First version * First version * Syntax fixes * small tweaks * Dropdown Details is not a component Dropdown comes from sphynix-design * Links and code Fixed links and inline code formatting * Integrate doc Fixed links and some wording * ML Kube tutorial Fixed some broken things from Sphinx migration Made some tweaks to wording * CK8s tweaks * Deploy CK8s Fixed links Made some tweaks * Deploy EKS * ML KF EKS * Migrate guide * Cos integration * Jira to wordlist * reorder wordlist * remove label * Project and Community Added some links * Alphabetical howtos * Duplicate constraint wording Made the bit about duplicate constraints clearer * Contributing link * How-to toc * Tutorial * Streamlined intro * Delete docs/.github Relic of starter pack * Newline * MicroK8s casing * remove tweaks note * Patience tweak * remove 5 mins * Some spelling fixes * Spelling * Final spell check * wordlist update * wordlist update * wordlist * Prereq sect * Fix links * Published versions MLflow and resource dispatcher Use published versions * Update docs/how-to/deploy-ck8s-aws.rst Co-authored-by: Phoevos Kalemkeris * Version tables rendering * TensorBoard * Wordlist spellcheck Alphbabetically ordered Removed mlflow lowercase Fixed spelling issue * stabe typo * Table spacing * reword * Correct apostrophe --------- Co-authored-by: Michal Hucko Co-authored-by: Phoevos Kalemkeris --- .github/workflows/automatic-doc-checks.yml | 16 ++ docs/.gitignore | 8 + docs/.readthedocs.yaml | 27 ++ docs/.sphinx/_static/custom.css | 189 ++++++++++++++ docs/.sphinx/_static/favicon.png | Bin 0 -> 57806 bytes docs/.sphinx/_static/github_issue_links.css | 24 ++ docs/.sphinx/_static/github_issue_links.js | 26 ++ docs/.sphinx/_static/header-nav.js | 10 + docs/.sphinx/_static/header.css | 167 +++++++++++++ docs/.sphinx/_static/tag.png | Bin 0 -> 6781 bytes docs/.sphinx/_templates/base.html | 7 + docs/.sphinx/_templates/footer.html | 90 +++++++ docs/.sphinx/_templates/header.html | 36 +++ docs/.sphinx/_templates/page.html | 49 ++++ docs/.sphinx/requirements.txt | 13 + docs/.sphinx/spellingcheck.yaml | 27 ++ docs/.wokeignore | 4 + docs/.wordlist.txt | 47 ++++ docs/Makefile | 65 +++++ docs/conf.py | 182 ++++++++++++++ docs/custom_conf.py | 126 ++++++++++ docs/doc-cheat-sheet-myst.md | 243 ++++++++++++++++++ docs/doc-cheat-sheet.rst | 258 ++++++++++++++++++++ docs/explanation/index.rst | 4 + docs/how-to/create-ck8s-aws.rst | 87 +++++++ docs/how-to/deploy-ck8s-aws.rst | 70 ++++++ docs/how-to/deploy-eks.rst | 68 ++++++ docs/how-to/deploy-mlflow-kubeflow-eks.rst | 130 ++++++++++ docs/how-to/index.rst | 39 +++ docs/how-to/integrate-cos.rst | 88 +++++++ docs/how-to/integrate-ml-ckf-ck8s.rst | 42 ++++ docs/how-to/migrate-v1-v2.rst | 131 ++++++++++ docs/index.rst | 62 +++++ docs/make.bat | 35 +++ docs/reference/index.rst | 4 + docs/reuse/links.txt | 10 + docs/tutorial/index.rst | 18 ++ docs/tutorial/mlflow-kubeflow.rst | 185 ++++++++++++++ docs/tutorial/mlflow.rst | 145 +++++++++++ 39 files changed, 2732 insertions(+) create mode 100644 .github/workflows/automatic-doc-checks.yml create mode 100644 docs/.gitignore create mode 100644 docs/.readthedocs.yaml create mode 100644 docs/.sphinx/_static/custom.css create mode 100644 docs/.sphinx/_static/favicon.png create mode 100644 docs/.sphinx/_static/github_issue_links.css create mode 100644 docs/.sphinx/_static/github_issue_links.js create mode 100644 docs/.sphinx/_static/header-nav.js create mode 100644 docs/.sphinx/_static/header.css create mode 100644 docs/.sphinx/_static/tag.png create mode 100644 docs/.sphinx/_templates/base.html create mode 100644 docs/.sphinx/_templates/footer.html create mode 100644 docs/.sphinx/_templates/header.html create mode 100644 docs/.sphinx/_templates/page.html create mode 100644 docs/.sphinx/requirements.txt create mode 100644 docs/.sphinx/spellingcheck.yaml create mode 100644 docs/.wokeignore create mode 100644 docs/.wordlist.txt create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/custom_conf.py create mode 100644 docs/doc-cheat-sheet-myst.md create mode 100644 docs/doc-cheat-sheet.rst create mode 100644 docs/explanation/index.rst create mode 100644 docs/how-to/create-ck8s-aws.rst create mode 100644 docs/how-to/deploy-ck8s-aws.rst create mode 100644 docs/how-to/deploy-eks.rst create mode 100644 docs/how-to/deploy-mlflow-kubeflow-eks.rst create mode 100644 docs/how-to/index.rst create mode 100644 docs/how-to/integrate-cos.rst create mode 100644 docs/how-to/integrate-ml-ckf-ck8s.rst create mode 100644 docs/how-to/migrate-v1-v2.rst create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/reference/index.rst create mode 100644 docs/reuse/links.txt create mode 100644 docs/tutorial/index.rst create mode 100644 docs/tutorial/mlflow-kubeflow.rst create mode 100644 docs/tutorial/mlflow.rst diff --git a/.github/workflows/automatic-doc-checks.yml b/.github/workflows/automatic-doc-checks.yml new file mode 100644 index 00000000..cba712e5 --- /dev/null +++ b/.github/workflows/automatic-doc-checks.yml @@ -0,0 +1,16 @@ +name: Main Documentation Checks + +on: + - push + - pull_request + - workflow_dispatch + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + documentation-checks: + uses: canonical/documentation-workflows/.github/workflows/documentation-checks.yaml@main + with: + working-directory: 'docs' diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..c8f8ee69 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,8 @@ +/*env*/ +.sphinx/venv +.sphinx/warnings.txt +.sphinx/.wordlist.dic +_build +.DS_Store +__pycache__ +.idea/ diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml new file mode 100644 index 00000000..45fe3a88 --- /dev/null +++ b/docs/.readthedocs.yaml @@ -0,0 +1,27 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + builder: dirhtml + configuration: docs/conf.py + fail_on_warning: true + +# If using Sphinx, optionally build your docs in additional formats such as PDF +formats: + - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/.sphinx/requirements.txt diff --git a/docs/.sphinx/_static/custom.css b/docs/.sphinx/_static/custom.css new file mode 100644 index 00000000..cad94b74 --- /dev/null +++ b/docs/.sphinx/_static/custom.css @@ -0,0 +1,189 @@ +/** Fix the font weight (300 for normal, 400 for slightly bold) **/ + +div.page, h1, h2, h3, h4, h5, h6, .sidebar-tree .current-page>.reference, button, input, optgroup, select, textarea, th.head { + font-weight: 300 +} + +.toc-tree li.scroll-current>.reference, dl.glossary dt, dl.simple dt, dl:not([class]) dt { + font-weight: 400; +} + +/** Table styling **/ + +th.head { + text-transform: uppercase; + font-size: var(--font-size--small); +} + +table.docutils { + border: 0; + box-shadow: none; + width:100%; +} + +table.docutils td, table.docutils th, table.docutils td:last-child, table.docutils th:last-child, table.docutils td:first-child, table.docutils th:first-child { + border-right: none; + border-left: none; +} + +/* Allow to centre text horizontally in table data cells */ +table.align-center { + text-align: center !important; +} + +/** No rounded corners **/ + +.admonition, code.literal, .sphinx-tabs-tab, .sphinx-tabs-panel, .highlight { + border-radius: 0; +} + +/** Admonition styling **/ + +.admonition { + border-top: 1px solid #d9d9d9; + border-right: 1px solid #d9d9d9; + border-bottom: 1px solid #d9d9d9; +} + +/** Color for the "copy link" symbol next to headings **/ + +a.headerlink { + color: var(--color-brand-primary); +} + +/** Line to the left of the current navigation entry **/ + +.sidebar-tree li.current-page { + border-left: 2px solid var(--color-brand-primary); +} + +/** Some tweaks for issue #16 **/ + +[role="tablist"] { + border-bottom: 1px solid var(--color-sidebar-item-background--hover); +} + +.sphinx-tabs-tab[aria-selected="true"] { + border: 0; + border-bottom: 2px solid var(--color-brand-primary); + background-color: var(--color-sidebar-item-background--current); + font-weight:300; +} + +.sphinx-tabs-tab{ + color: var(--color-brand-primary); + font-weight:300; +} + +.sphinx-tabs-panel { + border: 0; + border-bottom: 1px solid var(--color-sidebar-item-background--hover); + background: var(--color-background-primary); +} + +button.sphinx-tabs-tab:hover { + background-color: var(--color-sidebar-item-background--hover); +} + +/** Custom classes to fix scrolling in tables by decreasing the + font size or breaking certain columns. + Specify the classes in the Markdown file with, for example: + ```{rst-class} break-col-4 min-width-4-8 + ``` +**/ + +table.dec-font-size { + font-size: smaller; +} +table.break-col-1 td.text-left:first-child { + word-break: break-word; +} +table.break-col-4 td.text-left:nth-child(4) { + word-break: break-word; +} +table.min-width-1-15 td.text-left:first-child { + min-width: 15em; +} +table.min-width-4-8 td.text-left:nth-child(4) { + min-width: 8em; +} + +/** Underline for abbreviations **/ + +abbr[title] { + text-decoration: underline solid #cdcdcd; +} + +/** Use the same style for right-details as for left-details **/ +.bottom-of-page .right-details { + font-size: var(--font-size--small); + display: block; +} + +/** Version switcher */ +button.version_select { + color: var(--color-foreground-primary); + background-color: var(--color-toc-background); + padding: 5px 10px; + border: none; +} + +.version_select:hover, .version_select:focus { + background-color: var(--color-sidebar-item-background--hover); +} + +.version_dropdown { + position: relative; + display: inline-block; + text-align: right; + font-size: var(--sidebar-item-font-size); +} + +.available_versions { + display: none; + position: absolute; + right: 0px; + background-color: var(--color-toc-background); + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 11; +} + +.available_versions a { + color: var(--color-foreground-primary); + padding: 12px 16px; + text-decoration: none; + display: block; +} + +.available_versions a:hover {background-color: var(--color-sidebar-item-background--current)} + +.show {display:block;} + +/** Fix for nested numbered list - the nested list is lettered **/ +ol.arabic ol.arabic { + list-style: lower-alpha; +} + +/** Make expandable sections look like links **/ +details summary { + color: var(--color-link); +} + +/** Fix the styling of the version box for readthedocs **/ + +#furo-readthedocs-versions .rst-versions, #furo-readthedocs-versions .rst-current-version, #furo-readthedocs-versions:focus-within .rst-current-version, #furo-readthedocs-versions:hover .rst-current-version { + background: var(--color-sidebar-item-background--hover); +} + +.rst-versions .rst-other-versions dd a { + color: var(--color-link); +} + +#furo-readthedocs-versions:focus-within .rst-current-version .fa-book, #furo-readthedocs-versions:hover .rst-current-version .fa-book, .rst-versions .rst-other-versions { + color: var(--color-sidebar-link-text); +} + +.rst-versions .rst-current-version { + color: var(--color-version-popup); + font-weight: bolder; +} diff --git a/docs/.sphinx/_static/favicon.png b/docs/.sphinx/_static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7109908f2af5c9bb0ad130c13ac143929643aa2 GIT binary patch literal 57806 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

&o&1XroP74a!84^ zr&3$0Q_CADVfP0>78w^ms7XSVnJ;@3M?v{6%hENcib09e;c}^}ye=3|mE!1nf!lOP*`5N625wWBINvvRcmM7Q~GTJp5`yH*Rdg z(m3roocC$8e>xR+7z}%Xv;D;TAaiz%k#`cg7`)CX0@xkl#gupbIO=ggur*?!V(KQ! zmy^HV;jv+=L+iM|z?p7`pJqfc;T;MV*`)3^A(-AGcB?T==Cc|bWK9Gr?#^d*t;nnI z!x@U=Xi2(?S=G+9Vx)`dt6z?#NMUmu&bsmlRTZ;M8p;$?v-si24d8cP6glXI`sAO_ z*!Rrd%?J84WHLJjk6(D<+4o?3WJ1gCzlHiJd#8D>Mf47oa1Eq6%nUsxe&ING?~ZlvP`OXbw}{f=l1OKh zSi<}5&;TZJRlxO~NTubfP0ff;&ajCr!Tlx~Vj4KWV&hpl?{IT&GIy?8X*E7VdAy6s zfq3%gS7ULUoGCXx{-3q|a`El09w%zkaxH>%xJ2%>wf0huy&D1Wbj81qPObG!{i`*_#*B8aB+1B~HtAArbHgOqM%=u#aW3OnoSidweMZZ&@ z1c;js(4N+T;W@IPt9F2&0^O3$_?@7JFObBq!3sc4-}8H}1>pN53r(9zr=i2!2Q^|? z!Lph%SR5p7B*hl{JdjX>@1df+TBC2$@Us=K%`Y- zwthdji-4EYt+P_x1iX6CDR$wfH;JCn?oH34{B7y_t(CX({?56p+&9D2fT?#(+}i(x zJO@=x}oZjA_kPea}9 zWAc}j{b!#__*?kR58wyb`yp|!r`a>mDV+)LdN57(Hyg7z!2&uJ$1Ch88nxwU6g6cp zKlz`a;fGv+H%odBv$g~v(00o z%xw=o=aYolF$XlJqt?Plex7GW6!@vbdz4NY?=7KzjCCCS{c)NK!OJ6<9E-knC+%(h1Ns^}dSiyV!D zpJRaAH&hp)^vvSVpA-p8;LDJ~=!9(ka;$2JUC6GcG*g!1-gj>@5_pMtb*l`eqR406 zMV-jrR&wC^!zA2$LCJK2rLMzw&#CN<@sP=Uzpl zEkzA8mFeH|Dt=@^&5U%fDHCH1x~$3)^=^r*!8m*6;t5rbgdFJoIwY;bTZa@+If&Sb zw|!r1wti1C^HwUlx^_)P#&olKO=C9Um$<6$FuG33unQaxA|NcL*oE3SwDd>FbBMOm zj%tz>v0`bGTNAyIHOGv~4EcaOK(Xprd)e^;a^u9>n5KWlca=Y~(45G(YBQRxyf(^e zBOf45{xdm@T+Q&%77dc5eX7DyxDh?_8IAR@8NL<(tJSS(`jMt(@?qTA+T>#a+w!k) zeWTspFoj)_Ub!}(WIBh|BqkRx;fv62PdS^}1uX0RZ3*>&%Fr6>7^51#LS#Nl6sz42 z4!Ha3;nK$f<_~0+Jpwx)IPB~t$TT#cECURa{P|(@>7z}kIw)hHbeYR0bYx`aES?y< zJe+}+C<$U4thhAHRc1>6YcLjTzjh^7eSO#IzJ>L5T7#EWJ-cC;Ho(*VKtM06)Q`+D z!8eL;SZsQZv@o zehl`I5h#OYI<2yzJBRO^!$Hi#8R_eNGN*hq=AtFntR34uVnk)Uu-)07(=+5M;!Qp% z%`U`+4CYafK#)VlRxHLxz$Oht@q~BmoJti+(ydxgIe1*O7X$Z{xfbt~c;!0WG{Q;=5Q^r| z2uurZ%AmvrE`GI*0iu5d80;U6nTJ?23e)5?N@&8^r^SK#KXkgD3~%&!(L0|;?yFC! zCgYDy!L$Y!5DsJ2{Z7rJl?QA+{hwg`1cqsf(a}}XlI|Xpk&nDxm1oD^fCWN|X8L`7 z(-g#01IN%qvqswqi=q^Uk*hVd&y~u}46>|PH=Vs#)HV}_38jj$kkQzY_1)N#l8f@A zfvebuJe-bR$z!^zVV$6-I2TXkGflj+&lrc#-zN9t79oo+pJ7EPxZ z47_)wG%t8pPSXzpzmD7b4TD^-SzDDwksB;FY9E}c*<=po8NNnEJvLh>%ta!3@=Y6w zm(>pH*5=C*PSJyV*EKqKucfu^TB6u=bxkTVksR!<;qpia&P68sVQ(3|r`WFDZ#JPDfj%)^*flsJ46Kf8Q z)n>`H+T3*CkV_?OyBj#b_{gqv;V(6Cct(n|?Wg>lqXtdom|XoZHc)2ipk(YDc1oP!R+A2 z8^j~eva_9;C$15Gl{&vHNh0~wG`n3E#wWXNMZ*lYX0XRx|dgo$qt~<}~ z1qtr=m>(Qh@WP?q;Vd>bc@zr)dx_a$_1Pwu*v`{KEcVHYZzy5@1Q=7J2Qn1h;>j#l zoBvBjKP&d;C2sTX!zqvuRwb41)tcRj6JucMj(u#k#Nk;qM97>J6hp*hAkkqBr1$O4ji5nIjJiiPPD= z)vNpVn-Z4Jjfl)h08AjSBOc%~C47LZ;K`0q}v+`i#>k(Ahuu?m7r zUjrzpQAe13ypOwJX97682P3Hp+Kx{*yPdH1z(!tffX(TV>~Rf)s6ctxNDOMVm^#Q| z)Q8$bbtFG-r(bS#5(b4+KS9U+=MnHYR<8`4AgXg(e1c5xCGS^NLiI0)fC~u6NuqS`nDr zGbaGF#qbDo*=r`)ee#GQS}I+4&j0Xk;p3OHKuGW$g@6Am8TTEh)Vg9~+uqHne#_2o zMX&{nS*&(xXy%IHe;#s=M(u1tR0XsA?4oT{h@>M5MSkL@KlIZH5!#>X`|Dv#UUyb3 zXz^~xO=E7^=s^3#!?FmSN3Z`Z54saLdxu_dpA!gSUUdcFW^zfknjnJqa!Z5E)Anu#4w?8aU+aUo1pEjD$98=8rqfRZ$_Z>UV5f{x=&c6Z|1~xFiC?3M+pYF5lA+(X)08p1iIDHd7)@B|x8m zTj2_e7e0R1{|)x0b`p%=9E4Cro`hASCi(=8IFrOsDL^JHhRX$_Td|azk=xCnZYP4h z-)_oQ1RIW!39dKwrCFFzM5(|eh+e6|gQyY1Uy0EDN6x(^%nxf_>=ZX?go$Ff2?V$L z`xBb&$ez~@`P^u9svnt(Z~y-&WT8+7r(ls7@Z3;_^$Q$HB4Gc(>6bz{U_)3qVh79; zhBH^G5~!rXIuO6n91~_8l9-+xh(fL(_a={nnKsdReJ62K0ZbK=mxC+12EMLF$JRdOtZ-=AvmfDD&(vZG? zA|++cz~n(r%DP009vmeo?N$=O6b4-eC`ovZE(Io!!ZE@QN)oY+9tY)SCfG23PG#b_*E zGb6(lVZOBFVjE*@F?^jKgou*{(#121a0fN>MRN;S)LEAlIcw0l?Y+mV!M0M0TwAm! zX}RGFOpnC#u}Tm$)9H|JWGZjn#vpmKnF z0Z%7mSPl3NbtRyM8n^IBIH3(|Isa}yn^f*K%@${8{f#i2PHt$NO_ zMtz)xhTF_iQh|zE0T{_Z zPNt2?u$Kr^bUP-a;v!6BG*bOyXZt+S{$3|dM%Yf;Bu?q3$`y^T9*NZih6q(yN)D%S z%25cXp>HDkJ0UWpon}yupe#p1e7v*{Ju-!Mrb}Nhvo}HLxnRDBuUo&oIIM!E+1wt) zl|_sZJHrCIMOdf=9-aF@-5g~HZaEAC15~=X^vXYLil5uE#3K^o!9#|UDMLMECmbt? zkWqpy>mcK>)kBm|3r8u$@IYqLl^$HhB*%>#Q#ZW^3mLx!mRBaP4NWzD$6Q&DFCFBJ z>AQSd+8Jkjjyv9VRD1pb#RwlXK@sSvgtO~7x5FJv=|{Aoxy?bYbnQUghV-%iXDLEG zbo87*9v}}#HbC%gQle>jl%hD0c0gF@o|5Rg7@HeNI&FO~IBKR+%L2Ux&M}s`t;*0I zM(2d#1`%ZSAY0ril*#bc{j`wQKpBU@OekOq48~zsgG`1=QN+XDlVR^PY%;YoSy%R4 zAaHUH89BPjqTRq5yz=T3(e){$5@UvQ;E}@cR5?>IiY#yNf)-sdaiEAMJs3zuf6Ah{-)`=IeP*bnnoAOsnvq=liGL!o%HTr(-+xSfnY ztg}J2VcX-^wCtAUj;uQgzX%LkrARk0PN?94=#Y2xrbeI+J?Z}V4DuHer?K>9W&V8{ z?^Pe7dP;pbrl^$IN(53I+R$bNB2!iT9YFLjwx^@OxiVxT3E{zIi17${z&D(HPbE?# zj)a#XjU1F_qy&d61-F5M#2(;A$!dPQV1r&xKGR30e<}lC%UV;=+1%S!&1>r0H!BtA z;WCKiq)9KU#M@SKyU2EHg;QRu!D~kFO1)mdg+#k6){S)!+gN=;RbRa`PSi64t=+BP z6U%x`9BdYYx!fOY28)EB4t>Nu)5W~$bmt|f?|%Et54PP@fi^M;)6$M!<|eZFiCSCcgu8;kd}&oZ z5*iWmZ#?^j%y$^kNOaw^XYs@B#PWa78vTde4oJxH^!Aeoz+=n?xsRYBv8<>`M64=f zzAuv$m-|$xXKir)hpg`#Pn@QN;T>QNYeYL|8%=O^0{Hse83WAXtt< z>T$to4f>16WR0cHOP_Z6A|M>+bJz|!4gP&|XB)WPbNzxm)*vh_+S+BdfmvGTLYiqrpK4krD1D?p3mB9+ zrcp=^FkQcsWMCHZ+)zhhcH`f-EWu(*&%RQ={KRAZ=Sy0TLF3zh^ojpZn6c?nU!k)F zQ-WrI;0=fXSFP|l>u!SYZ&|@Sb?h!<#Vo86Awx!V3Zqp=xa`Y%koI|PMl{W%VTRhy zx!Be94`2cUZLt5C;m(Q3zB|R^Ow}?y-SS{oH^`e#Kflw>nJGF0@;OO2a@!wa$LN$} z?q^nDM8e5T4Bc~dgN)~0_p;FGoLv_48x^E2W%4d!_c1)rW+SB#c@f3XD6VAAqBOnV z)auO`mf*dYK_4jOP(3}NJUm{Ow?xY>S@qRvK(f?=^%&bAd@Qcf1CR}wSeDw;XoL>* zb`!!0p^GH1mKa3KURkxBX$!Q}Y~>iMAi}sgttW$7oCo!slK+*& zT(;7gw8j|>Buzx$(9I?!D2mnpw3xA^W^KpN1kdHu7v|BM&U*osVzBP!)d!&9+v&K9 zvc)!)NNP1o(uCX-qnhC!0c`rQH3d9~24t5ZUn>6&94OlTX>6emv7m5-$vv7eTI>)Y zyx948F_mYVl`T0YZ#>4py3wJ!XWD!Z!TcXWsZ8Bq_HCLT4??Ui7~J6Px%k*y|NY3G z)Pe$vwZpmWy(~lR|4&}Hd&PHr8mcS4o1FU0mIuN)kmFieskEvIU2FnZOiUlmIZ(mQ z*%WC8YgYlM2yVu=iyjbsAFLAa#bUt#Vz0un!Z^kNSd~;z(J>&0_pH_jo)Zvr!Y*$+ zGw#`z_o(A(tsl%z4S$TYY53s=U!O$CXmKLPWl&DEh2h|{sr0)%an>cBpj?j$a^$y3 zzdGMhL%h9^j6}G)pjGLq`m|EYudB%U9HcRo4a0s;0RSiDl~dl8=Km%a7BSpW&G;wH zl6d;`Gg~&6_6LKqoD&*YSVlC$vEn{6wAN#1ai{i4galUhNhqN=wY}ml2K8vdTP=)O z)8UkYC^L8450WOXJF!R1)Av(7*67z&#|X^7B2y8R2!;g+UzoAF{sM$SQm}u}4(4-A z;k=G|&!M#q{@p_H%)yXejhmXeKR0K>6{#B@(KeADN=4{uzDBgpUX{49F=9nshl0K?>Pe zuusHBc#(w>r!yyJ3slt*@;5OmYQ4wk$%_+uH3&VkJ|}!T)CKNa)1(E05HY zwaNgmVrTQ59|LOMk>gVt2jTG4kF1VOu(ou1NXAm*KX$q@sdf3_7xKQ2w2$CgCVIKH zI^Fr7R*1fJ{Pt~u<6YCuq3*cPmpZQU3>_S6q+0jao?X|bIO&!ayKf#U9-~EJ)ib|F z`Ek7`QC&7dJDI%xC;qLwd%74%boCYfav5u%Xa*YI5+&SM-cWF7;w>&!U+3z@&D!CD zY^lTCx|ep-70=At?swS)7*e^Otg&Ue;AZKQO}R!O26w1e#8x4Y!MA%6roZ|a}F<34t7 zjqq(Q!u@8sAH!ZP`7Z;QepM@f9?mWQ zo@>`TOiuj(;EDL*tRsG}-Tnr#>a<+k|DYdB1DJ^SFxD#Z5<(W2c=YhQaA22R-Qjs! zz1rl8+ds2g%J|(^7G>xBpJo}kf2u0~y*Wc_E_m9znDM3=8;=bz&r+%j8_(C@6|T7{ zvE`TX@s4tAH(ac?Z{Yi`Y>FN9iNbPa9<>P(RrN1caka@r?2V6n^WPub|1#h=H8L|r zF;Lu986R8e9drMSX}L!K>4M9hhqL{=tACcrtoo1~|G%mrvdI6SyrAjpVmSQH{yo{< zl83rR>F3JAlzkTBrTXN)#TU(qvz&1am-f*5@Y{;L;W>&(@pFLvI+6gp^idiYos>g? z5K_%zp1Uvq#Sh2Aic4@763bsNyN;E;V%|x`7S3kBK`lhTA{sBK=9`{d`nB@6m_7Yd zfU!9~Z~qKI6f>bWH9wkJ6ESMR^ZhEpGQeInu5B~fw-8-2CdTUSHHoIkrXvy>W)sk!Rn)j8= zu@QEBMDBNVWQJP-NxbA~yu=to5*__I6~PcTL>m%8UxJShB0=4iIhZ{NG5#((vPS?LT{JwxS^XdTQ-SyMQmBTNBH%x z@g^Uc4J$t*$5HWuugI?M$(%dhe>;zO_3ZGyjO*&gTYj+4zl~XVIwpCX)n6t2eSL4g zu8h_dSb06duJkSkyILQsYpqAxo<{q7RSFcC$HR|GLhR5i0nIx1w8JOPL}C!wl`W^u zLoi$tG_x1I3 z88keV@GX<5n?a8wJ%SEVN4@oI&_y`Q*jgTuN|o50=ZN1=P_@bBM; z6|yH|*5`D7?r$$fZU)cXq6|^xuT% zXJ=Wewt<%4sSs5|_eI8!H^3{9m(=6wlbD=;5wEcJP&x0M`bfd{fFJL9LkZ4q@n2P( zZhJR<{jVx+(tfMr^HWj7Rr+&TISn!qrDxwpm(sbP2Uj4Qgyg>laPB6o?5BFByB|;KsfxCuku-t84FK#HK`&I{ni<#X*zL z>QKI!;pdtl4R8!&OhHu|buV^;9cm9+S2mPso0l-d*qVi{;Y-4$Y)1^C@OqZ7*gw|J z&cN9;F^d7j^I9RpwE0({ptvbgBG~G~XKN+UjBAFufS`z=RK>4?>&h~Q|SmIEkOZ6dhZYvA@tBg z4?Xli2m}(6JkB}y-22`eZ`^ms`2T;8HL^3t+I#I)_MB^e`&)A^6mHz*Pryz4cJh~u zb_A;J)`dHMi=_T)s=Ah0qgMZ?P(kp{O}!Yv{waX}K*O^o#v^Yo8W1a!<#i2j~!FB=^l2%vT1| zaTk;DW7F{2yDP}`m3Yn9Ms2$Ot-J3mW~M1#(ma;9yfP74V#iv&9~%2;=)Ny=$SY~b z0QyE)<{x15c;=VC(nd%ZYls?QJ2Gg@#Ny#b+&8Q3xU03GL+fJ|Dccv}a@?HFo#WKL zijspeiPzRK_Ok-sgYZ79=D(eOdR{-Tt+jqQb5qKlx2-9tYST6J5hUyHK|IgxQXv02T(=HNfsJpj9Ir1KdUS(6I+3KO|{UtCUp?RQ~Cea_gux5jeAUC!Ufc-FOR&xp3%`Z+C4 zJ_=!f^6+?0YKJPa`6qz8mGXC53*`}d3$1&XwObGPQt^u24*mQE8;JQFN4?~{R42b) zsTa?(w3kaun2$I^*UJS7vfB?pskR($y7dx3Tqu{XCf5Be!)lj<^l!JVPsRl^&2Cab zU7h%6ld=uVYb*ApZe-(s@oZlH_~an(f6;8kV>Vgpae0Ssk233DpFH~XB7-~H^|kVq z|1X{`mZ49!TmF*i4VEF}#wR}`Js>#|XK0vkMNG@;jg;k47dw?1D9_W1Eqe?Y-T|~!VS35dTi2E0 z_^Vqqfc2&W3X1$Rd8G)w3WTTF0VNJ}uAC97>*=@IBgN{qfUQzBVDaaPZYIFA741;* z9>s}ONcKL<1xYSO{TOAne!!S=iy+$p#+8okAlR(hbKnm|hp@d&Te40O_u_Fo9UpDLLYz$g4N%BBm5(f9d&y<( z@bER9>B@8{WT+sLE^Z!;T_`7uU%S-UpTav8$#H4%$L1;KcX^sLvsN@kIk%H$5V+0v zWWSmY(U7N{1Jv+rgk{T~n-LKFQDD&cq=0RPIH+*d#uCq$@))5IHhM|%(xWweEghVA ziK2KTyInp0+1Q=kCBbu_kM4c_wxj}IU##T1XHFevm(UoDSo+bNz=gF*WWIke%5eaG zMzJ2e;UgMq`L@QO@k5G=JhU)D2Js z6+XNrdd4} zw)K#m-bVAr^P9rAq$?YC#wM^ice&Mr10O1pNb~{s`4#Y+Oo*AZL4tx5ChfQI z<)wHkiA0jn@+%1j=2G90kPFmw0001^@XD|LEDDWo%p(6xR%@k}IS;{)n zIE9+>^~trQCD{@{fG_VxpiNa`7~bP`3hdn)bz2Dp)j5lVvu(a}7xALkfFMnNF)vh( zM;G!rQOowU1Oz5dTrUaoJFbxLVO2g)90MZn+uUJW>)WMCZdMbx%)-XAVs^JLG08xOQ)a#HVN}~T#R_pcnV`*xSIS1Iem6(+la_%_z zMFC#5Uf&jp>#Z0Da1g42KG{JDy`g1wdl0mSFwEemvV`P>Bv|U}FccL9Gm`kjEq9zi z!!z2(?&C&)2>5s>;J#5$Ah!okQFhnx;&R&V`^kVvX7++_Y+dXAujtj-R`;dLZ`Z^xl*tL@ouoygfK`0J=pX* zVc1-KR;L_;B2STiXL$bkx|#ZFEW9y~B!|Hb%W)$Km}fzU52qiaV{~{pfCtdjMnbLj z+jp&xRod|I{}9N3N~Mji(&hi{N0QQtKy+HXv)oUcyEHVk#{Z>+Og{Z@QbH4p@4{bR zjWT5Py3FXs&gI4ak3W?yVa)YEZ&E)qVZxqd>XWu~T?dVmZ$JNkC4pftUQ8QSAEVG$ z8$f_J=@UF`P(Hi}pNyXfrh*)2f_G8anL0ADpMZgr z;pIer6rq^#xNJOtl%+ln<3qXpXVr*=e?+_cpB=}Kffr9o8dP5VItzuzCh=e0`>E=G zR=|IM)tQdZW;TB@e(k-CZfT7#yRMF8jLXSj{c(Sa;bqmUob>07A3f+ROaGPXAyR5J zH)8d5+^jy}-E^N$xp#>9ygT+ql!DV>%VqYcMTPe5qiH7teoVh7!%A$!5rGVoPw;s& zF&(l~J7F85&#a~yb9dp*%-2pI-GDgs?c0|W1@l<6F|UJDGQP&qk2I#MT)NQnME3f% zBbbX0E7l&JHG37t)EMYnSr!+=nalv}M9c?fB{0c&8s5|ctsIJz@#5~}v6nw!(~XLI z-?V|HF~-2w;-t1Cd9Kj|1%S9~-78f%`<6ZP7zOarV_G3$C-a_YlN%6NoEDxr9A^N! z(klD$$7O2vjTctfE9b{Uku?jq#EVEyH3DLJQa!Y38TkQcfnspzf0BB-d z@5_Y~*PEa(4x``wzAN5@u0r_84*i3y9Whq^C1%`Xgp`MK{aKEpMsvOU|s zTO1J)R!aByQkqb1Y>Y-$p8ve`eCA_pGFkBW9@upK{PM`KbKkP}(&Z%h@JL!xfEe3 zkdbeuPVjB9q5!gATvOECG4)&*`;i$avu;TwU(Vp&l6zl!Hu)`o?Q$gtk!0pcKV{eN@4%h=_Kh5}Z?=`MV^AVa zTlUK6;jp-ImLjub_+yN(@$`9Tw9xgRHJ_epa<{Mf%uR|s@9MsGn{eo6duRu>zaz@v z2!4+udC2v{P27TPwOC~46-G*(_Ca)h`e-)$^aN%h3#E~d2a_6q9Tz{&m{@AS+|90g zSp*sFo$%C@*W5ZP6+SB|W1pkvyg)n3DGxacmb&Ki^l<57FH*D|#onxsa9B&x(>aO{ z54i)9_Zax5m@$16pQnrxCuLOYm+k?msCBvw<4_%>Y$qI?TNg@~{piV(V8R^X33j1= z^dzg4G~8${SH>!=Lv7%{Q9iYxQb4K|{Mqbrk2A<=OBpL)5j|g!twy@q(v>K0;C8|w zwX>7SmA(2>3<0au_k&4(-gKQZH|ip*x<1kaO4ra=lT+2{wZ@Q^N4qjBSF`JGUg`!p z3T<)3hnKT&ZE~*(trz$jtp_;s;w`gxX3`49FppPnc6-5Zo!3;GUvEnPsK{GApP&Br`~sxz~#Ryx)-0`>yF zK+7=|uV$BZ3BtIT4s`*Q29tq7TB-ySIz2k6z%ClQvOh^8Q;a#l-!8ncR@E#7AxWy^ z-D`AKKQVI&kTdRJOqu|Yrby#o19Bua%zZPxgY-C-Wj3v}N%$85>?l=%3J)u_ie+fBE3B-mU{rPgV~Vn7pp9%T4Ct4{xKX1g>P`2=MhKh&3pL zIk$)W9yGhAiyY3AZLi7r5oA!{2!0nI4vB3Ij^Ohg zGB+qvo#%EwKgnwhA;T7agw$N5nJONL8iALt?d4d4JOHour?s^ZVI8BWIJt_*eLJ`}|Ck zeO$cVOfK{7RqOGby9ZY}?G{mtNT6vEDScp^E8jNOmIKdB5UFSW>o~!YCK;Z>RPa1~ z@v5Ylz`cp{55Cbwq`Y$Ovvd0#_sf2>J8IQ0bOo(NQGPziQe;s;7Zf~qEX!4CTQTkT zJ)UKgrLeRoD^zU1K(D7F2R4l}%hW3DNFJjiLRvP@KJtuHUlvhOb+$v)1THJ;>%r?w zmn)>%6W;5xZ@r%cKOBQgwMEu)3zIz1-&RbhL0CY?1crH(V7Ja;@Fo8~s<@xBRalm- z0nCx5vIs9zZ#xpv&)=<;yUxD~dhB=v*VJs=Cd28}a0UU{yfzV0y-h0B7$a6^iNn!m z4s(ez+ZjVc&-l`1n@3cMDxTKpSZcjP-dn~^$N=>RRb!!_ldBk2bd$#moeB;%GfmhK z7rPc|ZSnjwB_Lvzw*5#69$XO5p9AGOa(`4uxru$*=9HYbE8@Kq5p&V-2DMeko}qTN zT-HbTm;FM2YLhGv4wy(d*#0erF36Xh@O)!`^9VV0P?#sq%V>M~0La=yLUnzi_Jm*+ zdP1&cpK84u{QeD$xub@2#8RUjRiGC!JAp{5kzKJftAK6V<@w~5mIhlRu7?79KJGuT z@BW@*`S>QSh0KIfT1l_+PwbTBgd8rcL9TCv>gYRe0c(KVYiPAu$2-YH&rqUe_yows z=Z*Xd*e)m-~!S_jFv51H?SR038#&P((ymXS0 zjE3J+zt<+Hjt1#zfl2WxPF9Y7HaG;^-yr;xI)2uWh-#?yu3fxpaZi3-emF?`^EB?# z(}_Kwj#J!dQ7G*?or(YZ@e4x;i?a26jXb}tT+YywYcXf^=>?MND*^76KysPbD1+NF z=E%@$FiZML~AWaiX6{dh>?3f z&xqtW09{^cgI$Ou0c$hPxj)lPWI{3=bZK?($i1Z!8f5c|*i`iuDCO%i!N=e7?Y ziLsVfdQ=pt@^kOzR=~=n|1K*C0^XYJc2mN~!~3#JSHHZh6z^DG4rfj4|VMbML{f^5|9T`~kB zjhtYfCUgQrCor~;;w>_j9ynWwLGY$V9pf(m2EH!TJbzP^zpTz$;!YjUWRMQ)5J+Ya zw)mWM?r)G<@54H|{w8 zPVCGhBPs2|8^PTVR2=3e6~^%q7P;4IYOjd$r~%eye}jj*fo{-3DJd(cO=$ zzwrTM&9s#<$y}>3o^db9-XuBDESeYNwU88jMv-?U!?d^aZhKJE z?U!IG{aRjknLf|jp(+r*@y)?ndC3gWRDifvxKGk|?eAzTv+Um~RLeACCR9{-wky=R z_xp}qg;Trm;CNzSf`QW5;Qoe-bKjzOfz<>@U)>1!L+GBT}pRg1{G-B&q+f-d~fWEroR0MudR0+ zXQ_p=jXY$qFjddb_?iGw)w~fp%Z{$UfK&9GuX&|vHyNF&ci4jXCM7x^h$*}|VPYso zw;(Na;e+7+!Q>sZ$3@Fp{lvqI+4U?8JnFxoGc54G2gU!h+KBv0u3`6b?;G`~C7EHw#1fzfTLmQ{v&) znWVxXp-YN{BFvIcehH1^G2u7WaO31NMLLv9A+L@W=>IcYD)a=mibjgJfk;P6*a|tv z^LUcRvoTT)G!?3U2tOw~Oes6EJ2WKvr~dqc+CiUW+q}S)tem&G(Q2FDSLU?S5(4ma z#uNu{Z40crp#2(aJVN+0*n;SbDCI$phw&sB5JDk@tc#71kN1}et$|TZg%Zq96fJ_+IX>hoWnd_)ys!^ zAyDKb2HOXrD8zowl2eH3n+fU$B)cW7pR(yuXlxp=%^PeA8Wvv)mly&Rg2R+ez5SaW z1A1=)37jU`9!;u*z8aqI|51Jz z&5>`n2~`7whh)EQsEz&_c*oU^JwgC+I;X4G2jt3bE=zCg+Zlp}V6=XR*iDb?Xx>Zgt+qhutki?i8@{~ZLsoMqh^^lsMHe$9k9it|_;eV}qV zm$|+A&%w=uUdrE{7uQm&M=kQ{YJuKwa849zFMkFH$t)|)XQH>&ly}PgWbE((%ydd% z*Zf3i^{NWB|5^7e8B#=tEKY``iCBHVQM~XB5AF=2*DSWyNdy10!h`xF(Ydj2+3+&Z zxmtMqb?DIx#)2a_=cdQPjg)}$P~$gTWu)2Pqp%c+2y9|QxVnB&={i@7;-kLtAahXFmI?98KWZ;EKj=u;D{Sg0eJ`La1(W2Y;glY)g4Lpw z+5V?&Ui{nk@g(bmpPgdOXZ>=`=xqXd`|Y#0m6yXu>c%AM>r%ZYtZ%=O)Ot=WXnA-ie# ze$}7S&<*@^$bozm;%zl90N3l#KjMc9=yfgKEyci&A5#}4ZaV(PdPKHuE>rQbGhZRO(RDTT)g&;t$RNwatB%M}L8Y9S4|fCw$`o+eZt%0OszClx;zSj<6vuh9`eS6GUyZy`J$& zn0J!=<(tGOemQY0qNG7P-3?3U^bJlR+X*aS_dCmPO&I4j9$hNH9N)JiMf$jhnt@(8 z-3_!o{Zt8!q9B@wFn$pP7|^_fmyKKoZL)0rF~tMsMwqz1WrE z&}M1rn`~}hy{+X;6k^>r(f30-hqFDi)7$(VXIIY!3~gAEN{E5wTbDEnv9N;wk!8bY9U?=Az02z}Fy7L}6*%PG5q!7eHv zDOU*Y?|2GaT=#Dr)Uz||qGngea~2)Kz2_}--fvBND8;j%RKNx$tDUM8JWXv=HEO|t zbSt~F>l^g+>x~I~*I}H}|DFq5Anlm-X{=#Ga($cCGC#HrHQCPO?+DSizUpEtPcxB{ zP)O&mOuJ?II=HF4YKGC8Hh%Hy4gGD;{P=SE*Vo$IZ-cMxflAsgAw1TDQ>D50hrhA9 zX)iOc7ccvpbPZB$Y@6TJ=rys;uQr(RsU9^8)er*Xw^E8X&LyomqkDb-9&Yn8dojmV zD>CN7k?JZ#tig9CBGFza!cXk?$1P}U)jNlG^1RY(%6fy0G{|D#Mf_OqvuRtik=~XV zj9$K%Kl{LT@_QBBwtTyM-)maM?TSiJsP+Nw*uS&b@$}P-HTj!&_vh?Kw6!pVfJ%Jk zeu&v-1947rghV=@t)wznzlsawo!;W`Bq@131`sn-y1#8Oys#dNCyVeusr zBlj_d%Q?MCAmldON@Ml>{s~_|VHRu`(R7Y)2EQC4wiNDxZ{?je=5s)$+Vw%PgQ)Oc z%CXey7@yL+7k6O+572`F#OV6;FNzFf3}+T9!UAHL;Ix><_S+qRRcpARjm9Y;oHlXI=QGcAm%jCANt;7UpDi1G8v0{I|<1uT3Z^6RzQdKGX|(TuZpEw=2nmsrjg) zh|V|IHC^VUh5EsAm1_`!%6E%T;hWOh#X%fTel>7QaGgB*n#=2r5PqjpOL-s>Iv8)8 zXwzrds5g`?=@qZhcp)x70MOAGvYNWcto*F#@ZcbB@V6-i=3OP#>OM8^>Oq~1n=C+t zR=U-BWY(R#x8(JDyL9KrXiVR(clJw1$o8h=n@TEY!n*9mhm3`;!O1Wm)p@(BmSXOT zUAB=xxyp&{`rHS*CZM?8Qlqjv2iHYf8{kjMI7&|8Gj&L z4SZ?queJdIsC^E@Aa0S?<5T47jZmGI$it>Nw2s=jYf(NWBa$WSP7f5C&@3uAU zIl*1R%NO|miYY@c1^jFgCnrdI^@CE`mZ6Y){zZO!0k=&nlS|xhd>Qs%#NDs*u4?*2 zZ77q8$4dQb{b;OY))3Q6b&0h@Y7Zk*4ocOsLbR1e z0u^zAJbO*_%3rrbAJ?(X*jN8Www7^57o3<97lM|$I2oJm&y?8XnXmE2`~d^zOA^?6KzdF$Gti3*MYafJys28v_BWxr&XpgbKpTd9!VQ2r> zb~)VsPn*!u)CQnl6zS}-SBeV=IUJ87^0P-z@XS?3>yXZ$s1&=It#9JGrTpyjj^awF z@{{FXS2tkJ9#CNOG*6WaWWMhy@feM+{C)&+Yl$M00~p5u_>N24LS;FiUo(BzP>1#E ziY43nGvD=C85lParTccDHgov{pAc3VTof`jZ!2l(O$7 z9wLQSW>DgX{-fB|<0zB#m0wt_VfrE2``!_ub=MjLDZPhD_RV@>(SQuVf9^jEIed_> zQaEcCI@$yFQnCBqMiW%jrrSKbkC~dH9Y7GimaiZZTqv}@1=!cdDpG5t(EO1~WKfOX zv>iyleAf|mlCqZ6I+Jj;K;HA!S;#V$g?YVvLfz?i=6yr1k^U;ww=r9B1_H(ZQIL_!M++igQtp3#ivD>kkvWA zuwKfh9IcyV4Ci{fp$*!=EL*@7!y`p9E{k6cwn63x!$y1rvdKw3GbrhyP8)yBwWQsm z*}1@g5h#96aXJk!9rXEZ)W7eueZ1}b1L$jVLPK-0_P-!*Irx7DV*mTA{snPo z|36j$X}Tw71{~}%YxY=_^UVZGL&rCx^DbQZL6b;4@!^ho^`9y5c^iaEnZY6w1gLz? z_e|nL(iW#q_7*!l;QzpjMTF@#C$gDB5laUttOdDrwD*1arycRQgTHNQp=L$Q_D%PSV71v0NNCz05rY!Dw5k%J=7A4{z(9qWj$!c7E&xG zh?=ewCHI^6po3}FNvTl3jv*i26CqTxiOm*3(~j`1J?PjX8R6e_bLP`w?5dsH-*+rj zBflkzPmO@hfO(diK!KeTN!Y?-UYL#Owq}D05>2Zrahnzlj*{%z%=nMK3{>YvpucET zpbDr59uim!+#HeLdefyz_OL84qHlf2nGw0>q?x_<^{B1+E8!sf6gjz{5E>N(CHS@d z6>m*FrAAQ>u31y?)vu5pKjU@pk^?qoI=EI`HiWW~iNxE$L7NjgU5rOwtaSppyG?XP zVh25)pN6X!STdY>+=~Xw3itqpL7xUi8wGh>+qN-Tc;-nbb3vhPe|n4|riSgykyAb2 zGz&ountlzpyFHa|+l%(Y7cPgDQR)2Nwl1`}0pmd-HnBtE;BL^6za4?9rL+}@@#uHb zI@-cp*d)(aZ$Tc{ZP1Bt7YnB^dU!#ed_9GK0fNwg#|V-+tbGvu4R=rMlxAhLhkC!& zED4A_B4$=qw#48&XSguL`3(mArx~WE&L(buN>>UTxwWF7)tintqGX{?Iw=0zR@y)- zRZu=qj2v*ZvsydmHawn01}STF+plUzq{1aZA6p9iR`&zfbb{x9^7ETeIv%q8BN^CR zZD*b;1K>{9Do5|H+rtIwK>O9YZ86NPoRE&w4kst4(aqC^38wW+hzQJVJ6=r9cI&{! z&I#C$%PD^b%*w4U7Nn-mq5L0)hEB#0@k~Ng5CxiWgRb2fCydW_{jvn&t-xf`{&QA@ zFL@Tsg$hZ+VTkUw1)#7xtmWD;E_W4^Ze3|TIW}#v%aoAAsLZjUs2_(JfARgjr{l2fKF=(*jEJtFfXG2LM zFp6tpanR@R)NX^JUI9quD>mq#Eti>Vwm#b^aXBb-e(D@|2j=ka*)Bq3$hl*f6U_<< z-q%~H6S4yQXTLmT!EoCxxJb9PW1;{x$;-#*U_3LkU0lb(D>sUJj?dYJXP+Xtj!&(P zDa*{E%4NdT*P&qvoxK>jw)TI&)b8T7slHoLysscvfUbWDwojH*(U<*QN=qhNg~}Gr zI>V8axGPMpkye(rMKR9s)HVKb*(-5yJ+X)UzFY6CEs~kF>H)dsh$AYCFZV)UX8zu^ zy)PZyk3$}<`(1?n{^>d+BHK(v``7GdDyK)G*gMLjS=(IXY^V~hqxL*jFUk7TM<+w> zpjH$Hml08nWlqxQMQ_DdGGw6s2Ky#nZm{)7uSf|)fJ$a=UEYT>*{6ISEA~eM)iBEQ z;B6qW?OQ^U{w^UnkPsWvcy|(UlN9OL7%Wctb=pkMJ{{#6>%28weg+*Nc-F=_$ncpNB?=21W{1^n2S4ZYT+x81 zo-&s&Yj4w(Ok@p)41`SepdGuh%%lvBR7jGf)COXvv>fSGUXcFI&+QFY+7|#^96nB& z1C^8wY~ixXz|dFQ{~oc53@g7Hc&5mn+_AC0S4jtUdu^ihC}=PebCItVIR7CDk^tha z`@WiW!~6sju+v$WH0|FHM6Xt$f9(;T?oq=MPAq4>?1_0vd-|&o_~UMAf%|}kl@jkx zfybI6tuW*WJ}z1jmEX)q_Pbm?sK5toK#X3!2+Fu2``=-D2$+f~gN*`sZ@c5|u~bxw;CNlpY0`iL)I znWhWM8m1%B*(p-aI?nTicxat=2~hvQ&8=osYvb z#cOSC*I#RrPl=Dc2Utorf4uay<#oBk#T>8Z~1AU2$uDNiWzOxBAe zJ@S%yul8n|kMO+>TWzY7HFyal1ld-Yjr_CjHu}vZ?|D93o_<2Lb2K>W zR{d^>fahA{(utHpznd8JK9y9BZ0p|DL zU(l=1jDDJpk$WI+fhpz@@mCxTa#X?}%lCnPf;Xshp^^1fA{|Ndk$MybNbbU1J18|o ztqUsT%TT`Y4HdYXxIA9!lVIKhcPWoGE)g)@$vl-9$29wCsd#L%XprH1$}!{3I`^o# zRR=mHI7POVr#4lxv(6=NveR)+I^VX=2WLx^i%hm!g~X?$`I@}<=APhn%Pde{Kf#OZ zR?WO22|+O}xn;P4pc_o~^xeU#@Pc`KGd^ivX>>OL8&O>O9-)v{!V%o#6Kn=gA!(Ln z9c&+%M!yP_rqbkmAPSbEa{tiKJmC5-19oih%rvF_@6FTy|J*YJ79`TJeShL8SyS}N zr@yz7G;d$Kes&g0GQWN0`lqBz;q>N_JwGkT^wDxTv&sT zb@t3W@|&%itEBMhlgkIGBW!FUV=goIcb^66-I722>R|As-%jCL_Xt3r;o)7^;ztb$ z&)q~QyIbyw0lPCATnXNt6~}Su3$)6HMd4mdY{OTXM9R~uA~nB&Scu=temj1z=Yx19 zG?szEAFPDMb7kzqKgo(t5F0w%e}8bh1m!#Lr=N3-EmZKdJ{~12OYpEs09#J$8}$Av zUV(nm{gX-WDO|@X$a(ZPON@7%U&^!n9$U1Zw%5yZZ8ZaS^$XK$?E>Qx4e2D=(?@*D zV46tg8h$xB<4vA0#G>BZxSAnqabo9StDrE&Pj`V3k`Pal>AoYb_EWFtGc&h~A~%yu zre1rCTbRNm+bQPUg95)Wlg?17!lUT8F>>uhn z(RBfukA)?C-@X0v=G);o({O`%6ri`4xA01zdS54QS(x+dm@=E_hU}5gqt3;4$b?L0 z@WSgoVBQZcfxU$zYaZq6`ngln?a6<*fb4}99&c&hKZA0eK9rSP3@a6o)q1v{reU=gNgv=ZoPm@B zit?^J4v4=P$KbBJfcQDb;nq*z>jD5CuJ`CnJ7d|ZIaS`zu}$9z2)F$BF8JAtK!z}O ze8la`VdX9cNAj8K^@(n{V zsmng+pUnWucN41mpFi)V@w0`$Q<74XRNM(94)%2hZxM?P1wbbHjxhLiR? z4M;^Z@*0cF`5}sQS1)(da{0+dRg*8=@QVuM1Fa~NTo;0&7TrwV-o*H@{skm1|Bybe z!_LpjTKyb$N-a!dG17^fj4LWUxm*h5s=vReElQFY% zRV;GpBa#qj7w`wil(RsBaPMnLq)ldvZMXY>%$|ELhV@k9-tRLvgtkhhiL8?stflIY1b60z6J2NJ>nD6&x#@X- zKeiNCYki-KEryUZLE=yea*C;m_ioiC<#K zFVg&Sn?o!8-ST6)3hCFwke{!A7Ud~fU$J}OuB7?lH?d3onp#(76yJosD;#`0xLb)~ z?aQ_bkC*Uy1#@b6&dH^IL26Yrm~KRJn#(tgM={w&r)v~5_?zMTzz%PB`USSt+!zhv zHQuq1D{{Radwrem4u!{g>-Bq!sUeTO4-LKn*t^cWiDO+*t!>|#g&Npg%(>nKC0&_N zM-Vc)Reuer{ZaVir^{Y3GyljyrP1#EsB3a!my=mS*<|#bUh36xp|5uC>CtDQ>^U*G`gq5L!>>wkZ$EK9!SsX1y_1*GvpHoaD?2 zzPb1T!N}ytsreqTOFj7M=b2G8eRTL@D!17~+Ccc@agEi2?YQjn#Pq@POF^*=hr3r6 zAE?yb`P<(6#v)l6ZkrrFS^v^bHoFkr9r~89p?`~Bdh$EqeT0vLS(RT?=~Al*FwlK%F63vbpb2S>z3WY05(UM|+vbfyvFa6a{`cju($S9XEE->mJ7kG?>Lv?)ZB%3#Izm@$CLFyNub_cuu*}r{NbM*2h5v<+?K08jSZMYAkJ?%`n z7|vTW$y7Y&dUgIs{0-{h*hIp;_et2;^vL@l<^>O?svSj}^^$@a*@9~nkS{}!a z1b2nVHW}EZ16gS6Sk{yyX(p~#>7L_vcpRsVb?6qz6AIvx*72o-Pd5GR#>8grZv1G zL;@aO7S;9ojasC-R&cfjs!TO zG2wdnpPQ5f1c@rXtcERHm>lj`RVjoXV_GUL()(CKgU{pl6yJQ}5&BaC@p#)PW$qky zjA(ftDmgLvPMSmYe&|VoKl>ZWPCAbK306Jr_a8HK+6RhnOpC*|Mz???-cvRmUTVfU zwn84)a1?RZ_jk?*pIcfAi(xJQORyMO2EY5bk`iuEM)T2&oqx;kuH5|ydfpQgCAD+k zlU)UapKS%A;u)&Le#Gcz68dn=qM-(FSWFKma*kX+B;Cu@Yr?zTSXg4qtlz_?UybGGEz2b8mpXeF zvh@w`|2p=&rLa7qHW}<0-d{4|@0wrgs=^%3p1{x9(_pc^jtGKr&bG<;(vc&7j$CF& zxv7VT1w%zni}PiZkSUadfiNjof9689IL>nAts^sOXC!&m1e2#>$ zN{M`Og?Dc!VJLw_`|0D3p@%T$uz0BZ+|j3lox7Kb(|(6W!GAg{?C zzU=sBAKTdF%aS~(Y@lUH#m84X4o|=)o^;Pr*ZICTUwf3T9a@$k^zsrHi0c*1<+739 z5;f;u8u+=%%|9+UGuroIIs(zXTqwje+ml+1n(TqE75x(u- z&=uHaE>xC_X3>wSSM1*DwIaC5`tA51#m^y>q~q6WRk}}i1Nb%y0%G0y*Gc8nV`4(u z`Mf-7Pi@Me?vK2)#jb_LAqV0#zRYE}zSAAJ`)1q#ax84cIlSAic-{X*w)#M&qF0@% z`g#t3DgXGTNYVHCJi}%B-50B}4j(qYrT9npogs{5C4QAA?4hpdoqUcXLS}E#NAJ@D zn)a1HkFqtnS^4C>SEj}ow728l_XIZWsPf_!d!<;XFFUr2|0 zgsze;{Sw8N6Krl@(Hj4r_7&}ob$>byO?tmuix^hP&j&7gC~fkK4?xk6QyA}XmEGC+ zD07hP6E$`-*_mdV#Aj%(LhD`WuWliaZrUhN7n@5_vs#bbH?B@{$Y_oFLs-b#C$DRNW7(PrX+?{dV_nJ*!r)TD$ud_b@(B!Czd4By4E5CATi%E_|7+a_ru?BW1`wucrEJC9~HtvK}<$!Z~Oj=w

x=_8 zXy$8Wu-H$ey>3W@ijWG(1DPm?*xrRHnD$hB5m}`EV zFqx_veXa{zXa#kF*qC0hUP!&sl1*O_n#F<>@-R&^EDCeYLp>YIn?j(-D$xAIwn}qH z5qdje$Z>=%!Ug`QosB_U31(M)ncjC$V#2rAaHzanj}q%Yb1+9YZs~Z^WfvJ(2fIwc zM~Um?!73P;2V3eX2TfzgiE5ZO0pWABzAHFkkZ-XehF+~^4eJw+1)kG<5~`k(1<|HC zC&24`lR=YjWeZ6|uD04csGJTgGg4{>T_k#FwK=N00P;(@*~3uvO7TjWWp;5c+i}8C zYbMh;=L9mrRP1utLn7*CtMaY!SPPUN$YwJ3J{>iOs>kjHZD*8dt+Lx(<=h}4G_)#$ z@sl}wUBz|V4X4tAq71Jl!y1m(Q`X5XWa%nzg&H+UW8Xj34wrdbu8e6UE|y~A#N||t zeIOEK4^PUrfp*$+4%HzEp@jrR_2C{8{fAk5?t z(F)a)_6`oe8+BNTmc8xek=B1;u|R1_4y8fF!qf0U6PDWGhw+xsvH+DkTqXu|;PfYy z((=mEc?j(NkecFV4$aunHZD(n{BDj#GKkGcb#TD4o-eZdJ1P{!BXNe}>&DTTd%>S~ z>bekT@WMQT0N7j@-kAY&CK;T}&mvTpFbEGW@<23V++iqjo??)<%<#{)}> z9{g73oM~;?vYX{?uIh@nb`RPv$Abw&FF7D;7=K^Qoz}x$M9Hz%4ob=>o%C6U%OsP1 zu_r`6OC$=@Ra=0nH1}%)xH{but!KlV3&B7XmX5NXn4l4JJjr<`$79#D7e`3(x*f%w z3lc+eIcY%$Xzh~5!QT~Bf1oEGD*N=K)+CuM=AHYuO$w={VBtNhg#m0-r44IRR_8me zl{r5*^yPg)biGWq-@DB;0m6o&W0$*^^0%Z8?yZ3ax%uqMzXpL*DS4HlsN@|Oe2;9b zZZQiSWZyBf$CMuTMlY5FME(2~p*CA71?se7n#kcqz$6EInwLFXa!gjjqFj3H`tRiS&I>qpjvE7^blM4YZmGTd6-4d z)~@yQLwVS6R*uwE=?K;t?ArMfu~@5pvku)7j>*UONoUD=B@7JNX#wBh=YAR$6MU)M z+4j}FX)XA4ftG6j3*e2qk0KhoESm6~c#|obz?~BfYbF-5yP@na`kq_u0?3Fip1KfP~ut2NnAR82Zs<0|ulfLzDTrmH8Pl9PvwH`46@4 z?{$+;ow{PKTe*#?KnA&m5!k6yL`4RRr3C+zG1@ofa0YjbNr)Gj_7t=>C=3RKQJqQWkh@j%%Dwi>=) z85DK+(EpSS&XKN?Zf4fspNJ0=*$bg~v6|*6-B1uVrbgLfcD)3P&KrStC>OgtJAFc? z>zDI9&=}NsNQ>rpw8T$wfCP*JFs;~b?kuR(`*0sj1GIS-Ov;v;u1OZW>lL`uH^%^n z%TY0va96~^l$_CAia7>$CYL>@$H6;Sq3k5wwvOLq=iE}>U&=r+jLtiU=R0F@Z!N%x z7=t(^Lq4aqOwCY!(?c#Q&ma{&%d-Z@G5dr>^32Q@_RFW%P5Bz4z{~T=eztPSYUJbq zeD-d@u*{JM+_Zs;Y2?Ab4T*5QsZxmaNr$37V8NYyn!igr-?2xZHN{7b#KfI1NUk)A+8oUH9{HCqa`bdzcAG!$rE z?wB3HV?)L=z0O{12yUXS1FL<8(Env&!vkcn{K>XHLUG*3TESI@GwNXXCD2hp7?`dB z{Uov8leHoBL>nMP@U(S50Q&G8X_z&EFmN$amos<%isu9+sDCM-vs1Uh*|Bsu-Cm@g zi?%&#$J|!}6iNJ3F_lAd9UBP;DY?EatP!k+oq`T~y6v<=^VyMG}S zjG7`QsADSY@tK3xUwMNs*$eKu*y45DyV|4d8M_EpeE&Q+vL9c-3O{#Tof z_U`4VPKSt$=9euXL=%4Gr$02Ygu%fQU;9k0k_U9!QGKWqkL_<2A|3N!&cyy4i26-K zt$3!x*5n6$aX8iY2uZ4j)@%K3heyEB+T0Fi0D1c0i$9f=lkpZ&&@~!jmVM$iMF>aC zyF8OY6}6J*LWmsm8txWq#MXhXis~Uns z4*-^_1G>^n)@d1w`_t`_4=57(SVQRJCliZbGh*g@z(>t5d-v)#&xPXh!S^Mx(mg1VX@vHl2&BteMpHfY@(!h<#t_be5)bQo_-{7dA3 zPgpxTGF_V^#`t9CQT*GAJ@LwLPG68>nATb~fX}MvXi=_HVCgwUnZk45e`qA>*b!XC zx!c;L{W9J#UD!*wZhz|#D|4{w39$Z%&)g>LLEI;EoA4(UA7i|s$;omQ=B#JL80(W1 zzeDDGRODO*CA0JUo^!oVhILAndAo?eGxt%f1xwaq6EkT@e@qo?{&4H zgs{?06#Y!?BxLuX#xj0gfpqR+J_hoWOp#CmRzMAgMVg>Oi5lL4-c5x4^!vWcA<Ssl(va^H;xc;J`ZdGNnQam_O6GW+96PL zISn187>A;XB>-FCEs-s3LL|psd2HunKIS_; z+0WCzC;_YkA9g-uycAOp!wcwRR|jGcq_^i>L44s|@~W9Ap-`6>Z1!fMyj`3jII4pY zcd)d~Ma9V_4B`s?0`DgLr+WjN_LmyUd(@mt^f2GzF%+rTG1Sy+s041N`a)-aLz+uJ z%}Q$=o_z$z)h48R60QnZ_O>?Z%X9(nXNxMouYR#3=c!VBu~r#}NF6nZp|knxy1B~J zpmeQ>9hizKMS2Vd-W@vod7n%NHEuuIO};6C$TiqG=sCc zIz>=MZxl}BZm%;zn}K3)Co%rsJTU&iEuj78_xT^~_eL^I*zfxb zH6$h)=VG6NqtHg(DUHYwXnUyXmq&dTo@?C>KW5oer@O(VYF#!}5ZLQ)8f0^-cQq}p zc%wYGKjpV5l@*LIZS{GW$D0S84v>8_yXnM3<#&ZmeR-qFzMo794G$E&8{d5%sEKn% zlOr_uSTMszKED%mvj3hLYI8`xnd_XZb*<5HK4!H@o>L21b(H(jbFsvll@bQtRv+%N1FN< zS1bm~AtEe|!Vh9>65Mg69!ArC15>3vFVBOmMc%yJAG-uDODA`}KtHmSAz1K3 z!|InL_E61y0n;sm3#TC_dMAJyOu~n+b~`)Q+g{ft0KmnfICalg?%|xm0bf~jxj!2`{^ z272ZXX1e}Jp?EYgS2Fv#IdccTwXkK}%Dr9nFp%`Uztbg?wao=l%q%U+d|N<3@cE2& zH_SXsL(D4HOmv+Z@=rZVN5J=qq^OH&1(@9n2TJ$sNTS-es&0GXY^ss8&THq zC5t2&)-;B~M^IyM&rAsKTHCdeT03?mEl?XGbxJVpIcB=mC4fgB_VO32kZB8XK*yNU zFlD#N{MqF>A?9(9&AVhRo7u0TFo7X^?55b%4+yeT8&k7#7Lcvc8tNmUks|bhtx5hu zHaNE~NgVk&zKu*~&-9pW9|CmNi-G|Z*a5BQE0Om+KE{AJGR;}}4|Fo1Ym0$+{$t0~ zP3gKP-wCsCp}h$2yq7Jsl-#L;ahlR$x+O3($;(~hQoQB_3Ix@>?TE`AhqDDUpRFpF zgs-P%Ivsvjcl{o+5=xdj@f6SnojBGBK59>%=+leg?`&KS#Df<{{(SjvUkjZ>EejL6iglxIs>j7g;YkFB&ZT#d7G{U0a1IC#T0aU@dup?O z$%31j5N;~SmigG!$kTi_;D?-`mp75GaK-WE-Mv_uhSOMD_2OVilqL+}jbBN_nBc}|}iMy3c1;*bR)Q_O&>FAj3=4tn8 zK~6O0ZZ&oBg}fG9e8@=r=GyWIR(GTbzi?k>Sf8I*Whm`NTt!NGdlJ8L&4|A+uUw%c zKzvh4QQgP8ISXuVgo?;;{XxdWR=G44^YW_NawOWKsIT?TL%uQrs==riSgw-gEkFRX0=kuZefg5O-o>UZWE6uQiF?$cz+H`7qv;Bq&DR8V^ zn`Am`X!vk_-^`gFWvHpPF5Q)Z3F~5+b-v)X;q*}M=i!<<^%o<=favix+e=wgv6DE* zKe)LDwd4=v%pOLF?Y(U$6xGgRYN8_G=mna-!n=c-9F6odG28c`M|f&YijZH`Guy7d zqiw6JrBxE zO=i!KoiX}1+=)LJbzyX}VmhZ!$V%asuvs`r0I|du+%gC{LMJ;INRYX?s8;&f8SpA0BSVyI zzkK)W3iygmG9a9#O^JP1p8)qP7)9O%a{zolf5<#%ViMI{m0;{`8*jf^^P`*;jI^gh zL06JSpvAU&zc-Gttg8t&ZlZ=?(ybAfvr4&&K^CGJZuRAndBNqDXH+wUnzmeMj~?i` zKn}aaPIk*O5(gh*U2Q|2@~9d%`Uk1;;L+q^D1Yd4@KCQIwrng$K>lFryK76~0V7W6 z7ENNf4IKe^wq`TKE4VJq$J0y4_bh5kj+ioLkcS*guv^=RRE}Ks#Q5%~Dk@ruN?){G zyTAr%q#{zdZ`eTv(F)#gLky!l*1b*>03E`=y%+mJ?$R zDJ2WGREzN?RvHPkQ1AU$jT~S*2fz7+^WDWqpI;`XU6;C>5H0~ zCA7J+l)5TXLL3=Dm|dL6$AJxc8qMQjbzxt4BW2gxGRw72Ieq1&QpU{D}Tg)v>t_GKv)C^Bidyh)>JtJh<@wie8W9!9#t z!_3U9&&8W&PX`%$bjy)c(Di2LTK!9y-_}klkwl94&jM>w&gfv(uT!3)z*Q{MLp^E&(0yr zY7HA{J>gCtN3u+P<@24-rdD+wb_yo zWvwl3?@WZ4$l2ZM%0EA0HO<~e%e%naE7g$Xp}R_gi?iM@`YrR@KF07q=zCkmD-A=s ziY^ZyRwO!id|Su0%CY>Iitc$IW0$Hstcqzt$+Ewd! z(V@RG$<9A4L3VYl`tfJL=TH8$du;i$#lIaM3rjSl3%V1*>W{g`UYe5ul@yK(ooL0L z4N0Z7OlXMcV5xlnww4(SlNPi6d^aj%|E`Y_Vzt>TX_!Tss4`_!SbHZyL;LHp8-GvHiG<~K%fN2W^?{Tyj2>D9<-{zE-^&B378P=Yv!o%e# ze5i1__5GgTDoFk7cpK2#xzh9i3ue2HOHsGoqFEu&ts5lp0(+lD!o0WWYwF8zv-?x^ zo+(Y!HYP3YPMOqZC!+qi8i3u(lQgCOlte&d?VG=UY5|whPwhQ(p1H+rysQK44d(aV z*TVJ=UqAb7(&^p)QENF%D);>VzliDk46~Syo90UM?HUY>g~CEb-AP^c6OWOt4Wogv zt)U4c$j0t({{-a|1lbuFS(!MI7@C+_0Qt#|8(YapER6Zd)YxU2W$i>w%q=9{9ZZzn z7$+Hl&M)p@j|2+ONE*ralHUxD12fG)F zOdtb0CIBNd(?27>ROEYM!XxTnV&G)!pkixlE%5h|i(5FDxY{~cIg*Gfvoiun059Wa zWM*VxW+#y~FtPwT**co@G5u%Ve|6?xVNJvgoJ<5>V6ihWb1*P-s{ojJn7MdZxi$Z- z{@+FaQC-&7*uvE9|5BZmg9pI%kLv#>`bTv>roZ_78?V1zjK7BZALsvH9F2|siP6s4 z!TN7k%h-s?#M;Eh1nA`Wf)en5qBJ(*F|~EDF>n&FurV+*VX_07@iG1DezxVpZ zEiXo8`lnO>yX*hF!v8CUe{ug`3IF$?Z;iYP?i;Q*T(2VVD)F1U-f+E&z^lY>?s~)Z zDgv((zq#uT*Q*G;O8n-oH(akG@G9|}yWViUiomPHZ|-`-^(q3d62H0Y4cDs(yh{A$ zt~Xq-BJe8lo4ej{y^6rA#Bc6;!}TfxuM)qx>kZed2)s)C=B_tfuOjd&@teEeaJ`Da ztHf{adc*Z90?s~)ZDgv((|Bvp1 z``6kJ6X45&57(EK9T0vFOJHD#5>jHqDxkSZt@qk0n^;+66VfuI>=@t-nHI%G=Hc+B zz=H^Xe3H~`MU7;6sUHK?74EZiUM3okb852l0|zwazxOWZMAI&ZkRU4}J2{xQGtPCe zLh4;tbfJ5XukXEgG8#R%JjHkGLx%+*~=o0jbE2UHln7C0E`9zrSY)mscMFYwaS zZwzj=w*tu2zxjQis8=24I<{4U2$Gc{ zByB8-!tiy^ZqKvM3UX#(ZRXR_uoxng?1BOe%Ne@y&Y9!!!Qp)hdDG4*@BMyqbc@~> z=1-#VTkp-0e;t06MIbf(fWW$grPKPpmHX%Xd)IpHlx(^3$!?hRo1nYfXmdr=ljV^v zwOW zKPzzHZR%o%9O!i2Rr`ky*P3KyrYq+u_8ja(i4JJde*fseiYEv(zKkw1g7&;r&=?*0 zdGH86NSO(IGqPU@cN8*l+#{ToxIFKko^6SpYND{qO8E&Z5GXtTqU}Qe1slDxfFBdE ze!s48`+Hu0F&+9(TC*^FtzacM(eU6oWEbJ@Z&6_eE#PeU3I1^)=M?l9USITerOoK?LrgRjT_yB<8;|6$~(^^aok;KG_Rz6zDV&2 z;=5Ma1u*iIbD@pNq@iK-OIo$w$mz>2%X2Y@pGncgpSKmt5$ofq*8tXd4rbsf9KpL) z4tFLBph^g7z1htq@F6r-M;}X5_$D8{TH{1E?BdqLtuV{2?ML2&Omj{^kzX!!2MYn8r9KYp)u3^lIRWCBvsHWU&!GE;Ha{!96_omA|su}Kf z*B#tOQ!JVl3pFr8?@GUp!Vhixz$(G;J8p@;!#S#Zx7Q!e?_JL20MyA zO*gC-s5h#HPOMIEbscxpK~uLgIiB z2q^9yF0J_FZCZCM;_5Z@!NK3pbC1h!b|`c)KNzCRE}&3- zKN!I}9mdJZk0$!C`|*!PN`L?z0cL}6B51tfodTBzDZczj<%c~)RTo9d*l9)atGF^~ zTmuV+wJg4e_rcxi-*0Z^(CWJ$(F%LWD6WgWYxE%{D_DaoYCFmK|R8QJl%!3 z*|bZIo4yr_ND)L*WTTh4;*S?gGxwYPR&r2n_4L3S0hw*4KS+3-6+e|OxyyHk6zhB| z@fVyC&=?L{Ac=@pXqUQT0G7|GI7h#ZKYjtGa>uL_b)l(BXNKA9OnUtY`NG0KaEZp$ zj&Rv)sPIpI6YJI-t(Tlww7;dZXZ;`LfiUG4+7hkWOV%?(K98t7b|HPWSrYnV?x{~i z!=kFhR8ono?g93P+QQHi%H{<4BdL^DoXY$4nCMqD7j#sFmqn9tdff`+r;C7gPxbT3<{Ao1?!w8T}lGsvaj?ShfXpQ+jK=N`|UOh!ue`LMwSXhvDKK;&&8H)#0vGqnbi^SX8!3bQQ8t9;}!Cyag8^sDR@@jt_-< zvI>5Tn+9Plgu}A3nZbZ?^Q#iKt<&~Q`h&$rk|gcTKx~$AXFrawUmvFkt-cpmCmrOU zCGYUx``hQpjfKS@+I7Qr**{D@ZF*2&ndN-Gr<(Zg6M{Ec{^=5jEb%);8ae?Sqa#^T zgZ7UE80oI{4`YV!rJOGmpwRt*ebIM+9O=LBvytPV`!1sZ{7Zma6vFAK4>A68O6q3f z`@VrLEbh!QN)49Z@diC1xt=`ySHaPI?y+s%8rts-)fGFpfheaTr$e(Hz@HP+;L*mM zp_%LNf=JCYX{!rBp$y=pBEC_ZH0z7!qm!Qu4aIS?H`)R^gm!u(_)x;n_)%y*wk_gf z@8Nf6$ldKQQ*c|P6)z;^s4q7_K^^9H>3=1nWnM$0b^}H*V~8iU2|;F^l46J}i0@G* zvsWCp>LFp&kYQxD!H9+lkNJ}thXVT$LlH`X3Rc!rcrl=BGtH<_5xhTh;e2<6$kj*| zjm1ImT3*ocAGyLUPX59L6g-VCO8LoGhcP5~CCn9>Yr7Onk|`pgKXG*DtJO!pahoiy zuqD=aD2dM4YCe%$yL5o1NOXuioe#c515TUMH#Q@e+L+SVFzv9~! zKRxo=eSJb8SoVgSZ1sYd`=XIJvnSXh0s9~TKV0mueKCXQrXIi)nW*V%e2ESWl|&p& z$f&h47D@Q<^DB5gGlcB0AV~?^=i6KSfkm{vd_$U~Uf82QaQ0qF5>N#Bvjud(XvP4Id;So_|0mzk}kj>Lq2hc?i2kg`y$GBqo=QrUaP2gj$ zf-aH;t7Y6vTBa!5<@*Y5p5jd3*6H0vL6(B2Wfx~$-``=1AkLx!*?(_IVvqv7B=*F2 zlcVPrOnrPSJElaxU_6AF!+iFj z8$|_O+`qIq-{{4_!i}n|@f^jZX{-QOXTtfbjIn3e$o$|Fk?Mx&ba3#<)A9Ao916X3 zo@Gw5_o4T@@}Wgs4jx&p!`P3p^!%~zgDV}~)?f4JX~IX*{cvU`tmj)Q9J2elLRsu# zK~;&0Wbi&^QIy5*_{$$AU0@Glf}J)5H>&#>8jfW2YWo0}Bno1O_Pr<^_ zB2A)^UzEGfiaB+uXnnxKPOpv>zn2ZQDDXEwqwr76sPAt7fYLS8#%Ay4%vi%m?_`rA ztzk@p5=q@T?$b^5`HB49`!+nNI2);8 z_;oQu^l!wCyDp`brkVYyeXN`08`JzcDpzK*#`S}B9WVXds7!qqeKI$(7H2C>{cYkj z4Hiqz81yFy2~$ai(cyip7*C~|>J&F~%Nh>S9W`CNhV>DZ^)f1GOp@NTWd^Dq%TMXL z3ul09BOI@?{pi8dz`PvZkoX&+-#tv%yH5cLF=a?X;s+BWx9#6vHl=a4WAQFh{espo z4zm5^yH?I&?Qz_Um)A!xrboq0R_e(6aqcLua=K1XRICS@>olXVuF9T!N~iX+l?zFJ zn<8XqtovJ@$x=-GDI%JIh>_|?3K}M0xgRYDs9Ylf1%wAa=SN};6E#-E Tci{bhEwz{WC?{4XqVNA-TM;k( literal 0 HcmV?d00001 diff --git a/docs/.sphinx/_static/github_issue_links.css b/docs/.sphinx/_static/github_issue_links.css new file mode 100644 index 00000000..af4be86c --- /dev/null +++ b/docs/.sphinx/_static/github_issue_links.css @@ -0,0 +1,24 @@ +.github-issue-link-container { + padding-right: 0.5rem; +} +.github-issue-link { + font-size: var(--font-size--small); + font-weight: bold; + background-color: #DD4814; + padding: 13px 23px; + text-decoration: none; +} +.github-issue-link:link { + color: #FFFFFF; +} +.github-issue-link:visited { + color: #FFFFFF +} +.muted-link.github-issue-link:hover { + color: #FFFFFF; + text-decoration: underline; +} +.github-issue-link:active { + color: #FFFFFF; + text-decoration: underline; +} diff --git a/docs/.sphinx/_static/github_issue_links.js b/docs/.sphinx/_static/github_issue_links.js new file mode 100644 index 00000000..980609cd --- /dev/null +++ b/docs/.sphinx/_static/github_issue_links.js @@ -0,0 +1,26 @@ +window.onload = function() { + const link = document.createElement("a"); + link.classList.add("muted-link"); + link.classList.add("github-issue-link"); + link.text = "Give feedback"; + link.href = ( + github_url + + "/issues/new?" + + "title=docs%3A+TYPE+YOUR+QUESTION+HERE" + + "&body=*Please describe the question or issue you're facing with " + + `"${document.title}"` + + ".*" + + "%0A%0A%0A%0A%0A" + + "---" + + "%0A" + + `*Reported+from%3A+${location.href}*` + ); + link.target = "_blank"; + + const div = document.createElement("div"); + div.classList.add("github-issue-link-container"); + div.append(link) + + const container = document.querySelector(".article-container > .content-icon-container"); + container.prepend(div); +}; diff --git a/docs/.sphinx/_static/header-nav.js b/docs/.sphinx/_static/header-nav.js new file mode 100644 index 00000000..3608576e --- /dev/null +++ b/docs/.sphinx/_static/header-nav.js @@ -0,0 +1,10 @@ +$(document).ready(function() { + $(document).on("click", function () { + $(".more-links-dropdown").hide(); + }); + + $('.nav-more-links').click(function(event) { + $('.more-links-dropdown').toggle(); + event.stopPropagation(); + }); +}) diff --git a/docs/.sphinx/_static/header.css b/docs/.sphinx/_static/header.css new file mode 100644 index 00000000..0b944090 --- /dev/null +++ b/docs/.sphinx/_static/header.css @@ -0,0 +1,167 @@ +.p-navigation { + border-bottom: 1px solid var(--color-sidebar-background-border); +} + +.p-navigation__nav { + background: #333333; + display: flex; +} + +.p-logo { + display: flex !important; + padding-top: 0 !important; + text-decoration: none; +} + +.p-logo-image { + height: 44px; + padding-right: 10px; +} + +.p-logo-text { + margin-top: 18px; + color: white; + text-decoration: none; +} + +ul.p-navigation__links { + display: flex; + list-style: none; + margin-left: 0; + margin-top: auto; + margin-bottom: auto; + max-width: 800px; + width: 100%; +} + +ul.p-navigation__links li { + margin: 0 auto; + text-align: center; + width: 100%; +} + +ul.p-navigation__links li a { + background-color: rgba(0, 0, 0, 0); + border: none; + border-radius: 0; + color: var(--color-sidebar-link-text); + display: block; + font-weight: 400; + line-height: 1.5rem; + margin: 0; + overflow: hidden; + padding: 1rem 0; + position: relative; + text-align: left; + text-overflow: ellipsis; + transition-duration: .1s; + transition-property: background-color, color, opacity; + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + white-space: nowrap; + width: 100%; +} + +ul.p-navigation__links .p-navigation__link { + color: #ffffff; + font-weight: 300; + text-align: center; + text-decoration: none; +} + +ul.p-navigation__links .p-navigation__link:hover { + background-color: #2b2b2b; +} + +ul.p-navigation__links .p-dropdown__link:hover { + background-color: var(--color-sidebar-item-background--hover); +} + +ul.p-navigation__links .p-navigation__sub-link { + background: var(--color-background-primary); + padding: .5rem 0 .5rem .5rem; + font-weight: 300; +} + +ul.p-navigation__links .more-links-dropdown li a { + border-left: 1px solid var(--color-sidebar-background-border); + border-right: 1px solid var(--color-sidebar-background-border); +} + +ul.p-navigation__links .more-links-dropdown li:first-child a { + border-top: 1px solid var(--color-sidebar-background-border); +} + +ul.p-navigation__links .more-links-dropdown li:last-child a { + border-bottom: 1px solid var(--color-sidebar-background-border); +} + +ul.p-navigation__links .p-navigation__logo { + padding: 0.5rem; +} + +ul.p-navigation__links .p-navigation__logo img { + width: 40px; +} + +ul.more-links-dropdown { + display: none; + overflow-x: visible; + height: 0; + z-index: 55; + padding: 0; + position: relative; + list-style: none; + margin-bottom: 0; + margin-top: 0; +} + +.nav-more-links::after { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23111' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E"); + background-position: center; + background-repeat: no-repeat; + background-size: contain; + content: ""; + display: block; + filter: invert(100%); + height: 1rem; + pointer-events: none; + position: absolute; + right: 1rem; + text-indent: calc(100% + 10rem); + top: calc(1rem + 0.25rem); + width: 1rem; +} + +.nav-ubuntu-com { + display: none; +} + +@media only screen and (min-width: 480px) { + ul.p-navigation__links li { + width: 100%; + } + + .nav-ubuntu-com { + display: inherit; + } +} + +@media only screen and (max-width: 800px) { + .nav-more-links { + margin-left: auto !important; + padding-right: 2rem !important; + width: 8rem !important; + } +} + +@media only screen and (min-width: 800px) { + ul.p-navigation__links li { + width: 100% !important; + } +} + +@media only screen and (min-width: 1310px) { + ul.p-navigation__links { + margin-left: calc(50% - 41em); + } +} diff --git a/docs/.sphinx/_static/tag.png b/docs/.sphinx/_static/tag.png new file mode 100644 index 0000000000000000000000000000000000000000..f6f6e5aa4bc55fb934c973726b10a0efc92445a8 GIT binary patch literal 6781 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 literal 0 HcmV?d00001 diff --git a/docs/.sphinx/_templates/base.html b/docs/.sphinx/_templates/base.html new file mode 100644 index 00000000..62ffe6b8 --- /dev/null +++ b/docs/.sphinx/_templates/base.html @@ -0,0 +1,7 @@ +{% extends "furo/base.html" %} + +{% block theme_scripts %} + +{% endblock theme_scripts %} diff --git a/docs/.sphinx/_templates/footer.html b/docs/.sphinx/_templates/footer.html new file mode 100644 index 00000000..75944868 --- /dev/null +++ b/docs/.sphinx/_templates/footer.html @@ -0,0 +1,90 @@ +{# ru-fu: copied from Furo, with modifications as stated below #} + +

+
+
+ {%- if show_copyright %} + + {%- endif %} + + {# ru-fu: removed "Made with" #} + + {%- if last_updated -%} +
+ {% trans last_updated=last_updated|e -%} + Last updated on {{ last_updated }} + {%- endtrans -%} +
+ {%- endif %} + + {%- if show_source and has_source and sourcename %} + + {%- endif %} +
+
+ + {# ru-fu: replaced RTD icons with our links #} + + {% if discourse %} + + {% endif %} + + {% if github_url and github_version and github_folder %} + + {% if github_issues %} + + {% endif %} + + + {% endif %} + + +
+
+ diff --git a/docs/.sphinx/_templates/header.html b/docs/.sphinx/_templates/header.html new file mode 100644 index 00000000..1a128b6f --- /dev/null +++ b/docs/.sphinx/_templates/header.html @@ -0,0 +1,36 @@ + diff --git a/docs/.sphinx/_templates/page.html b/docs/.sphinx/_templates/page.html new file mode 100644 index 00000000..bda30610 --- /dev/null +++ b/docs/.sphinx/_templates/page.html @@ -0,0 +1,49 @@ +{% extends "furo/page.html" %} + +{% block footer %} + {% include "footer.html" %} +{% endblock footer %} + +{% block body -%} + {% include "header.html" %} + {{ super() }} +{%- endblock body %} + +{% if meta and ((meta.discourse and discourse_prefix) or meta.relatedlinks) %} + {% set furo_hide_toc_orig = furo_hide_toc %} + {% set furo_hide_toc=false %} +{% endif %} + +{% block right_sidebar %} +
+ {% if not furo_hide_toc_orig %} +
+ + {{ _("Contents") }} + +
+
+
+ {{ toc }} +
+
+ {% endif %} + {% if meta and ((meta.discourse and discourse_prefix) or meta.relatedlinks) %} + + + {% endif %} +
+{% endblock right_sidebar %} diff --git a/docs/.sphinx/requirements.txt b/docs/.sphinx/requirements.txt new file mode 100644 index 00000000..0c267c75 --- /dev/null +++ b/docs/.sphinx/requirements.txt @@ -0,0 +1,13 @@ +sphinx==7.1.2 +sphinx-autobuild +sphinx-design +furo +sphinx-tabs +sphinx-reredirects +pyspelling +sphinxext-opengraph +lxd-sphinx-extensions +sphinx-copybutton +myst-parser +sphinxcontrib-jquery +sphinx-notfound-page diff --git a/docs/.sphinx/spellingcheck.yaml b/docs/.sphinx/spellingcheck.yaml new file mode 100644 index 00000000..ebef2081 --- /dev/null +++ b/docs/.sphinx/spellingcheck.yaml @@ -0,0 +1,27 @@ +matrix: +- name: rST files + aspell: + lang: en + d: en_GB + dictionary: + wordlists: + - .wordlist.txt + output: .sphinx/.wordlist.dic + sources: + - _build/**/*.html + pipeline: + - pyspelling.filters.html: + comments: false + attributes: + - title + - alt + ignores: + - code + - pre + - spellexception + - link + - title + - div.relatedlinks + - div.visually-hidden + - img + - a.p-navigation__link diff --git a/docs/.wokeignore b/docs/.wokeignore new file mode 100644 index 00000000..c64a6037 --- /dev/null +++ b/docs/.wokeignore @@ -0,0 +1,4 @@ +# the cheat sheets contain a link to a repository with a block word which we +# cannot avoid for now, ie +# https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html +doc-cheat-sheet* diff --git a/docs/.wordlist.txt b/docs/.wordlist.txt new file mode 100644 index 00000000..5fb54827 --- /dev/null +++ b/docs/.wordlist.txt @@ -0,0 +1,47 @@ +addons +API +APIs +balancer +CharmHub +CLI +DEX +Diátaxis +dropdown +EBS +EKS +favicon +filesystem +Grafana +hostname +IAM +installable +Jira +JSON +Juju +Kubeflow +Kubernetes +lifecycle +Makefile +MicroK +MinIO +MLflow +MLOps +MyST +namespace +namespaces +NodePort +observability +OIDC +OLM +Permalink +ReadMe +readthedocs +reproducibility +reST +reStructuredText +RTD +subdirectories +subtree +UI +TensorBoard +VM diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..dcf383a4 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,65 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build +VENV = .sphinx/venv/bin/activate + + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +install: + @echo "... setting up virtualenv" + python3 -m venv .sphinx/venv + . $(VENV); pip install --upgrade -r .sphinx/requirements.txt + + @echo "\n" \ + "--------------------------------------------------------------- \n" \ + "* watch, build and serve the documentation: make run \n" \ + "* only build: make html \n" \ + "* only serve: make serve \n" \ + "* clean built doc files: make clean-doc \n" \ + "* clean full environment: make clean \n" \ + "* check spelling: make spelling \n" \ + "* check inclusive language: make woke \n" \ + "--------------------------------------------------------------- \n" +run: + . $(VENV); sphinx-autobuild -c . -b dirhtml "$(SOURCEDIR)" "$(BUILDDIR)" + +html: + . $(VENV); $(SPHINXBUILD) -c . -b dirhtml "$(SOURCEDIR)" "$(BUILDDIR)" -w .sphinx/warnings.txt + +epub: + . $(VENV); $(SPHINXBUILD) -c . -b epub "$(SOURCEDIR)" "$(BUILDDIR)" -w .sphinx/warnings.txt + +serve: + cd "$(BUILDDIR)"; python3 -m http.server 8000 + +clean: clean-doc + rm -rf .sphinx/venv + +clean-doc: + git clean -fx "$(BUILDDIR)" + +spelling: html + . $(VENV) ; python3 -m pyspelling -c .sphinx/spellingcheck.yaml + +linkcheck: + . $(VENV) ; $(SPHINXBUILD) -c . -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" + +woke: + type woke >/dev/null 2>&1 || { sudo snap install woke; } + woke *.rst **/*.rst -c https://github.com/canonical-web-and-design/Inclusive-naming/raw/main/config.yml + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + . $(VENV); $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..be11d82f --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,182 @@ +import sys + +sys.path.append('./') +from custom_conf import * + +# Configuration file for the Sphinx documentation builder. +# You should not do any modifications to this file. Put your custom +# configuration into the custom_conf.py file. +# If you need to change this file, contribute the changes upstream. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +############################################################ +### Extensions +############################################################ + +extensions = [ + 'sphinx_design', + 'sphinx_tabs.tabs', + 'sphinx_reredirects', + 'youtube-links', + 'related-links', + 'custom-rst-roles', + 'terminal-output', + 'sphinx_copybutton', + 'sphinxext.opengraph', + 'myst_parser', + 'sphinxcontrib.jquery', + 'notfound.extension' +] +extensions.extend(custom_extensions) + +### Configuration for extensions + +# Additional MyST syntax +myst_enable_extensions = [ + 'substitution', + 'deflist' +] + +# Used for related links +if 'discourse' in html_context: + html_context['discourse_prefix'] = html_context['discourse'] + '/t/' + +# The default for notfound_urls_prefix usually works, but not for +# documentation on documentation.ubuntu.com +if slug: + notfound_urls_prefix = '/' + slug + '/en/latest/' + +notfound_context = { + 'title': 'Page not found', + 'body': '

Page not found

\n\n

Sorry, but the documentation page that you are looking for was not found.

\n

Documentation changes over time, and pages are moved around. We try to redirect you to the updated content where possible, but unfortunately, that didn\'t work this time (maybe because the content you were looking for does not exist in this version of the documentation).

\n

You can try to use the navigation to locate the content you\'re looking for, or search for a similar page.

\n', +} + +# Default image for OGP (to prevent font errors, see +# https://github.com/canonical/sphinx-docs-starter-pack/pull/54 ) +if not 'ogp_image' in locals(): + ogp_image = 'https://assets.ubuntu.com/v1/253da317-image-document-ubuntudocs.svg' + +############################################################ +### General configuration +############################################################ + +exclude_patterns = [ + '_build', + 'Thumbs.db', + '.DS_Store', + '.sphinx', + 'doc-cheat-sheet*', +] +exclude_patterns.extend(custom_excludes) + +rst_epilog = ''' +.. include:: /reuse/links.txt +''' +if 'custom_rst_epilog' in locals(): + rst_epilog = custom_rst_epilog + +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} + +if not 'conf_py_path' in html_context and 'github_folder' in html_context: + html_context['conf_py_path'] = html_context['github_folder'] + +# For ignoring specific links +linkcheck_anchors_ignore_for_url = [ + r'https://github\.com/.*' +] + +############################################################ +### Styling +############################################################ + +# Find the current builder +builder = 'dirhtml' +if '-b' in sys.argv: + builder = sys.argv[sys.argv.index('-b')+1] + +# Setting templates_path for epub makes the build fail +if builder == 'dirhtml' or builder == 'html': + templates_path = ['.sphinx/_templates'] + +# Theme configuration +html_theme = 'furo' +html_last_updated_fmt = '' +html_permalinks_icon = '¶' +html_theme_options = { + 'light_css_variables': { + 'font-stack': 'Ubuntu, -apple-system, Segoe UI, Roboto, Oxygen, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif', + 'font-stack--monospace': 'Ubuntu Mono, Consolas, Monaco, Courier, monospace', + 'color-foreground-primary': '#111', + 'color-foreground-secondary': 'var(--color-foreground-primary)', + 'color-foreground-muted': '#333', + 'color-background-secondary': '#FFF', + 'color-background-hover': '#f2f2f2', + 'color-brand-primary': '#111', + 'color-brand-content': '#06C', + 'color-api-background': '#cdcdcd', + 'color-inline-code-background': 'rgba(0,0,0,.03)', + 'color-sidebar-link-text': '#111', + 'color-sidebar-item-background--current': '#ebebeb', + 'color-sidebar-item-background--hover': '#f2f2f2', + 'toc-font-size': 'var(--font-size--small)', + 'color-admonition-title-background--note': 'var(--color-background-primary)', + 'color-admonition-title-background--tip': 'var(--color-background-primary)', + 'color-admonition-title-background--important': 'var(--color-background-primary)', + 'color-admonition-title-background--caution': 'var(--color-background-primary)', + 'color-admonition-title--note': '#24598F', + 'color-admonition-title--tip': '#24598F', + 'color-admonition-title--important': '#C7162B', + 'color-admonition-title--caution': '#F99B11', + 'color-highlighted-background': '#EbEbEb', + 'color-link-underline': 'var(--color-background-primary)', + 'color-link-underline--hover': 'var(--color-background-primary)', + 'color-version-popup': '#772953' + }, + 'dark_css_variables': { + 'color-foreground-secondary': 'var(--color-foreground-primary)', + 'color-foreground-muted': '#CDCDCD', + 'color-background-secondary': 'var(--color-background-primary)', + 'color-background-hover': '#666', + 'color-brand-primary': '#fff', + 'color-brand-content': '#06C', + 'color-sidebar-link-text': '#f7f7f7', + 'color-sidebar-item-background--current': '#666', + 'color-sidebar-item-background--hover': '#333', + 'color-admonition-background': 'transparent', + 'color-admonition-title-background--note': 'var(--color-background-primary)', + 'color-admonition-title-background--tip': 'var(--color-background-primary)', + 'color-admonition-title-background--important': 'var(--color-background-primary)', + 'color-admonition-title-background--caution': 'var(--color-background-primary)', + 'color-admonition-title--note': '#24598F', + 'color-admonition-title--tip': '#24598F', + 'color-admonition-title--important': '#C7162B', + 'color-admonition-title--caution': '#F99B11', + 'color-highlighted-background': '#666', + 'color-link-underline': 'var(--color-background-primary)', + 'color-link-underline--hover': 'var(--color-background-primary)', + 'color-version-popup': '#F29879' + }, +} + +############################################################ +### Additional files +############################################################ + +html_static_path = ['.sphinx/_static'] + +html_css_files = [ + 'custom.css', + 'header.css', + 'github_issue_links.css', +] +html_css_files.extend(custom_html_css_files) + +html_js_files = ['header-nav.js'] +if 'github_issues' in html_context and html_context['github_issues'] and not disable_feedback_button: + html_js_files.append('github_issue_links.js') +html_js_files.extend(custom_html_js_files) diff --git a/docs/custom_conf.py b/docs/custom_conf.py new file mode 100644 index 00000000..20bb6e1d --- /dev/null +++ b/docs/custom_conf.py @@ -0,0 +1,126 @@ +import datetime + +# Custom configuration for the Sphinx documentation builder. +# All configuration specific to your project should be done in this file. +# +# The file is included in the common conf.py configuration file. +# You can modify any of the settings below or add any configuration that +# is not covered by the common conf.py file. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +############################################################ +### Project information +############################################################ + +# Product name +project = 'Charmed MLflow' +author = 'Canonical Group Ltd' + +# Uncomment if your product uses release numbers +# release = '1.0' + +# The default value uses the current year as the copyright year +copyright = '%s, %s' % (datetime.date.today().year, author) + +## Open Graph configuration - defines what is displayed in the website preview +# The URL of the documentation output +ogp_site_url = 'https://canonical-starter-pack.readthedocs-hosted.com/' +# The documentation website name (usually the same as the product name) +ogp_site_name = 'mlflow' +# An image or logo that is used in the preview +ogp_image = 'https://assets.ubuntu.com/v1/253da317-image-document-ubuntudocs.svg' + +# Update with the favicon for your product (default is the circle of friends) +html_favicon = '.sphinx/_static/favicon.png' + +# (Some settings must be part of the html_context dictionary, while others +# are on root level. Don't move the settings.) +html_context = { + + # Change to the link to your product website (without "https://") + 'product_page': 'documentation.ubuntu.com', + + # Add your product tag to ".sphinx/_static" and change the path + # here (start with "_static"), default is the circle of friends + 'product_tag': '_static/tag.png', + + # Change to the discourse instance you want to be able to link to + # using the :discourse: metadata at the top of a file + # (use an empty value if you don't want to link) + 'discourse': 'https://discourse.charmhub.io/tag/mlflow', + + # Change to the GitHub info for your project + 'github_url': 'https://github.com/canonical/mlflow-operator', + + # Change to the branch for this version of the documentation + 'github_version': 'main', + + # Change to the folder that contains the documentation + # (usually "/" or "/docs/") + 'github_folder': '/docs/', + + # Change to an empty value if your GitHub repo doesn't have issues enabled. + # This will disable the feedback button and the issue link in the footer. + 'github_issues': 'enabled' +} + +# If your project is on documentation.ubuntu.com, specify the project +# slug (for example, "lxd") here. +slug = "" + +############################################################ +### Redirects +############################################################ + +# Set up redirects (https://documatt.gitlab.io/sphinx-reredirects/usage.html) +# For example: 'explanation/old-name.html': '../how-to/prettify.html', + +redirects = {} + +############################################################ +### Link checker exceptions +############################################################ + +# Links to ignore when checking links + +linkcheck_ignore = [ + 'http://127.0.0.1:8000' + ] + +############################################################ +### Additions to default configuration +############################################################ + +## The following settings are appended to the default configuration. +## Use them to extend the default functionality. + +# Add extensions +custom_extensions = [] + +# Add files or directories that should be excluded from processing. +custom_excludes = [] + +# Add CSS files (located in .sphinx/_static/) +custom_html_css_files = [] + +# Add JavaScript files (located in .sphinx/_static/) +custom_html_js_files = [] + +## The following settings override the default configuration. + +# Specify a reST string that is included at the end of each file. +# If commented out, use the default (which pulls the reuse/links.txt +# file into each reST file). +# custom_rst_epilog = '' + +# By default, the documentation includes a feedback button at the top. +# You can disable it by setting the following configuration to True. +disable_feedback_button = False + +############################################################ +### Additional configuration +############################################################ + +## Add any configuration that is not covered by the common conf.py file. diff --git a/docs/doc-cheat-sheet-myst.md b/docs/doc-cheat-sheet-myst.md new file mode 100644 index 00000000..19b07f53 --- /dev/null +++ b/docs/doc-cheat-sheet-myst.md @@ -0,0 +1,243 @@ +--- +orphan: true +myst: + substitutions: + reuse_key: "This is **included** text." + advanced_reuse_key: "This is a substitution that includes a code block: + ``` + code block + ```" +--- + +(cheat-sheet-myst)= +# Markdown/MyST cheat sheet + +This file contains the syntax for commonly used Markdown and MyST markup. +Open it in your text editor to quickly copy and paste the markup you need. + +Also see the [MyST documentation](https://myst-parser.readthedocs.io/en/latest/index.html) for detailed information, and the [Canonical Documentation Style Guide](https://docs.ubuntu.com/styleguide/en) for general style conventions. + +## H2 heading + +### H3 heading + +#### H4 heading + +##### H5 heading + +## Inline formatting + +- {guilabel}`UI element` +- `code` +- {command}`command` +- {kbd}`Key` +- *Italic* +- **Bold** + +## Code blocks + +Start a code block: + + code: + - example: true + +``` +# Demonstrate a code block +code: + - example: true +``` + +```yaml +# Demonstrate a code block +code: + - example: true +``` + +(_a_section_target)= +## Links + +- [Canonical website](https://canonical.com/) +- https://canonical.com/ +- {ref}`a_section_target` +- {ref}`Link text ` +- {doc}`index` +- {doc}`Link text ` + + +## Navigation + +Use the following syntax:: + + ```{toctree} + :hidden: + + sub-page1 + sub-page2 + ``` + +## Lists + +1. Step 1 + - Item 1 + * Sub-item + - Item 2 + 1. Sub-step 1 + 1. Sub-step 2 +1. Step 2 + 1. Sub-step 1 + - Item + 1. Sub-step 2 + +Term 1 +: Definition + +Term 2 +: Definition + +## Tables + +## Markdown tables + +| Header 1 | Header 2 | +|------------------------------------|----------| +| Cell 1
Second paragraph | Cell 2 | +| Cell 3 | Cell 4 | + +Centred: + +| Header 1 | Header 2 | +|:----------------------------------:|:--------:| +| Cell 1
Second paragraph | Cell 2 | +| Cell 3 | Cell 4 | + +## List tables + +```{list-table} + :header-rows: 1 + +* - Header 1 + - Header 2 +* - Cell 1 + + Second paragraph + - Cell 2 +* - Cell 3 + - Cell 4 +``` + +Centred: + +```{list-table} + :header-rows: 1 + :align: center + +* - Header 1 + - Header 2 +* - Cell 1 + + Second paragraph + - Cell 2 +* - Cell 3 + - Cell 4 +``` + +## Notes + +```{note} +A note. +``` + +```{tip} +A tip. +``` + +```{important} +Important information +``` + +```{caution} +This might damage your hardware! +``` + +## Images + +![Alt text](https://assets.ubuntu.com/v1/b3b72cb2-canonical-logo-166.png) + +```{figure} https://assets.ubuntu.com/v1/b3b72cb2-canonical-logo-166.png + :width: 100px + :alt: Alt text + + Figure caption +``` + +## Reuse + +### Keys + +Keys can be defined at the top of a file, or in a `myst_substitutions` option in `conf.py`. + +{{reuse_key}} + +{{advanced_reuse_key}} + +### File inclusion + +```{include} index.rst + :start-after: include_start + :end-before: include_end +``` + +## Tabs + +````{tabs} +```{group-tab} Tab 1 + +Content Tab 1 +``` + +```{group-tab} Tab 2 +Content Tab 2 +``` +```` + +## Glossary + +```{glossary} + +some term + Definition of the example term. +``` + +{term}`some term` + +## More useful markup + +- ```{versionadded} X.Y +- {abbr}`API (Application Programming Interface)` + +---- + +## Custom extensions + +Related links at the top of the page (surrounded by `---`): + + relatedlinks: https://github.com/canonical/lxd-sphinx-extensions, [RTFM](https://www.google.com) + discourse: 12345 + +Terms that should not be checked by the spelling checker: {spellexception}`PurposelyWrong` + +A terminal view with input and output: + +```{terminal} + :input: command + :user: root + :host: vampyr + +the output +``` + +A link to a YouTube video: + +```{youtube} https://www.youtube.com/watch?v=iMLiK1fX4I0 + :title: Demo +``` diff --git a/docs/doc-cheat-sheet.rst b/docs/doc-cheat-sheet.rst new file mode 100644 index 00000000..b12ebcdf --- /dev/null +++ b/docs/doc-cheat-sheet.rst @@ -0,0 +1,258 @@ +:orphan: + +.. _cheat-sheet: + +reStructuredText cheat sheet +============================ + +This file contains the syntax for commonly used reST markup. +Open it in your text editor to quickly copy and paste the markup you need. + +See the `reStructuredText style guide `_ for detailed information and conventions. + +Also see the `Sphinx reStructuredText Primer `_ for more details on reST, and the `Canonical Documentation Style Guide `_ for general style conventions. + +H2 heading +---------- + +H3 heading +~~~~~~~~~~ + +H4 heading +^^^^^^^^^^ + +H5 heading +.......... + +Inline formatting +----------------- + +- :guilabel:`UI element` +- ``code`` +- :file:`file path` +- :command:`command` +- :kbd:`Key` +- *Italic* +- **Bold** + +Code blocks +----------- + +Start a code block:: + + code: + - example: true + +.. code:: + + # Demonstrate a code block + code: + - example: true + +.. code:: yaml + + # Demonstrate a code block + code: + - example: true + +.. _a_section_target: + +Links +----- + +- `Canonical website `_ +- `Canonical website`_ (defined in ``reuse/links.txt`` or at the bottom of the page) +- https:\ //canonical.com/ +- :ref:`a_section_target` +- :ref:`Link text ` +- :doc:`index` +- :doc:`Link text ` + + +Navigation +---------- + +Use the following syntax:: + + .. toctree:: + :hidden: + + sub-page1 + sub-page2 + + +Lists +----- + +1. Step 1 + + - Item 1 + + * Sub-item + - Item 2 + + i. Sub-step 1 + #. Sub-step 2 +#. Step 2 + + a. Sub-step 1 + + - Item + #. Sub-step 2 + +Term 1: + Definition +Term 2: + Definition + +Tables +------ + ++----------------------+------------+ +| Header 1 | Header 2 | ++======================+============+ +| Cell 1 | Cell 2 | +| | | +| Second paragraph | | ++----------------------+------------+ +| Cell 3 | Cell 4 | ++----------------------+------------+ + +.. list-table:: + :header-rows: 1 + + * - Header 1 + - Header 2 + * - Cell 1 + + Second paragraph + - Cell 2 + * - Cell 3 + - Cell 4 + +.. rst-class:: align-center + + +----------------------+------------+ + | Header 1 | Header 2 | + +======================+============+ + | Cell 1 | Cell 2 | + | | | + | Second paragraph | | + +----------------------+------------+ + | Cell 3 | Cell 4 | + +----------------------+------------+ + +.. list-table:: + :header-rows: 1 + :align: center + + * - Header 1 + - Header 2 + * - Cell 1 + + Second paragraph + - Cell 2 + * - Cell 3 + - Cell 4 + +Notes +----- + +.. note:: + A note. + +.. tip:: + A tip. + +.. important:: + Important information + +.. caution:: + This might damage your hardware! + +Images +------ + +.. image:: https://assets.ubuntu.com/v1/b3b72cb2-canonical-logo-166.png + +.. figure:: https://assets.ubuntu.com/v1/b3b72cb2-canonical-logo-166.png + :width: 100px + :alt: Alt text + + Figure caption + +Reuse +----- + +.. |reuse_key| replace:: This is **included** text. + +|reuse_key| + +.. include:: index.rst + :start-after: include_start + :end-before: include_end + +Tabs +---- + +.. tabs:: + + .. group-tab:: Tab 1 + + Content Tab 1 + + .. group-tab:: Tab 2 + + Content Tab 2 + + +Glossary +-------- + +.. glossary:: + + example term + Definition of the example term. + +:term:`example term` + +More useful markup +------------------ + +- .. versionadded:: X.Y +- | Line 1 + | Line 2 + | Line 3 +- .. This is a comment +- :abbr:`API (Application Programming Interface)` + +---- + +Custom extensions +----------------- + +Related links at the top of the page:: + + :relatedlinks: https://github.com/canonical/lxd-sphinx-extensions, [RTFM](https://www.google.com) + :discourse: 12345 + +Terms that should not be checked by the spelling checker: :spellexception:`PurposelyWrong` + +A terminal view with input and output: + +.. terminal:: + :input: command + :user: root + :host: vampyr + + the output + +A link to a YouTube video: + +.. youtube:: https://www.youtube.com/watch?v=iMLiK1fX4I0 + :title: Demo + + + +.. LINKS +.. _Canonical website: https://canonical.com/ diff --git a/docs/explanation/index.rst b/docs/explanation/index.rst new file mode 100644 index 00000000..9737e91b --- /dev/null +++ b/docs/explanation/index.rst @@ -0,0 +1,4 @@ +Explanation +=========== + +Coming soon. \ No newline at end of file diff --git a/docs/how-to/create-ck8s-aws.rst b/docs/how-to/create-ck8s-aws.rst new file mode 100644 index 00000000..15a710a6 --- /dev/null +++ b/docs/how-to/create-ck8s-aws.rst @@ -0,0 +1,87 @@ +Create an MLOps-ready Charmed Kubernetes cluster +================================================ + +This how-to guide will show you how to create a Charmed Kubernetes (CK8s) cluster with an appropriate configuration for deploying an MLOps platforms such as Kubeflow or MLflow. + +**Prerequisites** + +- A local machine with Ubuntu 22.04 or later. +- An AWS account (`How to create an AWS account `_). + +Install and set up AWS CLI +--------------------------- + +First, `install the AWS CLI `_ on your local machine, and then set it up. You can use any of the authentication methods available for the AWS CLI. For example, you can use `IAM user credentials `_. + +Install other tools +------------------- + +To install some helpful tools, run this command: + +.. code-block:: bash + + sudo snap install juju --classic --channel=2.9/stable + for snap in juju-wait kubectl jq; \ + do sudo snap install $snap --classic; \ + done + +This installs the following: + +* ``juju``: Needed to deploy and manage the CK8s cluster. +* ``juju-wait``: CLI tool used for waiting during Juju deployments. +* ``kubectl``: Kubernetes client used to communicate with a Kubernetes cluster. +* ``jq``: A lightweight and versatile command-line tool for parsing and manipulating JSON data. + + + +Setup Juju with AWS +------------------- + +Set up Juju to communicate with AWS. + +.. code-block:: bash + + juju add-credential aws + +You will be prompted for information related to your AWS account that you provided while setting up the AWS CLI (e.g., access key, secret access key). + +Create Juju controller +---------------------- + +Bootstrap a Juju controller that will be responsible for deploying cluster applications. + +.. code-block:: bash + + juju bootstrap aws kf-controller + +Deploy Charmed Kubernetes 1.24 +------------------------------ + +Clone the `Charmed Kubernetes bundle repository `_, and update CPU, disk, and memory constraints to meet Kubeflow requirements. + +.. code-block:: bash + + git clone https://github.com/charmed-kubernetes/bundle + sed -i '/^ *charm: kubernetes-worker/,/^ *[^:]*:/s/constraints: cores=2 mem=8G root-disk=16G/constraints: cores=8 mem=32G root-disk=200G/' ./bundle/releases/1.24/bundle.yaml + +Deploy the Charmed Kubernetes bundle on AWS with the storage overlay. This overlay enables you to create Kubernetes volumes backed by AWS EBS. + +.. code-block:: bash + + juju deploy ./bundle/releases/1.24/bundle.yaml \ + --overlay ./bundle/overlays/aws-storage-overlay.yaml --trust + +Wait until all components are ready. + +.. code-block:: bash + + juju-wait -m default -t 3600 + +Retrieve the Kubernetes configuration from the control plane leader unit. + +.. code-block:: bash + + mkdir ~/.kube + juju ssh kubernetes-control-plane/leader -- cat config > ~/.kube/config + +Now you can use ``kubectl`` to talk to your newly created Charmed Kubernetes cluster. \ No newline at end of file diff --git a/docs/how-to/deploy-ck8s-aws.rst b/docs/how-to/deploy-ck8s-aws.rst new file mode 100644 index 00000000..eba1aed5 --- /dev/null +++ b/docs/how-to/deploy-ck8s-aws.rst @@ -0,0 +1,70 @@ +Deploy Charmed MLflow to Charmed Kubernetes on AWS +======================================================== + ++-----------+---------+ +| Component | Version | ++-----------+---------+ +| MLflow | 2 | ++-----------+---------+ + +This guide shows how to connect Juju to an existing `Charmed Kubernetes `_ (CK8s) cluster and deploy the MLflow bundle on top of it. + +Prerequisites +------------- + +We assume that you have access to a CK8s cluster using ``kubectl``. If you don't have a cluster set up, you can follow this guide: :doc:`Create CK8s on AWS `. + +Install Juju +------------ + +Install Juju: + +.. code-block:: bash + + sudo snap install juju --classic --channel=2.9/stable + +Connect Juju to Charmed Kubernetes cluster +------------------------------------------ + +Configure Juju to communicate with the CK8s cluster by creating a controller: + +.. code-block:: bash + + juju add-k8s charmed-k8s-aws --controller $(juju switch | cut -d: -f1) \ + --storage=cdk-ebs + +Create a model. The model name is up to you. However, if you plan to connect MLflow with Kubeflow you must use ``kubeflow`` as the model name. + +.. code-block:: bash + + juju add-model kubeflow charmed-k8s-aws + +Deploy MLflow bundle +-------------------- + +Deploy the MLflow bundle: + +.. code-block:: bash + + juju deploy mlflow --channel=2.1/stable --trust + +Wait until the deployments are active: + +.. code-block:: bash + + juju-wait -m kubeflow -t 2700 + +Connect to MLflow dashboard +--------------------------- + +By default, the MLflow UI is exposed as a NodePort Kubernetes service, accessible at each node's IP address. MLflow runs on port 31380 by default. AWS nodes are EC2 instances. To connect to an instance, it must be configured to allow traffic to this port. + +You can connect to any EC2 instance in the cluster. List all available nodes in your Kubernetes cluster and choose any ``EXTERNAL-IP`` that you will use to access the MLflow UI: + +.. code-block:: bash + + kubectl get nodes -o wide + +In your AWS account find the EC2 instance with that particular ``EXTERNAL-IP`` and enable access to the port 31380 in the inbound rules of the security group. To see how, consult `AWS docs `_. + +Open a web browser and visit ``:31380`` to access the MLflow UI. diff --git a/docs/how-to/deploy-eks.rst b/docs/how-to/deploy-eks.rst new file mode 100644 index 00000000..80a2ddd4 --- /dev/null +++ b/docs/how-to/deploy-eks.rst @@ -0,0 +1,68 @@ +Deploy Charmed MLflow to EKS +============================ + ++-----------+---------+ +| Component | Version | ++-----------+---------+ +| MLflow | 2 | ++-----------+---------+ + +This guide shows how to deploy Charmed MLflow on `AWS Elastic Kubernetes Service `_ (EKS). In this guide, we will create an AWS EKS cluster, connect Juju to it, and deploy the MLflow bundle. + +Prerequisites: +-------------- +We assume the following: + +- Your machine runs Ubuntu 22.04 or later +- You have an AWS account (`How to create an AWS account `_) + +Create EKS cluster +------------------- +See the `EKS creation guide `_ for how to do that. + +Setup Juju +---------- + +Set up your local ``juju`` to talk to the remote Kubernetes (K8s) cloud. First, install ``juju``: + +.. code-block:: bash + + sudo snap install juju --classic + +Connect Juju to Kubernetes: + +.. code-block:: bash + + juju add-k8s kubeflow + +.. note:: You must choose the name ``kubeflow`` if you plan to connect MLflow to Kubeflow. Otherwise you can choose any name. + +Create a controller: + +.. code-block:: bash + + juju bootstrap --no-gui kubeflow kubeflow-controller + +.. note:: You can use whatever controller name you like here, we chose ``kubeflow-controller``. + +Add a Juju model: + +.. code-block:: bash + + juju add-model kubeflow + +.. note:: You must choose the name ``kubeflow`` if you plan to connect MLflow to Kubeflow. Otherwise you can choose any name. + +Deploy MLflow bundle +--------------------- +Deploy the MLflow bundle with the following command: + +.. code-block:: bash + + juju deploy mlflow --channel=2.1/stable --trust + +Wait until all charms are in the active state. You can check the state of the charms with the command: + +.. code-block:: bash + + juju status --watch 5s --relations diff --git a/docs/how-to/deploy-mlflow-kubeflow-eks.rst b/docs/how-to/deploy-mlflow-kubeflow-eks.rst new file mode 100644 index 00000000..bf280e40 --- /dev/null +++ b/docs/how-to/deploy-mlflow-kubeflow-eks.rst @@ -0,0 +1,130 @@ +Deploy Charmed MLflow and Kubeflow to EKS +========================================= + ++-----------+---------+ +| Component | Version | ++-----------+---------+ +| MLflow | 2 | ++-----------+---------+ + +This guide shows how to deploy Charmed MLflow alongside Kubeflow on `AWS Elastic Kubernetes Service `_ (EKS). In this guide, we will create an AWS EKS cluster, connect Juju to it, deploy the MLflow and Kubeflow bundles, and relate them to each other. + +Prerequisites +------------- + +We assume the following: + +- Your machine runs Ubuntu 22.04 or later +- You have an AWS account (`How to create an AWS account `_) + +Deploy EKS cluster +------------------- + +See our `EKS creation guide `_ for a complete guide on how to do this. **Do not forget** to edit the ``instanceType`` field under ``managedNodeGroups[0].instanceType`` from ``t2.2xlarge`` to ``t3.2xlarge``, as instructed in the guide, since worker nodes of type ``t3.2xlarge`` are required for deploying both MLflow and Kubeflow. + +Setup Juju +---------- + +Set up your local ``juju`` to talk to the remote Kubernetes cloud. First, install Juju with: + +.. code-block:: bash + + sudo snap install juju --classic + +Connect it to Kubernetes: + +.. code-block:: bash + + juju add-k8s kubeflow + +Create the controller: + +.. code-block:: bash + + juju bootstrap --no-gui kubeflow kubeflow-controller + +.. note:: we chose the name ``kubeflow-controller``, but you can choose any other name. + +Add a Juju model: + +.. code-block:: bash + + juju add-model kubeflow + +Deploy MLflow bundle +--------------------- + +Deploy the MLflow bundle with the following command: + +.. code-block:: bash + + juju deploy mlflow --channel=2.1/stable --trust + +Wait until all charms are in the active state. You can check the state of the charms with the command: + +.. code-block:: bash + + juju status --watch 5s --relations + +Deploy Kubeflow bundle +---------------------- + +Deploy the Kubeflow bundle with the following command: + +.. code-block:: bash + + juju deploy kubeflow --channel=1.7/stable --trust + +Wait until all charms are in the active state. You can check the state of the charms with the command: + +.. code-block:: bash + + juju status --watch 5s --relations + +Relate MLflow to Kubeflow +------------------------- + +The resource dispatcher is used to connect MLflow with Kubeflow. In particular, it is responsible for configuring MLflow related Kubernetes objects for Kubeflow user namespaces. Deploy the resource dispatcher to the cluster with the command: + +.. code-block:: bash + + juju deploy resource-dispatcher --channel 1.0/stable --trust + +Relate the resource dispatcher to MLflow with the following commands: + +.. code-block:: bash + + juju relate mlflow-server:secrets resource-dispatcher:secrets + juju relate mlflow-server:pod-defaults resource-dispatcher:pod-defaults + +Wait until all charms are in the active state. You can check the state of the charms with the command: + +.. code-block:: bash + + juju status --watch 5s --relations + +Configure Kubeflow dashboard +---------------------------- + +Get the hostname from the ``istio-ingressgateway-workload`` Kubernetes load balancer service: + +.. code-block:: bash + + export INGRESS_HOST=$(kubectl get svc -n kubeflow istio-ingressgateway-workload -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') + +Then, configure OIDC and DEX with the ``INGRESS_HOST`` we just retrieved, and also a username and password of your choosing: + +.. code-block:: bash + + juju config dex-auth public-url="http://${INGRESS_HOST}" + juju config oidc-gatekeeper public-url="http://${INGRESS_HOST}" + juju config dex-auth static-password=user123 + juju config dex-auth static-username=user123@email.com + +Wait until all charms are in the active state. You can check the state of the charms with the command: + +.. code-block:: bash + + juju status --watch 5s --relations + +Now you can access the Kubeflow dashboard at the value from ``INGRESS_HOST`` in your browser. \ No newline at end of file diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst new file mode 100644 index 00000000..1dd506bb --- /dev/null +++ b/docs/how-to/index.rst @@ -0,0 +1,39 @@ +How-to guides +============= + +These guides provide practical instructions for specific tasks related to deploying, managing and using MLflow. + +Preparation +----------- + +.. toctree:: + :maxdepth: 1 + + create-ck8s-aws + +Deployment +---------- + +.. toctree:: + :maxdepth: 1 + + deploy-ck8s-aws + deploy-eks + deploy-mlflow-kubeflow-eks + +Integration +----------- + +.. toctree:: + :maxdepth: 1 + + integrate-cos + integrate-ml-ckf-ck8s + +Upgrading +--------- + +.. toctree:: + :maxdepth: 1 + + migrate-v1-v2 \ No newline at end of file diff --git a/docs/how-to/integrate-cos.rst b/docs/how-to/integrate-cos.rst new file mode 100644 index 00000000..5c9acdaf --- /dev/null +++ b/docs/how-to/integrate-cos.rst @@ -0,0 +1,88 @@ +Integrate MLflow with the Canonical Observability Stack (COS) +============================================================= + +This guide shows how to integrate MLflow with the Canonical Observability Stack (COS). + +Prerequisites +------------- + +This guide assumes: + +#. You have deployed the COS stack in the ``cos`` model. For steps on how to do this, see the `MicroK8s tutorial `_. +#. You have deployed the MLflow bundle in the ``kubeflow`` model. For steps on how to do this, see :doc:`../tutorial/mlflow`. + +Deploy Grafana Agent +-------------------- + +Deploy the `Grafana Agent `_ to your ``kubeflow`` model alongside the MLflow bundle. Run the following command: + +.. code-block:: bash + + juju deploy grafana-agent-k8s --channel=edge --trust + +Relate MLflow Server Prometheus Metrics to Grafana Agent +-------------------------------------------------------- + +Establish the relationship between the MLflow Server Prometheus metrics and the Grafana Agent. Use the following command: + +.. code-block:: bash + + juju add-relation mlflow-server:metrics-endpoint grafana-agent-k8s:metrics-endpoint + +Relate Grafana Agent to Prometheus in the COS Model +--------------------------------------------------- + +Next, relate the Grafana Agent to Prometheus in the ``cos`` model. Execute the following command: + +.. code-block:: bash + + juju add-relation grafana-agent-k8s admin/cos.prometheus-receive-remote-write + +Relate MLflow Server in the Kubeflow Model to Grafana Charm in the COS Model +---------------------------------------------------------------------------- + +Establish the relationship between the MLflow Server in the ``kubeflow`` model and the Grafana charm in the ``cos`` model. Run the following command: + +.. code-block:: bash + + juju add-relation mlflow-server admin/cos.grafana-dashboards + +Obtain the Grafana Dashboard Admin Password +------------------------------------------- + +Switch the model to ``cos`` and retrieve the Grafana dashboard admin password. Execute the following commands: + +.. code-block:: bash + + juju switch cos + juju run-action grafana/0 get-admin-password --wait + +Obtain the Grafana Dashboard URL +-------------------------------- + +To access the Grafana dashboard, you need the URL. Run the following command to get the URLs for the COS endpoints: + +.. code-block:: bash + + juju show-unit catalogue/0 | grep url + +You will see a list of endpoints similar to the following: + +.. code-block:: bash + + url: http://10.43.8.34:80/cos-catalogue + url: http://10.43.8.34/cos-grafana + url: http://10.43.8.34:80/cos-prometheus-0 + url: http://10.43.8.34:80/cos-alertmanager + +Choose the ``cos-grafana`` URL and access it in your browser. + +Login to Grafana +---------------- + +Login to Grafana with the password obtained from the previous section. The username is ``admin``. + +Access the dashboard in the UI +------------------------------ + +Go to the left sidebar and choose the MLflow Dashboards from the list. From the General dashboards folder choose the ``MLflow metrics Dashboard``. When accessing the dashboard for the first time, choose some reasonable time range from the top right dropdown. diff --git a/docs/how-to/integrate-ml-ckf-ck8s.rst b/docs/how-to/integrate-ml-ckf-ck8s.rst new file mode 100644 index 00000000..7d2ed589 --- /dev/null +++ b/docs/how-to/integrate-ml-ckf-ck8s.rst @@ -0,0 +1,42 @@ +Integrate Charmed MLflow with Charmed Kubeflow on Charmed Kubernetes +========================================================================= + ++-----------+---------+ +| Component | Version | ++-----------+---------+ +| MLflow | 2 | ++-----------+---------+ + +In this guide, we will guide you through the process of integrating Charmed MLflow with Charmed Kubeflow on `Charmed Kubernetes `_. + +Prerequisites +-------------- +We assume that: + +* You have access to a Charmed Kubernetes cluster using ``kubectl``. If you don't have a cluster set up, you can follow the :doc:`creation guide ` to deploy one on AWS. +* You have deployed the Charmed Kubeflow bundle. If you don't have it, here is `a guide `_ on how to do it. +* You have deployed the Charmed MLflow bundle. To see how, follow our :doc:`deployment guide `. + +Deploy resource dispatcher +-------------------------- + +Deploy the resource dispatcher: + +.. code-block:: bash + + juju deploy resource-dispatcher --channel 1.0/stable --trust + +Relate Resource dispatcher to MLflow +------------------------------------ + +Relate the Resource dispatcher to MLflow: + +.. code-block:: bash + + juju relate mlflow-server:secrets resource-dispatcher:secrets + juju relate mlflow-server:pod-defaults resource-dispatcher:pod-defaults + +Integrate MLflow with Kubeflow notebook +--------------------------------------- + +Please refer to this doc: :doc:`../tutorial/mlflow-kubeflow`. \ No newline at end of file diff --git a/docs/how-to/migrate-v1-v2.rst b/docs/how-to/migrate-v1-v2.rst new file mode 100644 index 00000000..ed0bc17a --- /dev/null +++ b/docs/how-to/migrate-v1-v2.rst @@ -0,0 +1,131 @@ +Migrate Charmed MLflow Version 1 to Version 2 +===================================================== + +This guide shows how to migrate Charmed MLflow version 1 to version 2. This guide assumes you are running the old Charmed MLflow stack version 1, which runs with MariaDB. With MLflow version 2, we only support the MySQL integration. This guide outlines how to move data from MariaDB to MySQL and how to migrate data from version 1 to version 2.1.1. Data from the object store doesn't need to be migrated. + +Prerequisites +------------- + +This guide assumes the following: + +#. You have deployed MLflow version 1 with MariaDB, MLflow server version 1.x, and MinIO. +#. You have CLI access to the machine where the Juju controller is deployed (all commands will be executed from there). + +MariaDB Backup +-------------- + +Install the ``mysqldump`` command: + +.. code-block:: bash + + sudo apt update + sudo apt install mysql-client + +Backup the MariaDB database with the following command: + +.. code-block:: bash + + mysqldump --host= --user=root --password=root --column-statistics=0 --databases database > mlflow-db.sql + +Deploy MySQL Charm +------------------- + +Deploy the MySQL charm, which is needed for MLflow v2: + +.. code-block:: bash + + juju deploy mysql-k8s --channel 8.0/beta --series jammy --trust + +.. note:: For MLflow version ``v.2.1``, we deploy the 8.0/beta version of the charm. You may deploy a more up to date version in your case. + +Please wait until the charm goes to active in ``juju status``. Then run the following command to get the password for MySQL: + +.. code-block:: bash + + juju run-action mysql-k8s/0 get-password --wait + +Adjust the Database Backup +-------------------------- + +Rename the database from ``database`` (used in MariaDB) to ``mlflow`` (used in MySQL): + +.. code-block:: bash + + sed 's/`database`/`mlflow`/g' mlflow-db.sql > mlflow-db-updated.sql + +Rename any duplicate constraints as MySQL does not allow that. In practice, the only duplicate constraint we've encountered is ``CONSTRAINT_1``. It has two occurrences. The first occurrence can be renamed to ``CONSTRAINT-1``, for example: + +.. code-block:: bash + + sed -i '0,/`CONSTRAINT_1`/s//`CONSTRAINT-1`/' mlflow-db-updated.sql + +You can do all the above modifications in the text editor of your choice if you prefer. + +Move Database to MySQL +---------------------- + +Install the MySQL CLI tool: + +.. code-block:: bash + + sudo apt update + sudo apt-get install mysql-shell + +Connect to the MySQL charm: + +.. code-block:: bash + + mysql --user=root --host= -p + # you will be prompted for password + +Create the MySQL database called ``mlflow``: + +.. code-block:: bash + + CREATE DATABASE mlflow; + +Leave the client with ``ctrl + D``. + +Move the updated database dump file to MySQL: + +.. code-block:: bash + + mysql -u root -p mlflow @/mlflow + +Update MLflow Server +--------------------- + +Remove relations from the old MLflow server: + +.. code-block:: bash + + juju remove-relation mlflow-db:mysql mlflow-server:db + juju remove-relation minio mlflow-server + +Update the MLflow server: + +.. code-block:: bash + + juju refresh mlflow-server --channel 2.1/edge + +Create relations with MinIO and MySQL: + +.. code-block:: bash + + juju relate mysql-k8s mlflow-server + juju relate minio mlflow-server diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..f35b2535 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,62 @@ +:relatedlinks: [Diátaxis](https://diataxis.fr/) + +.. _home: + +Charmed MLflow Documentation +============================ + +Charmed MLflow is a platform for managing the end-to-end machine learning lifecycle. + +It provides tools for tracking experiments, packaging code into reproducible runs, and sharing and deploying models. It integrates with popular machine learning frameworks. + +It addresses a number of common machine learning challenges: collaboration, reproducibility, maintenance, organisation and scaling. + +It's ideal for data scientists, ML engineers, hobbyists and teams looking to optimise their ML workflows with charms. + +--------- + +In this documentation +--------------------- + +.. grid:: 1 1 2 2 + + .. grid-item:: :doc:`Tutorial ` + + **Start here**: a hands-on introduction to Charmed MLflow for newcomers + + .. grid-item:: :doc:`How-to guides ` + + **Step-by-step guides** covering key operations and common tasks in Charmed MLflow + +.. grid:: 1 1 2 2 + :reverse: + + .. grid-item:: :doc:`Reference ` + + **Technical information** - specifications, APIs, architecture of Charmed MLflow + + .. grid-item:: :doc:`Explanation ` + + **Discussion and clarification** of key Charmed MLflow concepts and features + +--------- + +Project and community +--------------------- + +Charmed MLflow is an open-source project that values its community. We warmly welcome contributions, suggestions, fixes, and constructive feedback from everyone. + +* `Code of conduct`_ +* `Contribute`_ +* `Join our online chat`_ +* `Upstream Project`_ +* `Discourse Forum`_ + +.. toctree:: + :hidden: + :maxdepth: 2 + + tutorial/index + how-to/index + reference/index + explanation/index \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..32bb2452 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/reference/index.rst b/docs/reference/index.rst new file mode 100644 index 00000000..27ebe866 --- /dev/null +++ b/docs/reference/index.rst @@ -0,0 +1,4 @@ +Reference +========= + +Coming soon. \ No newline at end of file diff --git a/docs/reuse/links.txt b/docs/reuse/links.txt new file mode 100644 index 00000000..10e483ae --- /dev/null +++ b/docs/reuse/links.txt @@ -0,0 +1,10 @@ +.. _reStructuredText style guide: https://canonical-documentation-with-sphinx-and-readthedocscom.readthedocs-hosted.com/style-guide/ +.. _Read the Docs at Canonical: https://library.canonical.com/documentation/read-the-docs +.. _How to publish documentation on Read the Docs: https://library.canonical.com/documentation/publish-on-read-the-docs +.. _Example product documentation: https://canonical-example-product-documentation.readthedocs-hosted.com/ + +.. _Code of conduct: https://ubuntu.com/community/ethos/code-of-conduct +.. _Join our online chat: https://chat.charmhub.io/charmhub/channels/charmed-mlops +.. _Contribute: https://github.com/canonical/mlflow-operator/blob/main/CONTRIBUTING.md +.. _Upstream Project: https://mlflow.org/ +.. _Discourse Forum: https://discourse.charmhub.io/tag/mlflow \ No newline at end of file diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst new file mode 100644 index 00000000..cf9a0654 --- /dev/null +++ b/docs/tutorial/index.rst @@ -0,0 +1,18 @@ +Tutorial +======== + +Step-by-step guides to help you get started with deploying and managing machine learning workflows using Charmed MLflow. + +We provide two pathways. For users who just want to try MLflow: + +.. toctree:: + :maxdepth: 1 + + mlflow + +For users who want to try MLflow and Kubeflow together: + +.. toctree:: + :maxdepth: 1 + + mlflow-kubeflow \ No newline at end of file diff --git a/docs/tutorial/mlflow-kubeflow.rst b/docs/tutorial/mlflow-kubeflow.rst new file mode 100644 index 00000000..b7c9b4c0 --- /dev/null +++ b/docs/tutorial/mlflow-kubeflow.rst @@ -0,0 +1,185 @@ +Getting Started with Charmed MLflow and Kubeflow +================================================ + ++-----------+---------+ +| Component | Version | ++-----------+---------+ +| MLflow | 2 | ++-----------+---------+ +| Kubeflow | 1.7 | ++-----------+---------+ + +Welcome to this tutorial on getting started with Charmed MLflow alongside Charmed Kubeflow. If you would like to deploy Kubeflow by itself, see our tutorial on `Charmed Kubeflow `_. + +Prerequisites +------------- + +This tutorial assumes you will be deploying Kubeflow and MLflow on a public cloud Virtual Machine (VM) with the following specs: + +- Runs Ubuntu 20.04 (focal) or later. +- Has at least 4 cores, 32GB RAM and 100GB of disk space available. +- Is connected to the internet for downloading the required snaps and charms. + +We'll also assume that you have a laptop that meets the following conditions: + +- Has an SSH tunnel open to the VM with port forwarding and a SOCKS proxy. To see how to set this up, see `How to setup SSH VM Access `_. +- Runs Ubuntu 20.04 (focal) or later. +- Has a web browser installed e.g. Chrome / Firefox / Edge. + +In the remainder of this tutorial, unless otherwise stated, it is assumed you will be running all command line operations on the VM, through the open SSH tunnel. It's also assumed you'll be using the web browser on your local machine to access the Kubeflow and MLflow dashboards. + +Deploy MLflow +------------- + +Follow the steps in this tutorial to deploy MLflow on your VM: :doc:`mlflow`. Before moving on with this tutorial, confirm that you can now access the MLflow UI on ``http://localhost:31380``. + +Deploy Kubeflow bundle +---------------------- + +Let's deploy Charmed Kubeflow alongside MLflow. Run the following command to initiate the deployment: + +.. code-block:: bash + + juju deploy kubeflow --trust --channel=1.7/stable + +Configure Dashboard Access +-------------------------- + +Run the following commands: + +.. code-block:: bash + + juju config dex-auth public-url=http://10.64.140.43.nip.io + juju config oidc-gatekeeper public-url=http://10.64.140.43.nip.io + +This tells the authentication and authorisation components of the bundle that users who access the bundle will be doing so via the URL ``http://10.64.140.43.nip.io``. In turn, this allows those components to construct appropriate responses to incoming traffic. + +Now set the dashboard username and password: + +.. code-block:: bash + + juju config dex-auth static-username=user123@email.com + juju config dex-auth static-password=user123 + +Deploy Resource Dispatcher +-------------------------- + +Next, let's deploy the resource dispatcher. The resource dispatcher is an optional component which will distribute Kubernetes objects related to MLflow credentials to all user namespaces in Kubeflow. This means that all your Kubeflow users can access the MLflow model registry from their namespaces. To deploy the dispatcher, run the following command: + +.. code-block:: bash + + juju deploy resource-dispatcher --channel 1.0/stable --trust + +This will deploy the latest edge version of the dispatcher. See `Resource Dispatcher on GitHub `_ for more info. Now we must relate the dispatcher to MLflow: + +.. code-block:: bash + + juju relate mlflow-server:secrets resource-dispatcher:secrets + juju relate mlflow-server:pod-defaults resource-dispatcher:pod-defaults + +Monitor The Deployment +---------------------- + +Now, at this point, we've deployed MLflow and Kubeflow and we've related them via the resource dispatcher. But that doesn't mean our system is ready yet: Juju will need to download charm data from CharmHub and the charms themselves will take some time to initialise. + +So, how do you know when all the charms are ready, then? You can do this using the ``juju status`` command. First, let's run a basic status command and review the output. Run the following command to print out the status of all the components of Juju: + +.. code-block:: bash + + juju status + +Review the output for yourself. You should see some summary information, a list of Apps and associated information, and another list of Units and their associated information. Don't worry too much about what this all means for now. If you're interested in learning more about this command and its output, see the `Juju Status command `_. + +The main thing we're interested in at this stage is the statuses of all the applications and units running through Juju. We want all the statuses to eventually become ``active``, indicating that the bundle is ready. Run the following command to keep a watch on the components which are not active yet: + +.. code-block:: bash + + watch -c 'juju status --color | grep -E "blocked|error|maintenance|waiting|App|Unit"' + +This will periodically run a ``juju status`` command and filter to components which are in a state of ``blocked``, ``error``, ``maintenance`` or ``waiting`` i.e. not ``active``. When this output becomes empty except for the “App” and “Unit” headings, then we know all statuses are active and our system is ready. + +Don't be surprised if some of the components' statuses change to ``blocked`` or ``error`` every now and then. This is expected behaviour, and these statuses should resolve by themselves as the bundle configures itself. However, if components remain stuck in the same error states, consult the troubleshooting steps below. + +.. dropdown:: Expand to troubleshoot: Waiting for gateway relation + + An issue you might have is the ``tensorboard-controller`` component might be stuck with a status of ``waiting`` and a message “Waiting for gateway relation”. To fix this, run: + + .. code-block:: bash + + juju run --unit istio-pilot/0 -- "export JUJU_DISPATCH_PATH=hooks/config-changed; ./dispatch" + + This is a known issue, see `TensorBoard controller GitHub issue `_ for more info. + +Be patient, it can take up to an hour for all those charms to download and initialise. In the meantime, why not try our `Juju tutorial `_? + +Integrate MLflow with Notebook +------------------------------ + +In this section, we're going to create a notebook server in Kubeflow and connect it to MLflow. This will allow our notebook logic to talk to MLflow in the background. Let's get started. + +First, to be able to use MLflow credentials in your Kubeflow notebook, visit the dashboard at ``http://10.64.140.43.nip.io/`` and fill the username and password which you configured in the previous section e.g. ``user123@email.com`` and ``user123``. + +Click on start setup to setup the Kubeflow user for the first time. + +Select ``Finish`` to finish the process. + +Now a Kubernetes namespace was created for your user. To use MLflow with this user, label the namespace with the following command: + +.. code-block:: bash + + microk8s kubectl label ns user123 user.kubeflow.org/enabled="true" + +You will get the following output: ``namespace/user123 labeled``. + +For more info on the label command, check `Kubernetes labels `_. For more info on Kubernetes namespaces for users, see the `upstream docs on Multi-user isolation `_. + +Now go back to the Dashboard. From the left panel, choose notebooks. Select +New Notebook. + +At this point, we can name the notebook as we want, and choose the desired image and resource limits. For now, let's just keep things simple: + +1. For ``Name``, enter ``test-notebook``. +2. Expand the *Custom Notebook* section and for ``image``, select ``kubeflownotebookswg/jupyter-tensorflow-full:v1.7.0``. + +Now, in order to allow our notebook server access to MLflow, we need to enable some special configuration options. Scroll down to ``Data Volumes -> Advanced options`` and from the ``Configurations`` dropdown, choose the following options: + +1. Allow access to Kubeflow pipelines. +2. Allow access to MinIO. +3. Allow access to MLflow. + +.. note:: Remember we related Kubeflow to MLflow earlier using the resource dispatcher? This is why we're seeing the MinIO and MLflow options in the dropdown! + +Great, that's all the configuration for the notebook server done. Hit the Launch button to launch the notebook server. Be patient, the notebook server will take a little while to initialise. + +When the notebook server is ready, you'll see it listed in the Notebooks table with a success status. At this point, select ``Connect`` to connect to the notebook server. + +When you connect to the notebook server, you'll be taken to the notebook environment in a new tab. Because of our earlier configurations, this environment is now connected to MLflow in the background. This means the notebooks we create here can access MLflow. Cool! + +To test this, create a new notebook and paste the following command into it, in a cell: + +.. code-block:: bash + + !printenv | grep MLFLOW + +Run the cell. This will print out two environment variables ``MLFLOW_S3_ENDPOINT_URL`` and ``MLFLOW_TRACKING_URI``, confirming MLflow is indeed connected. + +Great, we've launched a notebook server that's connected to MLflow! Now let's upload some example notebooks to this server to see MLflow in practice. + +Run MLflow examples +------------------- + +To run MLflow examples on your newly created notebook server, click on the source control icon in the leftmost navigation bar. + +From the menu, choose the ``Clone a Repository`` option. + +Now insert this repository address ``https://github.com/canonical/kubeflow-examples.git``. + +This will clone a whole ``kubeflow-examples`` repository onto the notebook server. The cloned repository will be a folder on the server, with the same name as the remote repository. Go inside the folder and after that, choose the ``mlflow-v2-examples`` sub-folder. + +There you will find two notebooks: + +- ``notebook-example.ipynb``: demonstrates how to talk to MLflow from inside a notebook. The example uses a simple classifier which is stored in the MLflow registry. +- ``pipeline-example.ipynb``: demonstrates how to talk to MLflow from a Kubeflow pipeline. The example creates and executes a three-step Kubeflow pipeline with the last step writing a model object to the MLflow registry. + +Go ahead, try those notebooks out for yourself! You can run them cell by cell using the run button, or all at once using the double chevron `>>`. + +.. note:: If you get an error in the Notebooks related to ``sklearn``, try replacing ``sklearn`` with ``scikit-learn``. See `here `_ for more details. diff --git a/docs/tutorial/mlflow.rst b/docs/tutorial/mlflow.rst new file mode 100644 index 00000000..2f48c588 --- /dev/null +++ b/docs/tutorial/mlflow.rst @@ -0,0 +1,145 @@ +Get Started with Charmed MLflow +================================== + ++-----------+---------+ +| Component | Version | ++-----------+---------+ +| MLflow | 2 | ++-----------+---------+ + +Welcome to the tutorial on Charmed MLflow! `MLflow `_ is an open-source platform, used for managing machine learning workflows. It has four primary functions that include experiment tracking, model registry, model management and code reproducibility. + +So wait, what does "Charmed MLflow" mean? Is it the same thing as MLflow? Yes and no. MLflow is a complex application, consisting of many components running together and communicating with each other. Charmed MLflow is a `charm bundle `_ that allows us to deploy MLflow quickly and easily. Don't worry too much about what a "charm bundle" is right now. The key thing is that it's going to make deploying MLflow very convenient for us: we'll get MLflow up and running with just a few command line commands! + +In this tutorial, we're going to explore Charmed MLflow in a practical way. Using the `Juju `_ CLI tool, we'll deploy MLflow to a local `MicroK8s `_ cloud. + +Prerequisites +------------- + +We are assuming that you are running this tutorial on a local machine with the following specs: + +* Runs Ubuntu 22.04 or later +* Has at least 50GB free disk space + +Install and prepare MicroK8s +---------------------------- +Let's install MicroK8s. MicroK8s is installed from a snap package. The published snap maintains different ``channels`` for different releases of Kubernetes. + +.. code-block:: bash + + sudo snap install microk8s --classic --channel=1.24/stable + +For MicroK8s to work without having to use ``sudo`` for every command, it creates a group called ``microk8s``. To make it more convenient to run commands, you will add the current user to this group: + +.. code-block:: bash + + sudo usermod -a -G microk8s $USER + newgrp microk8s + +It is also useful to make sure the user has the proper access and ownership of any ``kubectl`` configuration files: + +.. code-block:: bash + + sudo chown -f -R $USER ~/.kube + +Enable the following MicroK8s addons to configure your Kubernetes cluster with extra services needed to run Charmed Kubeflow. + +.. code-block:: bash + + microk8s enable dns hostpath-storage ingress metallb:10.64.140.43-10.64.140.49 + +Here, we added a ``dns`` service, so the applications can find each other, storage, an ingress controller so we can access Kubeflow components and the ``MetalLB`` load-balancer application. +You can see that we added some detail when enabling ``MetalLB``, in this case the address pool to use. + +> See More : `MicroK8s | How to use addons `_ + +We've now installed and configured MicroK8s. It will start running automatically, but can take 5 minutes or so before it's ready for action. Run the following command to tell MicroK8s to report its status to us when it's ready: + +.. code-block:: bash + + microk8s status --wait-ready + +Be patient - this command may not return straight away. The ``--wait-ready`` flag tells MicroK8s to wait for the Kubernetes services to initialise before returning. Once MicroK8s is ready, you will see something like the following output: + +.. code-block:: bash + + microk8s is running + +Below this there will be a bunch of other information about the cluster. + +Great, we have now installed and configured MicroK8s, and it's running and ready! + +Install Juju +------------ +`Juju `_ is an operation Lifecycle manager (OLM) for clouds, bare metal or Kubernetes. We will be using it to deploy and manage the components which make up Kubeflow. + +To install Juju from snap, run this command: + +.. code-block:: bash + + sudo snap install juju --classic --channel=2.9/stable + +Now, run the following command to deploy a Juju controller to the Kubernetes we set up with MicroK8s: + +.. code-block:: bash + + juju bootstrap microk8s + +Sit tight while the command completes! The controller may take a minute or two to deploy. + +The controller is the agent of Juju, running on Kubernetes, which can be used to deploy and control the components of Kubeflow. + +Next, we'll need to add a model for Kubeflow to the controller. Run the following command to add a model called ``kubeflow``: + +.. code-block:: bash + + juju add-model kubeflow + +.. note:: The model name here can be anything. We're just using ``kubeflow`` because often you may want to deploy MLflow along with Kubeflow, and in that case, the model name must be ``kubeflow``. So it's not a bad habit to have. + +The controller can work with different ``models``, which map 1:1 to namespaces in Kubernetes. In this case, the model name must be ``kubeflow``, due to an assumption made in the upstream Kubeflow Dashboard code. + +Great job: Juju has now been installed and configured for Kubeflow! + +Deploy MLflow bundle +-------------------- +Before deploying, run these commands: + +.. code-block:: bash + + sudo sysctl fs.inotify.max_user_instances=1280 + sudo sysctl fs.inotify.max_user_watches=655360 + +We need to run the above because under the hood, MicroK8s uses ``inotify`` to interact with the filesystem, and in large MicroK8s deployments sometimes the default ``inotify`` limits are exceeded. + +Let's now use Juju to deploy Charmed MLflow. Run the following command: + +.. code-block:: bash + + juju deploy mlflow --channel=2.1/stable --trust + +This deploys the latest edge version of MLflow with `MinIO `_ as object storage and `MySQL `_ as metadata store. + +Access MLflow +------------- +To access MLflow, visit the following URL in your web browser: + +.. code-block:: bash + + http://localhost:31380/ + +This will take you to the MLflow UI. + +.. note:: by default Charmed MLflow creates a `NodePort `_ on port 31380 where you can access the MLflow UI. + +That's it! Charmed MLflow has been deployed locally with MicroK8s and Juju. You can now start using MLflow. + +Reference: Object storage credentials +------------------------------------- +To use MLflow you need to have credentials to the object storage. The aforementioned bundle comes with MinIO. To get the ``MinIO`` credentials run the following command: + +.. code-block:: bash + + juju run-action mlflow-server/0 get-minio-credentials --wait + +This action will output ``secret-key`` and ``secret-access-key``.