diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/404.html b/404.html new file mode 100644 index 0000000000..a9d6e82717 --- /dev/null +++ b/404.html @@ -0,0 +1,2152 @@ + + + + + + + + + + + + + + + + + + + smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ +

404 - Not found

+ +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_sass/color_schemes/new_colors.scss b/_sass/color_schemes/new_colors.scss new file mode 100644 index 0000000000..d7fadca000 --- /dev/null +++ b/_sass/color_schemes/new_colors.scss @@ -0,0 +1,15 @@ +$body-background-color: $grey-dk-300; +$sidebar-color: $grey-dk-300; +$border-color: $grey-dk-200; +$body-text-color: $grey-lt-300; +$body-heading-color: $grey-lt-000; +$nav-child-link-color: $grey-dk-000; +$search-result-preview-color: $grey-dk-000; +$link-color: $green-000; +$btn-primary-color: $blue-200; +$base-button-color: $grey-dk-250; +$search-background-color: $grey-dk-250; +$table-background-color: $grey-dk-250; +$feedback-color: darken($sidebar-color, 3%); + +$code-background-color: #31343f; diff --git a/assets/fonts/LICENSE.txt b/assets/fonts/LICENSE.txt new file mode 100644 index 0000000000..75d0c06f01 --- /dev/null +++ b/assets/fonts/LICENSE.txt @@ -0,0 +1,94 @@ +Copyright (c) 2022, Matthias Tellen matthias.tellen@googlemail.com, +with Reserved Font Name mononoki. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/MononokiNerdFont-Bold.ttf b/assets/fonts/MononokiNerdFont-Bold.ttf new file mode 100644 index 0000000000..ad20a4923c Binary files /dev/null and b/assets/fonts/MononokiNerdFont-Bold.ttf differ diff --git a/assets/fonts/MononokiNerdFont-BoldItalic.ttf b/assets/fonts/MononokiNerdFont-BoldItalic.ttf new file mode 100644 index 0000000000..a9da568d74 Binary files /dev/null and b/assets/fonts/MononokiNerdFont-BoldItalic.ttf differ diff --git a/assets/fonts/MononokiNerdFont-Italic.ttf b/assets/fonts/MononokiNerdFont-Italic.ttf new file mode 100644 index 0000000000..1e684ef8f5 Binary files /dev/null and b/assets/fonts/MononokiNerdFont-Italic.ttf differ diff --git a/assets/fonts/MononokiNerdFont-Regular.ttf b/assets/fonts/MononokiNerdFont-Regular.ttf new file mode 100644 index 0000000000..9b7900520c Binary files /dev/null and b/assets/fonts/MononokiNerdFont-Regular.ttf differ diff --git a/assets/fonts/MononokiNerdFontMono-Bold.ttf b/assets/fonts/MononokiNerdFontMono-Bold.ttf new file mode 100644 index 0000000000..4aba29bc98 Binary files /dev/null and b/assets/fonts/MononokiNerdFontMono-Bold.ttf differ diff --git a/assets/fonts/MononokiNerdFontMono-BoldItalic.ttf b/assets/fonts/MononokiNerdFontMono-BoldItalic.ttf new file mode 100644 index 0000000000..b3bf8dddcc Binary files /dev/null and b/assets/fonts/MononokiNerdFontMono-BoldItalic.ttf differ diff --git a/assets/fonts/MononokiNerdFontMono-Italic.ttf b/assets/fonts/MononokiNerdFontMono-Italic.ttf new file mode 100644 index 0000000000..4d7e28ac01 Binary files /dev/null and b/assets/fonts/MononokiNerdFontMono-Italic.ttf differ diff --git a/assets/fonts/MononokiNerdFontMono-Regular.ttf b/assets/fonts/MononokiNerdFontMono-Regular.ttf new file mode 100644 index 0000000000..2ec771146c Binary files /dev/null and b/assets/fonts/MononokiNerdFontMono-Regular.ttf differ diff --git a/assets/images/cloud_favicon.png b/assets/images/cloud_favicon.png new file mode 100644 index 0000000000..4c51b845b0 Binary files /dev/null and b/assets/images/cloud_favicon.png differ diff --git a/assets/images/cnpg_operator_screenshot.png b/assets/images/cnpg_operator_screenshot.png new file mode 100644 index 0000000000..0a4cac2a6b Binary files /dev/null and b/assets/images/cnpg_operator_screenshot.png differ diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000000..1cf13b9f9d Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/images/icons/argo_icon.png b/assets/images/icons/argo_icon.png new file mode 100644 index 0000000000..6bebfdb58e Binary files /dev/null and b/assets/images/icons/argo_icon.png differ diff --git a/assets/images/icons/bitwarden.png b/assets/images/icons/bitwarden.png new file mode 100644 index 0000000000..aab64709dd Binary files /dev/null and b/assets/images/icons/bitwarden.png differ diff --git a/assets/images/icons/cert-manager_icon.png b/assets/images/icons/cert-manager_icon.png new file mode 100644 index 0000000000..77c5f493aa Binary files /dev/null and b/assets/images/icons/cert-manager_icon.png differ diff --git a/assets/images/icons/cilium.png b/assets/images/icons/cilium.png new file mode 100644 index 0000000000..569d1c2961 Binary files /dev/null and b/assets/images/icons/cilium.png differ diff --git a/assets/images/icons/eso_icon.png b/assets/images/icons/eso_icon.png new file mode 100644 index 0000000000..11c172827b Binary files /dev/null and b/assets/images/icons/eso_icon.png differ diff --git a/assets/images/icons/home_assistant_icon.png b/assets/images/icons/home_assistant_icon.png new file mode 100644 index 0000000000..13ca064c1f Binary files /dev/null and b/assets/images/icons/home_assistant_icon.png differ diff --git a/assets/images/icons/k0s-logo.svg b/assets/images/icons/k0s-logo.svg new file mode 100644 index 0000000000..bc27f00484 --- /dev/null +++ b/assets/images/icons/k0s-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/assets/images/icons/k3d.png b/assets/images/icons/k3d.png new file mode 100644 index 0000000000..ea1807e7f5 Binary files /dev/null and b/assets/images/icons/k3d.png differ diff --git a/assets/images/icons/k3s_icon.ico b/assets/images/icons/k3s_icon.ico new file mode 100644 index 0000000000..d2e0870b1e Binary files /dev/null and b/assets/images/icons/k3s_icon.ico differ diff --git a/assets/images/icons/k8s_icon.png b/assets/images/icons/k8s_icon.png new file mode 100644 index 0000000000..10a5df53c7 Binary files /dev/null and b/assets/images/icons/k8s_icon.png differ diff --git a/assets/images/icons/k8tz.png b/assets/images/icons/k8tz.png new file mode 100644 index 0000000000..2bb1266b9e Binary files /dev/null and b/assets/images/icons/k8tz.png differ diff --git a/assets/images/icons/k8up.png b/assets/images/icons/k8up.png new file mode 100644 index 0000000000..71f1d1e96e Binary files /dev/null and b/assets/images/icons/k8up.png differ diff --git a/assets/images/icons/k9s_icon.png b/assets/images/icons/k9s_icon.png new file mode 100644 index 0000000000..0b734d5436 Binary files /dev/null and b/assets/images/icons/k9s_icon.png differ diff --git a/assets/images/icons/kepler.png b/assets/images/icons/kepler.png new file mode 100644 index 0000000000..8af0ed7487 Binary files /dev/null and b/assets/images/icons/kepler.png differ diff --git a/assets/images/icons/keycloak.png b/assets/images/icons/keycloak.png new file mode 100644 index 0000000000..48188dedaa Binary files /dev/null and b/assets/images/icons/keycloak.png differ diff --git a/assets/images/icons/kind_icon.png b/assets/images/icons/kind_icon.png new file mode 100644 index 0000000000..eb7e82b8ff Binary files /dev/null and b/assets/images/icons/kind_icon.png differ diff --git a/assets/images/icons/kyverno_icon.png b/assets/images/icons/kyverno_icon.png new file mode 100644 index 0000000000..8791786fba Binary files /dev/null and b/assets/images/icons/kyverno_icon.png differ diff --git a/assets/images/icons/logo.png b/assets/images/icons/logo.png new file mode 100644 index 0000000000..59c156deac Binary files /dev/null and b/assets/images/icons/logo.png differ diff --git a/assets/images/icons/longhorn_icon.png b/assets/images/icons/longhorn_icon.png new file mode 100644 index 0000000000..194f77e56c Binary files /dev/null and b/assets/images/icons/longhorn_icon.png differ diff --git a/assets/images/icons/mastodon.png b/assets/images/icons/mastodon.png new file mode 100644 index 0000000000..b8620f4710 Binary files /dev/null and b/assets/images/icons/mastodon.png differ diff --git a/assets/images/icons/matrix.png b/assets/images/icons/matrix.png new file mode 100644 index 0000000000..68540cf329 Binary files /dev/null and b/assets/images/icons/matrix.png differ diff --git a/assets/images/icons/metallb_icon.png b/assets/images/icons/metallb_icon.png new file mode 100644 index 0000000000..4a49a8d1f0 Binary files /dev/null and b/assets/images/icons/metallb_icon.png differ diff --git a/assets/images/icons/minio.png b/assets/images/icons/minio.png new file mode 100644 index 0000000000..24f73811b7 Binary files /dev/null and b/assets/images/icons/minio.png differ diff --git a/assets/images/icons/netmaker-icon.png b/assets/images/icons/netmaker-icon.png new file mode 100644 index 0000000000..8fab6fc163 Binary files /dev/null and b/assets/images/icons/netmaker-icon.png differ diff --git a/assets/images/icons/nextcloud.png b/assets/images/icons/nextcloud.png new file mode 100644 index 0000000000..c912ce7a87 Binary files /dev/null and b/assets/images/icons/nextcloud.png differ diff --git a/assets/images/icons/nginx.ico b/assets/images/icons/nginx.ico new file mode 100644 index 0000000000..c509adfeac Binary files /dev/null and b/assets/images/icons/nginx.ico differ diff --git a/assets/images/icons/prometheus.png b/assets/images/icons/prometheus.png new file mode 100644 index 0000000000..7a8cc58161 Binary files /dev/null and b/assets/images/icons/prometheus.png differ diff --git a/assets/images/icons/seaweedfs.png b/assets/images/icons/seaweedfs.png new file mode 100644 index 0000000000..74bfb12a0d Binary files /dev/null and b/assets/images/icons/seaweedfs.png differ diff --git a/assets/images/icons/vouch.png b/assets/images/icons/vouch.png new file mode 100644 index 0000000000..f2dfe9fcee Binary files /dev/null and b/assets/images/icons/vouch.png differ diff --git a/assets/images/icons/zitadel.png b/assets/images/icons/zitadel.png new file mode 100644 index 0000000000..5a89c78a86 Binary files /dev/null and b/assets/images/icons/zitadel.png differ diff --git a/assets/images/screenshots/add_k3s_option_screen.svg b/assets/images/screenshots/add_k3s_option_screen.svg new file mode 100644 index 0000000000..f0d9b88b88 --- /dev/null +++ b/assets/images/screenshots/add_k3s_option_screen.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol k8s lab — Kubernetes distro config + + + 🌱 Select a k8s distro────────────────────────────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔▔kind runs k8s clusters using Docker containers as nodes.  +kindDesigned for testing k8s itself. Learn more: kind.sigs.k8s.io +▁▁▁▁▁▁▁▁▁▁▁▁▁ + +───────────────────────────────────────────────────Inputs below are optional + + +Adjust how many of each node type to deploy ───────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔ +control plane:1workers:0 +▁▁▁▁▁▁▁▁▁▁▁▁ + + +─────────────────────────────────────────────────────────────────────────────── + +────────────────────────────────────────────────────────────────────── + + Ad─── +Addnewkind networking option. +Ne +━━━━━ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Anew kind networking option➕ add option +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +i─────────────────────────────────────────────────────────────cancel +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +disableDefaultCNI▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +:False🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +apiServerAddress:127.0.0.1🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +podSubnet:10.244.0.0/16🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +───────────────────────────────────────────────────────────── ➕ kind option + + + + + + diff --git a/assets/images/screenshots/add_kind_option_screen.svg b/assets/images/screenshots/add_kind_option_screen.svg new file mode 100644 index 0000000000..3c5b47edec --- /dev/null +++ b/assets/images/screenshots/add_kind_option_screen.svg @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol k8s lab — Kubernetes distro config + + + 🌱 Select a k8s distro─────────────────────────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔kind runs k8s clusters using Docker containers as nodes.  +kindDesigned for testing k8s itself. Learn more:  + +────────────────────────────────────────────────Inputs below are optional + + +Adjust how many of each node type to deploy ────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁ + +──────────────────────────────────────────────────────────────────────────── +─────────────────────────────────────────────────────────────────── + + + AdAddnewkind networking option.─── + +Ne +━━━━━ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +new kind networking option➕ add option +A▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +i──────────────────────────────────────────────────────────cancel +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +disableDefaultCN▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +I:False🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +apiServerAddress▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +:127.0.0.1🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▆▆ +podSubnet:10.244.0.0/16🚮 + +───────────────────────────────────────────────────────────➕ kind option + + + ?  Help  c  Config  f  Toggle footer  esc  Cancel  f5  Speak  n  New Cluster  + + + diff --git a/assets/images/screenshots/add_node_k3s_tab.svg b/assets/images/screenshots/add_node_k3s_tab.svg new file mode 100644 index 0000000000..5ec64b309a --- /dev/null +++ b/assets/images/screenshots/add_node_k3s_tab.svg @@ -0,0 +1,331 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Kubernetes distro config for nice-kitten + + + 🌱 Select a k8s distro────────────────────────────────────────────────────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔K3s, by Rancher Labs, is a minimal Kubernetes distro that fits in about 70MB.  +k3s(it's also optomized for ARM) Learn more: k3s.io. +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +───────────────────────────────────────────────────────────────────────────Inputs below are optional + + +Customize k3s install with extra optionsandnodes────────────────────────────────────────────────── + +k3s.yamlKubelet Config Options🆕 Add Remote Nodes +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Add a node below for something to appear here... + +               _____ +              /     \ +              vvvvvvv  /|__/| +                 I   /O,O   | +                 I /_____   |     /|/| +                C|/^ ^ ^ \  |   /oo  |    _//| +                 |^ ^ ^ ^ |W|  |/^^\ |   /oo | +                  \m___m__|_|   \m_m_|   \mm_| + +                "Totoros" (from "My Neighbor Totoro") +                    --- Duke Lee + + + + + + + 🖥️  Add a newnode + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +host:hostname or ip addressnode type:worker +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +ssh port:22ssh key:id_rsa +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +node labels:labels to apply to this nodenode taints:taints to apply to this node +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +──────────────────────────────────────────────────────────────────────────────────────────── ➕ node + + + + ctrl+n  add new  b  Back  n  Next  ?  Help  c  Config  f  Hide footer  f5  Speak  + + + diff --git a/assets/images/screenshots/apps_screen.svg b/assets/images/screenshots/apps_screen.svg new file mode 100644 index 0000000000..48478d069c --- /dev/null +++ b/assets/images/screenshots/apps_screen.svg @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Apps Configuration for leuke-friend (now with mo… + + +selectapps──────── 🔧 configure parameters for Argo Cd───────────── + +argo-cd +cert-manager▼ Argo CD Application Configuration +cilium +cnpg-operator +external-secrets-…▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +generic-device-pl…repo:https://github.com/small-hack +home-assistant▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +infisical +ingress-nginx▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +k8tzpath:argocd/app_of_apps/ +k8up▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +kepler +kubevirt▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +kyvernorevision:main +longhorn▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +mastodon +matrix▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +metallbcluster:https://kubernetes.default.sv +minio-operator▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +minio-tenant +netmaker▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +nextcloudnamespace:argocd +openbao▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +postgres-operator▃▃ +prometheus-crdsdirectory ▔▔▔▔▔▔▔▔ +prometheusrecursion:▁▁ +seaweedfs▁▁▁▁▁▁▁▁ + +───────────✨ newapp +▼ Template values for Argo CD ApplicationSet + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +✏️  Modify Globalshostname:argo-cd.mydomain.org +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +─────────────────────────────────────────────────── + + 📓 Argo Cd notes──────────────────────────────────────────────────────────── + +Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. +▃▃ +smol-k8s-lab installs Argo CD with helm initially to support initial  +configuration of your admin user and disabling of dex. After your OIDC  +provider is configured, Argo CD begins managing itself using the below  +configured Argo CD repo. +─────────────────────────────────────────────────────────────────────────────── + + + + + + diff --git a/assets/images/screenshots/apps_screen_backups.svg b/assets/images/screenshots/apps_screen_backups.svg new file mode 100644 index 0000000000..2c83705f84 --- /dev/null +++ b/assets/images/screenshots/apps_screen_backups.svg @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Apps Configuration for k3s-lovely-bunny (now with more 🦑) + + +selectapps───────────────── 🔧 configure parameters for Nextcloud───────────────────────────── + +argo-cdInitialization ConfigArgo CD App ConfigBackupRestore +cert-manager━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +cilium +cnpg-operator +external-secrets-operator▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +generic-device-plugin +home-assistant💾 Backup Now +infisical +ingress-nginx▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +k8tz +k8up +kepler +kubevirt +kyverno📆 Scheduled backups +longhorn +mastodon +matrix▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +metallb📁 PVC schedule:10 0 * * * +minio-operator▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +minio-tenant +netmaker▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +nextcloud🐘 DB schedule:0 0 0 * * * +openbao▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +postgres-operator +prometheus-crds +prometheus▼ S3 Configuration +seaweedfs +seaweedfs-csi-driver +vault▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +vouchendpoint:s3.eu-central-003.backblazeb2.com▇▇ +zitadel▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +────────────────────✨ newapp▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +bucket:my-remote-s3-bucket +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +✏️  Modify Globalsregion:eu-central-003 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +────────────────────────────────────────────────🔁 sync / 🗑️ delete + + 📓 Nextcloud notes───────────────────────────────────────────────────────────────────────────────────── + +Nextcloud Hub is the industry-leading, fully open-source, on-premises content collaboration platform.  +Teams access, share and edit their documents, chat and participate in video calls and manage their  +mail and calendar and projects across mobile, desktop and web interfaces▃▃ + +smol-k8s-lab supports initialization by setting up your admin username, password, and SMTP username  +and password, as well as your redis and postgresql credentials. + +────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + + b  Back  a  New App  n  Next  ?  Help  c  Config  f  Hide footer  f5  Speak  + + + diff --git a/assets/images/screenshots/apps_screen_init.svg b/assets/images/screenshots/apps_screen_init.svg new file mode 100644 index 0000000000..b82566c276 --- /dev/null +++ b/assets/images/screenshots/apps_screen_init.svg @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Apps Configuration for k3s-lovely-bunny (now with more 🦑) + + +selectapps────────────── 🔧 configure parameters for Nextcloud─────────────────────── + +argo-cdInitialization ConfigArgo CD App ConfigBackupRestore +cert-manager━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +cilium +cnpg-operator▔▔▔▔▔▔▔▔ +external-secrets-operatorInitializationEnabled:  +generic-device-plugin▁▁▁▁▁▁▁▁ +home-assistant +infisical +ingress-nginxInit Values +k8tz +k8up +kepler▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +kubevirtadmin user:my_nextcloud_admin +kyverno▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +longhorn +mastodon +matrix▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +metallbsmtp user:my_smtp_nextcloud_username +minio-operator▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +minio-tenant +netmaker +nextcloud▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +openbaosmtp host:smtp-server.com +postgres-operator▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +prometheus-crds +prometheus +seaweedfs▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +seaweedfs-csi-driversmtp password:Enter a smtp password +vault▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +vouch +zitadel + +─────────────────✨ newapp + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +✏️  Modify Globals +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +──────────────────────────────────────────🔁 sync / 🗑️ delete + + 📓 Nextcloud notes──────────────────────────────────────────────────────────────────────────── + +Nextcloud Hub is the industry-leading, fully open-source, on-premises content collaboration  +platform. Teams access, share and edit their documents, chat and participate in video calls  +and manage their mail and calendar and projects across mobile, desktop and web interfaces▃▃ + +smol-k8s-lab supports initialization by setting up your admin username, password, and SMTP  +username and password, as well as your redis and postgresql credentials. + +───────────────────────────────────────────────────────────────────────────────────────────────── + + + + b  Back  a  New App  n  Next  ?  Help  c  Config  f  Hide footer  f5  Speak  + + + diff --git a/assets/images/screenshots/apps_screen_restores.svg b/assets/images/screenshots/apps_screen_restores.svg new file mode 100644 index 0000000000..b442467c88 --- /dev/null +++ b/assets/images/screenshots/apps_screen_restores.svg @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab 5.8.0 + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab 5.8.0 — Apps Configuration for k3s-lovely-bunny + + +selectapps─────────────── 🔧 configure parameters for Nextcloud─────────────────────────────── + +argo-cdInitialization ConfigArgo CD App ConfigBackupRestore +cert-manager━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +cilium +cnpg-operator▔▔▔▔▔▔▔▔ +external-secrets-operatorRestore from backupEnabled:  +generic-device-plugin▁▁▁▁▁▁▁▁ +home-assistant +infisical▔▔▔▔▔▔▔▔ +ingress-nginxRestore 🐘 CNPG clusterEnabled:  +k8tz▁▁▁▁▁▁▁▁ +k8up +kepler +kubevirtRestic Snapshot IDs +kyverno +longhorn +mastodon▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +matrixseaweedfs volume:latest +metallb▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +minio-operator +minio-tenant▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +netmakerseaweedfs filer:latest +nextcloud▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +openbao +postgres-operator▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +prometheus-crdsnextcloud files:latest +prometheus▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +seaweedfs +seaweedfs-csi-driver +vault +vouch +zitadel + +──────────────────✨ newapp + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +✏️  Modify Globals +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +──────────────────────────────────────────────────🔁 sync / 🗑️ delete + + 📓 Nextcloud notes──────────────────────────────────────────────────────────────────────────────────── + +Nextcloud Hub is the industry-leading, fully open-source, on-premises content collaboration platform. +Teams access, share and edit their documents, chat and participate in video calls and manage their  +mail and calendar and projects across mobile, desktop and web interfaces + +smol-k8s-lab supports initialization by setting up your admin username, password, and SMTP username  +and password, as well as your redis and postgresql credentials.▂▂ + +To avoid providing sensitive values everytime you run smol-k8s-lab, consider exporting the following  +environment variables before running smol-k8s-lab: +  - NEXTCLOUD_SMTP_PASSWORD +───────────────────────────────────────────────────────────────────────────────────────────────────────── + + + + + + diff --git a/assets/images/screenshots/argocd_screenshot.png b/assets/images/screenshots/argocd_screenshot.png new file mode 100644 index 0000000000..c3e2d49900 Binary files /dev/null and b/assets/images/screenshots/argocd_screenshot.png differ diff --git a/assets/images/screenshots/bitwarden_credentials_screen.svg b/assets/images/screenshots/bitwarden_credentials_screen.svg new file mode 100644 index 0000000000..e6da5026f1 --- /dev/null +++ b/assets/images/screenshots/bitwarden_credentials_screen.svg @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Review your configuration for smol-bunny (las… + + +ReviewAllValues──────────────────────────────────────────────────────── + +Core ConfigK8s Distro ConfigApps ConfigGlobal Parameters Config +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +smol_k8s_lab: + 🛡️ Enter Bitwarden Vault Credentials ────────────────────────────────────── + +Requires personal API credentials. To avoid this prompt, export the  +following env vars before running smol-k8s-lab: BW_PASSWORD, BW_CLIENTID, +BW_CLIENTSECRET + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +password:password +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +client ID:client_id +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +client secret:client_secret +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +submitcancel +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +──────────────────────────────────────────────────────────────────────────── + +──────────────────────────────────────────────────────────────────────────── + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +🚊 Let's roll!✋Go Back +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + b  Back  ?  Help  c  Config  f  Hide footer  f5  Speak  n  New Cluster  + + + diff --git a/assets/images/screenshots/bweso_screenshot.png b/assets/images/screenshots/bweso_screenshot.png new file mode 100644 index 0000000000..bfcb969903 Binary files /dev/null and b/assets/images/screenshots/bweso_screenshot.png differ diff --git a/assets/images/screenshots/certmanager_screenshot.png b/assets/images/screenshots/certmanager_screenshot.png new file mode 100644 index 0000000000..c677215716 Binary files /dev/null and b/assets/images/screenshots/certmanager_screenshot.png differ diff --git a/assets/images/screenshots/confirm_screen.svg b/assets/images/screenshots/confirm_screen.svg new file mode 100644 index 0000000000..dde893a053 --- /dev/null +++ b/assets/images/screenshots/confirm_screen.svg @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Review your configuration for smol-bunny (las… + + +ReviewAllValues──────────────────────────────────────────────────────── + +Core ConfigK8s Distro ConfigApps ConfigGlobal Parameters Config +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +smol_k8s_lab: +tui: +enabled:true +# show bottom footer help bar +show_footer:true +# accessibility options for users that benefit from TTS and Bell s +accessibility: +bell: +# ring the built in terminal bell on focus to new elements on  +on_focus:false +# ring the built in terminal bell when something is wrong +on_error:true +text_to_speech: +language:en +speech_program:'' +# read aloud the screen title +screen_titles:false▁▁ +# read aloud the screen description +screen_descriptions:false +# read aloud the element id, value, and tooltip each time you  +on_focus:false +# press f5 to read the element id and selected row of DataTabl +on_key_press:true + +run_command: +# command to run after smol-k8s-lab tui is done or immediately whe +command:k9s --command applications.argoproj.io +# tell me which terminal you use if you'd like to use split or tab +terminal:wezterm + +──────────────────────────────────────────────────────────────────────────── + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +🚊 Let's roll!✋Go Back +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + b  ⬅️ Back  ?  Help  c  Config  f  Hide footer  f5  Speak  + + + diff --git a/assets/images/screenshots/delete_app_modal_screen.svg b/assets/images/screenshots/delete_app_modal_screen.svg new file mode 100644 index 0000000000..7398bb89df --- /dev/null +++ b/assets/images/screenshots/delete_app_modal_screen.svg @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab 5.4.0 + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab 5.4.0 — Apps Configuration for k3s-lovely-bunny + + +selectapps──────── 🔧 configure parameters for Matrix────────────── + +argo-cdzation ConfigArgo CD App ConfigBackupRes +cert-manager━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +cilium +cnpg-operator +external-secrets-…▼ Argo CD Application Configuration +generic-device-pl… +home-assistant +infisical▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▆▆ +ingress-nginxrepo:https://github.com/small-hack +k8tz▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +k8up +kepler▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +kubevirtpath:matrix/app_of_apps/ +kyverno▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +longhorn +mastodon▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Are you sure you want to delete the matrix Application? ──────────── + + +──────────────▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +🔨 Force🚮 Delete +──────────────▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +─────────────────────────────────────────────────────────────cancel +───────────✨ newapp▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +directory ▔▔▔▔▔▔▔▔ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔recursion: +✏️  Modify Globals▁▁▁▁▁▁▁▁ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +──────────────────────────────🔁 sync / 🗑️ delete + + 📓 Matrix notes───────────────────────────────────────────────────────────── + +Matrix is an open protocol for decentralised, secure communications. +This deploys a matrix synapse server, element (web frontend), and turn  +server (voice) + +smol-k8s-lab supports initialization by creating initial secrets for your: +  - matrix, element, and federation hostnames, +─────────────────────────────────────────────────────────────────────────────── + + + b Back a New App n Next ? Help c Config f Hide footer f5 Speak + + + diff --git a/assets/images/screenshots/delete_cluster_confirmation.svg b/assets/images/screenshots/delete_cluster_confirmation.svg new file mode 100644 index 0000000000..49d6eddd3b --- /dev/null +++ b/assets/images/screenshots/delete_cluster_confirmation.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + + +                     _       _    ___            _       _      + ___ _ __ ___   ___ | |     | | _( _ ) ___      | | __ _| |__   +/ __| '_ ` _ \ / _ \| |_____| |/ / _ \/ __|_____| |/ _` | '_ \  +\__ \ | | | | | (_) | |_____|   < (_) \__ \_____| | (_| | |_) | +|___/_| |_| |_|\___/|_|     |_|\_\___/|___/     |_|\__,_|_.__/  + + + + + +──────────── Select a row to modify or delete an existingcluster──────────── + +───────────────────────────────────────────────────────────────────── + + + +Are you sure you want to deletek3s-cute-wasbeertje? + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 🚮 Yes  🤷 Cancel  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +───────────────────────────────────────────────────────────────────── +────────────────────────────────────────────────────────────────────────────── + + +──────────────────Create a newcluster with the name below ────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +leuke-kitten ✨ New Cluster  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +────────────────────────────────────────────────────────────────────────────── + + + + + + diff --git a/assets/images/screenshots/delete_node_confirm_modal_screen.svg b/assets/images/screenshots/delete_node_confirm_modal_screen.svg new file mode 100644 index 0000000000..8623252267 --- /dev/null +++ b/assets/images/screenshots/delete_node_confirm_modal_screen.svg @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Kubernetes nodes config for k3s-lovely-bunny + + + + + + + + + + + + + + +    Node      Status   Type   SSH Port  SSH Key     Labels               Taints           + + smol-node  Ready   worker    2222    id_rsa   reserved=iot  reserved=iot:NoSchedule  + + + + + +────────────────────────────────────────────────────────────────────────────────────────── + + + +Are you sure you want to deletesmol-node? + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +🚮 Yes🤷 Cancel +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +────────────────────────────────────────────────────────────────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +host:hostname or ip addressnode type:worker +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +ssh port:22ssh key:id_rsa +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +node labels:labels to apply to this nodenode taints:taints to apply to this node +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + b  Back  ?  Help  c  Config  f  Hide footer  f5  Speak  n  New Cluster  + + + diff --git a/assets/images/screenshots/distro_config_screen.svg b/assets/images/screenshots/distro_config_screen.svg new file mode 100644 index 0000000000..b9b9b118a1 --- /dev/null +++ b/assets/images/screenshots/distro_config_screen.svg @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol k8s lab — Kubernetes distro config + + + 🌱 Select a k8s distro────────────────────────────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔▔k3d is a lightweight wrapper to run k3s (Rancher Lab’s  +k3dminimal Kubernetes distribution) in Docker containers. Learn  +▁▁▁▁▁▁▁▁▁▁▁▁▁more: k3d.io. + +───────────────────────────────────────────────────Inputs below are optional + + +Adjust how many of each node type to deploy ───────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔ +control plane:1workers:0 +▁▁▁▁▁▁▁▁▁▁▁▁ + + +─────────────────────────────────────────────────────────────────────────────── + + + +Add extra options for the k3s install script ──────────────────────────────── + +k3s.yamlKubelet Config Options +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + +Add extra k3s options to pass to the k3s install script via a config  +file stored in /home/friend/.cache/smol-k8s-lab/k3s.yaml. Please use  +the second tab for extra kubelet args. + +secrets ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +encryption:true🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +disable:traefik🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +node label:ingress-ready=true🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + +────────────────────────────────────────────────────────────── ➕ k3s option + + + + + + diff --git a/assets/images/screenshots/eso_screenshot.png b/assets/images/screenshots/eso_screenshot.png new file mode 100644 index 0000000000..dc0930e7f3 Binary files /dev/null and b/assets/images/screenshots/eso_screenshot.png differ diff --git a/assets/images/screenshots/help_text.svg b/assets/images/screenshots/help_text.svg new file mode 100644 index 0000000000..0945665e78 --- /dev/null +++ b/assets/images/screenshots/help_text.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + term + + + + + + + + + +                           🧸smol k8s lab + +Install slim Kubernetes distros + plus all your apps via Argo CD. + +Usage:smol-k8s-lab[OPTIONS] + +╭─ ʕ ᵔᴥᵔʔ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + +-c--config CONFIG_FILEFull path and name of the YAML config file to parse.             +Defaults to $XDG_CONFIG_HOME/smol-k8s-lab/config.yaml + +-D--delete CLUSTER_NAMEDelete an existing cluster by name.                              + +-i--interactive⚙️ Interactively configures smol-k8s-lab + +-v--versionPrint the version of smol-k8s-lab (v5.11.0)                      + +-f--final_cmdRun command immediately after smol-k8s-lab before main cli phase + +-h--helpShow this message and exit.                                      +╰────────────────────────────────────────────────────────────────────── ♥ docs: https://small-hack.github.io/smol-k8s-lab─╯ + + + + diff --git a/assets/images/screenshots/home-assistant-helm.png b/assets/images/screenshots/home-assistant-helm.png new file mode 100644 index 0000000000..bd3c413c13 Binary files /dev/null and b/assets/images/screenshots/home-assistant-helm.png differ diff --git a/assets/images/screenshots/home-assistant.png b/assets/images/screenshots/home-assistant.png new file mode 100644 index 0000000000..86dcfd4172 Binary files /dev/null and b/assets/images/screenshots/home-assistant.png differ diff --git a/assets/images/screenshots/ingress_nginx_screenshot.png b/assets/images/screenshots/ingress_nginx_screenshot.png new file mode 100644 index 0000000000..899c4ecc5a Binary files /dev/null and b/assets/images/screenshots/ingress_nginx_screenshot.png differ diff --git a/assets/images/screenshots/invalid_apps_screen.svg b/assets/images/screenshots/invalid_apps_screen.svg new file mode 100644 index 0000000000..51be5981c5 --- /dev/null +++ b/assets/images/screenshots/invalid_apps_screen.svg @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + + + + + + + ⚠️ The following app fields are empty ──────────────────────────────────────── + +Click the app links below to fix the errors or disable them. + + + Application   Invalid Fields                                    + + argo_cd       hostname                                          + + + cert_manager  email                                             + + + k8up          timezone                                          + + + metallb       address_pool                                      + + + vouch         domains, emails, hostname                         + + + zitadel       username, email, first_name, last_name, hostname  + + + +─────────────────────────────────────────────────────────────────────────────── + + + + + + + ?  Help  c  Config  f  Toggle footer  b  ⬅️ Back  f5  Speak  + + + diff --git a/assets/images/screenshots/k8tz_screenshot.png b/assets/images/screenshots/k8tz_screenshot.png new file mode 100644 index 0000000000..4a016847e2 Binary files /dev/null and b/assets/images/screenshots/k8tz_screenshot.png differ diff --git a/assets/images/screenshots/k8up_screenshot.png b/assets/images/screenshots/k8up_screenshot.png new file mode 100644 index 0000000000..d827f3b469 Binary files /dev/null and b/assets/images/screenshots/k8up_screenshot.png differ diff --git a/assets/images/screenshots/kind_config_screen.svg b/assets/images/screenshots/kind_config_screen.svg new file mode 100644 index 0000000000..d78bb0de17 --- /dev/null +++ b/assets/images/screenshots/kind_config_screen.svg @@ -0,0 +1,300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol k8s lab — Kubernetes distro config + + + 🌱 Select a k8s distro────────────────────────────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔▔kind runs k8s clusters using Docker containers as nodes.  +kindDesigned for testing k8s itself. Learn more: kind.sigs.k8s.io +▁▁▁▁▁▁▁▁▁▁▁▁▁ + +───────────────────────────────────────────────────Inputs below are optional + + +Adjust how many of each node type to deploy ───────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔ +control plane:1workers:0 +▁▁▁▁▁▁▁▁▁▁▁▁ + + +─────────────────────────────────────────────────────────────────────────────── + + + + Add extra options for kind config files ───────────────────────────────────── + +Networking optionsKubelet Config Options +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + +Add key value pairs to kind networking config. + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +ipFamily:ipv4🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +disableDefaultCNI▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +:False🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +apiServerAddress:127.0.0.1🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +podSubnet:10.244.0.0/16🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +───────────────────────────────────────────────────────────── ➕ kind option + + + + + + diff --git a/assets/images/screenshots/kind_config_screen2.svg b/assets/images/screenshots/kind_config_screen2.svg new file mode 100644 index 0000000000..c843aa5af3 --- /dev/null +++ b/assets/images/screenshots/kind_config_screen2.svg @@ -0,0 +1,298 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol k8s lab — Kubernetes distro config + + + 🌱 Select a k8s distro────────────────────────────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔▔kind runs k8s clusters using Docker containers as nodes.  +kindDesigned for testing k8s itself. Learn more: kind.sigs.k8s.io +▁▁▁▁▁▁▁▁▁▁▁▁▁ + +───────────────────────────────────────────────────Inputs below are optional + + +Adjust how many of each node type to deploy ───────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔ +control plane:1workers:0 +▁▁▁▁▁▁▁▁▁▁▁▁ + + +─────────────────────────────────────────────────────────────────────────────── + + + + Add extra options for kind config files ───────────────────────────────────── + +Networking optionsKubelet Config Options +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + +Add key value pairs to pass to your kubeletconfiguration. + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +node-labels:ingress-ready=true🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +resolv-conf:etc/resolv.conf🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +max-pods:110🚮 +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + +───────────────────────────────────────────────────────────── ➕ kind option + + + + + + diff --git a/assets/images/screenshots/kubevirt-manager.png b/assets/images/screenshots/kubevirt-manager.png new file mode 100644 index 0000000000..149fea5a4e Binary files /dev/null and b/assets/images/screenshots/kubevirt-manager.png differ diff --git a/assets/images/screenshots/logging_password_config.svg b/assets/images/screenshots/logging_password_config.svg new file mode 100644 index 0000000000..b39f5dc365 --- /dev/null +++ b/assets/images/screenshots/logging_password_config.svg @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol k8s lab — Configure logging and password manager + + + 🪵ConfigureLogging──────────────────────────────────────────────────── + + +Configure logging for all of smol-k8s-lab. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +level:debugfile:/home/friend/.local/state/smo +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +────────────────────────────────────────────────────────────────────────── + + 🔒ConfigurePassword Manager─────────────────────────────────────────── + + +Save app credentials to a local password manager vault. Only Bitwarden  +is supported at this time, but if enabled, Bitwarden can be used as  +your k8s external secret provider. To avoid a password prompt, export  +the following env vars: BW_PASSWORD, BW_CLIENTID, BW_CLIENTSECRET +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +enabled:duplicate strategy:edit +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +────────────────────────────────────────────────────────────────────────── + + 💻 Configurecommand to run after config─────────────────────────────── + + +If window behavior is set to same window, command runs after +smol-k8s-lab has completed. +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +terminal:weztermwindow split right +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁behavior:▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +command:k9s --command applications.argoproj.io +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +────────────────────────────────────────────────────────────────────────── + + + + b  Back  n  Next  ?  Help  c  Config  f  Hide footer  f5  Speak  + + + diff --git a/assets/images/screenshots/mastodon_networking_screenshot.png b/assets/images/screenshots/mastodon_networking_screenshot.png new file mode 100644 index 0000000000..2315c1e348 Binary files /dev/null and b/assets/images/screenshots/mastodon_networking_screenshot.png differ diff --git a/assets/images/screenshots/mastodon_screenshot.png b/assets/images/screenshots/mastodon_screenshot.png new file mode 100644 index 0000000000..7c383c80eb Binary files /dev/null and b/assets/images/screenshots/mastodon_screenshot.png differ diff --git a/assets/images/screenshots/matrix-networking.png b/assets/images/screenshots/matrix-networking.png new file mode 100644 index 0000000000..85cc101e3f Binary files /dev/null and b/assets/images/screenshots/matrix-networking.png differ diff --git a/assets/images/screenshots/matrix-web-app.png b/assets/images/screenshots/matrix-web-app.png new file mode 100644 index 0000000000..e3f5ad0c85 Binary files /dev/null and b/assets/images/screenshots/matrix-web-app.png differ diff --git a/assets/images/screenshots/matrix.png b/assets/images/screenshots/matrix.png new file mode 100644 index 0000000000..3f64d2516d Binary files /dev/null and b/assets/images/screenshots/matrix.png differ diff --git a/assets/images/screenshots/metallb_example.svg b/assets/images/screenshots/metallb_example.svg new file mode 100644 index 0000000000..bfff024fd3 --- /dev/null +++ b/assets/images/screenshots/metallb_example.svg @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol k8s lab — Apps Configuration (now with more 🦑) + + +Selectapps───────────── 🛠️ Configure parameters for metallb─────────────────────── +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +argo-cd▔▔▔▔▔▔▔▔ +appset-secret-plug…InitializationEnabled:  +bitwarden-eso-prov…▁▁▁▁▁▁▁▁ +cert-manager +cilium▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +external-secrets-o…address pool:192.168.20.23/32, 192.168.20.24/32 +infisical▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +ingress-nginx +k8tz +k8upArgo CD Application Configuration +kepler +keycloak +kubevirt▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +kyvernorepo:https://github.com/small-hack/argocd +mastodon▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +matrix +metallb▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +miniopath:metallb/ +nextcloud▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +prometheus▆▆ +vouch▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +─────────────────────────────ref:main +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▃▃ + ✨ New App ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁namespace:metallb-system +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +Template values for Argo CD ApplicationSet  +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ✏️ Modify Globals +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁smol-k8s-lab doesn't include any templated values for  +this app, but you can add some below if you're using a  +custom Argo CD App repo. +──────────────────────────────────────────────────────────── + + App Description ────────────────────────────────────────────────────────────────────────── + +Helps expose IP addresses for loadbalancers on metal if you're on a vm or container where  +you can't get an IP. + +Cloud Compatibility: https://metallb.org/installation/clouds/ + +Learn more: https://metallb.org/ + +smol-k8s-lab support initialization by deploying a default l2Advertisement  IPAddressPool. + +──────────────────────────────────────────────────────────────────────────────────────────── + + + + ?  Help  c  Config  f  Toggle footer  b  Back  f5  Speak  n  Next  + + + diff --git a/assets/images/screenshots/modify_cluster_modal_screen.svg b/assets/images/screenshots/modify_cluster_modal_screen.svg new file mode 100644 index 0000000000..994df9ac94 --- /dev/null +++ b/assets/images/screenshots/modify_cluster_modal_screen.svg @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + + + + +                     _       _    ___            _       _      + ___ _ __ ___   ___ | |     | | _( _ ) ___      | | __ _| |__   +/ __| '_ ` _ \ / _ \| |_____| |/ / _ \/ __|_____| |/ _` | '_ \  +\__ \ | | | | | (_) | |_____|   < (_) \__ \_____| | (_| | |_) | +|___/_| |_| |_|\___/|_|     |_|\_\___/|___/     |_|\__,_|_.__/  + + + + + + + +────────── Select a row to modify or delete an existingcluster────────── + + +────────────────────────────────────────────────────────────────────────── + + + +What would you like to do with k3s-lovely-bunny? + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +✏️  Modify Apps🖥️ Modify Nodes🚮 Delete +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +─────────────────────────────────────────────────────────────────cancel +────────────────Create a newcluster with the name below ──────────────── + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +soft-bunny✨ New Cluster +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +────────────────────────────────────────────────────────────────────────── + + + + + + + + + + b  Back  f5  Speak  ?  Help  c  Config  f  Hide footer  n  New Cluster  + + + diff --git a/assets/images/screenshots/modify_global_parameters_modal_screen.svg b/assets/images/screenshots/modify_global_parameters_modal_screen.svg new file mode 100644 index 0000000000..9e62c454e1 --- /dev/null +++ b/assets/images/screenshots/modify_global_parameters_modal_screen.svg @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Apps Configuration for k3s-lovely-bunny (now with more 🦑) + + +selectapps─────────── 🔧 configure parameters for Argo Cd─────────────────── + +argo-cd +cert-manager▼ Argo CD Application Configuration +cilium +cnpg-operator +external-secrets-ope…▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +generic-device-pluginrepo:https://github.com/small-hack/arg +home-assistant▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +────────────────────────────────────────────────────────────────────────────── + + +Modifyglobally available Argo CD ApplicationSet templating values. + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +cluster issuer:letsencrypt-prod +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +time zone:Europe/Amsterdam +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂▂ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +external secrets:bitwarden +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +──── + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +▔▔new key name +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▁▁ + +─── +──────────────────────────────────────────────────────────────────────close + 📓 Argo Cd notes───────────────────────────────────────────────────────────────────── + +Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. +▂▂ +smol-k8s-lab installs Argo CD with helm initially to support initial configuration  +of your admin user and disabling of dex. After your OIDC provider is configured,  +Argo CD begins managing itself using the below configured Argo CD repo. + +──────────────────────────────────────────────────────────────────────────────────────── + + + b  Back  f5  Speak  ?  Help  c  Config  f  Hide footer  n  New Cluster  + + + diff --git a/assets/images/screenshots/modify_node_modal_screen.svg b/assets/images/screenshots/modify_node_modal_screen.svg new file mode 100644 index 0000000000..fdfa50df85 --- /dev/null +++ b/assets/images/screenshots/modify_node_modal_screen.svg @@ -0,0 +1,284 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Kubernetes nodes config for k3s-lovely-bunny + + + + + + + + + + + +    Node      Status   Type   SSH Port  SSH Key     Labels               Taints           + + smol-node  Ready   worker    2222    id_rsa   reserved=iot  reserved=iot:NoSchedule  + + + + +──────────────────────────────────────────────────────────────────────────── + + + +What would you like to do with smol-node? + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +✏️  Modify🚮 Delete🤷 Cancel +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +────────────────────────────────────────────────────────────────────────────▔▔▔▔▔▔▔▔ +host:hostname or ip addressnode type:worker +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +ssh port:22ssh key:id_rsa +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +node labels:labels to apply to this nodenode taints:taints to apply to this node +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + b  Back  ?  Help  c  Config  f  Hide footer  f5  Speak  n  New Cluster  + + + diff --git a/assets/images/screenshots/modify_nodes_screen.svg b/assets/images/screenshots/modify_nodes_screen.svg new file mode 100644 index 0000000000..5fa9786ab0 --- /dev/null +++ b/assets/images/screenshots/modify_nodes_screen.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Kubernetes nodes config for k3s-lovely-bunny + + + + + + + + + + + +    Node      Status   Type   SSH Port  SSH Key     Labels               Taints           + + smol-node  Ready   worker    2222    id_rsa   reserved=iot  reserved=iot:NoSchedule  + + + + + + + + + + + + + + + 🖥️  Add a newnode + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +host:hostname or ip addressnode type:worker +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +ssh port:22ssh key:id_rsa +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +node labels:labels to apply to this nodenode taints:taints to apply to this node +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + b  Back  ctrl+n  add new node  ?  Help  c  Config  f  Hide footer  f5  Speak  n  New Cluster  + + + diff --git a/assets/images/screenshots/modify_nodes_widget.svg b/assets/images/screenshots/modify_nodes_widget.svg new file mode 100644 index 0000000000..fcb14f4bf5 --- /dev/null +++ b/assets/images/screenshots/modify_nodes_widget.svg @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Kubernetes distro config for cute-wasbeertje + + + 🌱 Select a k8s distro────────────────────────────────────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔K3s, by Rancher Labs, is a minimal Kubernetes distro that fits in  +k3sabout 70MB. (it's also optomized for ARM) Learn more: k3s.io. +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +───────────────────────────────────────────────────────────Inputs below are optional + + +Customize k3s install with extra optionsandnodes────────────────────────────────── + +k3s.yamlKubelet Config Options🆕 Add Remote Nodes +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + + + + + +    Node       Type   SSH Port  SSH Key     Labels               Taints           + + smol-node  worker    2222    id_rsa   reserved=iot  reserved=iot:NoSchedule  + + + + + + + + 🖥️  Add a newnode + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +host:hostname or ip addressnode type:worker +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +ssh port:22ssh key:id_rsa +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +node labels to apply to node taints to apply to this +labels:▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁taints:▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +──────────────────────────────────────────────────────────────────────────── ➕ node + + + ctrl+n  add new  b  Back  n  Next  ?  Help  c  Config  f  Hide footer  f5  Speak  + + + diff --git a/assets/images/screenshots/netmaker-network.png b/assets/images/screenshots/netmaker-network.png new file mode 100644 index 0000000000..0157018cbc Binary files /dev/null and b/assets/images/screenshots/netmaker-network.png differ diff --git a/assets/images/screenshots/netmaker.png b/assets/images/screenshots/netmaker.png new file mode 100644 index 0000000000..d00dabf4b4 Binary files /dev/null and b/assets/images/screenshots/netmaker.png differ diff --git a/assets/images/screenshots/new_app_modal_screen.svg b/assets/images/screenshots/new_app_modal_screen.svg new file mode 100644 index 0000000000..7afd57dec1 --- /dev/null +++ b/assets/images/screenshots/new_app_modal_screen.svg @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol-k8s-lab  — Apps Configuration for cool-kitten (now with mor… + + +selectapps──────── 🔧 configure parameters for Argo Cd───────────── + +argo-cd +cert-manager▼ Argo CD Application Configuration +cilium +cnpg-operator +external-secrets-…▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +generic-device-pl…repo:https://github.com/small-hack +home-assistant▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +infisical +ingress-nginx▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +k8tzpath:argocd/app_of_apps/ +────────────────────────────────────────────────────────────────────── + + +Please enter a name and description for your Argo CD Application. + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Name of your Argo CD Application +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +(optional) Description of your Argo CD Application▄▄ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +──── + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +submit +▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▁▁ +─────────────────────────────────────────────────────────────cancel +─────────────────────────────────────────────────── + + 📓 Argo Cd notes──────────────────────────────────────────────────────────── + +Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. +▃▃ +smol-k8s-lab installs Argo CD with helm initially to support initial  +configuration of your admin user and disabling of dex. After your OIDC  +provider is configured, Argo CD begins managing itself using the below  +configured Argo CD repo. +─────────────────────────────────────────────────────────────────────────────── + + + + b  Back  f5  Speak  ?  Help  c  Config  f  Hide footer  n  New Cluster  + + + diff --git a/assets/images/screenshots/nextcloud-web-app.png b/assets/images/screenshots/nextcloud-web-app.png new file mode 100644 index 0000000000..e5dcdeebfc Binary files /dev/null and b/assets/images/screenshots/nextcloud-web-app.png differ diff --git a/assets/images/screenshots/nextcloud.png b/assets/images/screenshots/nextcloud.png new file mode 100644 index 0000000000..c0e934de25 Binary files /dev/null and b/assets/images/screenshots/nextcloud.png differ diff --git a/assets/images/screenshots/prometheus_screenshot.png b/assets/images/screenshots/prometheus_screenshot.png new file mode 100644 index 0000000000..3e33882377 Binary files /dev/null and b/assets/images/screenshots/prometheus_screenshot.png differ diff --git a/assets/images/screenshots/start_screen.svg b/assets/images/screenshots/start_screen.svg new file mode 100644 index 0000000000..28f3d66b0a --- /dev/null +++ b/assets/images/screenshots/start_screen.svg @@ -0,0 +1,255 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + + + + + + + + + + + + + + + +                     _       _    ___            _       _      + ___ _ __ ___   ___ | |     | | _( _ ) ___      | | __ _| |__   +/ __| '_ ` _ \ / _ \| |_____| |/ / _ \/ __|_____| |/ _` | '_ \  +\__ \ | | | | | (_) | |_____|   < (_) \__ \_____| | (_| | |_) | +|___/_| |_| |_|\___/|_|     |_|\_\___/|___/     |_|\__,_|_.__/  + + + +────────────Create a newcluster with the name below ───────────── + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +adorable-raccoon✨ New Cluster +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +─────────────────────────────────────────────────────────────────── + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/screenshots/start_screen_with_existing_clusters.svg b/assets/images/screenshots/start_screen_with_existing_clusters.svg new file mode 100644 index 0000000000..7c4d7b640d --- /dev/null +++ b/assets/images/screenshots/start_screen_with_existing_clusters.svg @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + + + + +                     _       _    ___            _       _      + ___ _ __ ___   ___ | |     | | _( _ ) ___      | | __ _| |__   +/ __| '_ ` _ \ / _ \| |_____| |/ / _ \/ __|_____| |/ _` | '_ \  +\__ \ | | | | | (_) | |_____|   < (_) \__ \_____| | (_| | |_) | +|___/_| |_| |_|\___/|_|     |_|\_\___/|___/     |_|\__,_|_.__/  + + + + + + + + + +────────── Select a row to modify or delete an existingcluster─────────── + + + + + + +     Cluster       Distro    Version      Platform    + + k3s-lovely-bunny   k3s    v1.29.3+k3s1  linux/amd64  + + + + + + + +─────────────────────────────────────────────────────────────────────────── + + +────────────────Create a newcluster with the name below ───────────────── + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +adorable-wasbeertje✨ New Cluster +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +─────────────────────────────────────────────────────────────────────────── + + + + + + + + + + + + + ?  Help  c  Config  f  Hide footer  f5  Speak  n  New Cluster  + + + diff --git a/assets/images/screenshots/tui_config_screen.svg b/assets/images/screenshots/tui_config_screen.svg new file mode 100644 index 0000000000..f33c6b7888 --- /dev/null +++ b/assets/images/screenshots/tui_config_screen.svg @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + ʕ ᵔᴥᵔʔ smol k8s lab — Configure Terminal UI and Accessibility features + + + ♿️ ConfigureAccessibility Features─────────────────────────────────────────────── + + +Terminal Bell Config██████████████████████████████████████████████████████████████ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +bell on focus:bell on error: +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +Text to Speech Config█████████████████████████████████████████████████████████████ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +speech program:name of program for speech +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +on key press:on focus: +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + +▔▔▔▔▔▔▔▔screen ▔▔▔▔▔▔▔▔ +screen titles:descriptions: +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +───────────────────────────────────────────────────────────────────────────────────── + + + 🖥️ ConfigureTerminal UI─────────────────────────────────────────────────────────── + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +tui enabled:footer: +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + +───────────────────────────────────────────────────────────────────────────────────── + + + + + + diff --git a/assets/images/screenshots/tui_help_screen.svg b/assets/images/screenshots/tui_help_screen.svg new file mode 100644 index 0000000000..1195a22d6c --- /dev/null +++ b/assets/images/screenshots/tui_help_screen.svg @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BaseApp + + + + + + + + + + + + +Welcome to smol-k8s-lab─────────────────────────────────────────────────── + +Use your 🐁 to click anything in the UI ✨ Or use the following key  +bindings. For additional help, check out the docs + + +    Key Binding      Description                                           + + right arrow         complete suggestion in input field                    + + + up and down arrows  navigate up or down selection lists and data tables   + + + tab                 focus next element                                    + + + shift+tab           focus previous element                                + + + enter               save input and/or press button                        + + + ? or h              toggle help screen                                    + + + spacebar            select selection option                               + + + meta+click          open link; terminal dependent, so meta can be shift,  +  option, windowsKey, command, or control              + + + esc or q            leave current screen and go home                      + + + c                   launch the config screen                              + + + f5                  read aloud current focused element ID                 + + + f                   toggle showing the footer                             + + + +─────────────────────────────────────────────── made with 💙 + 🐍 + textual + + + + + + diff --git a/assets/images/screenshots/vouch_screenshot.png b/assets/images/screenshots/vouch_screenshot.png new file mode 100644 index 0000000000..0a8bd605b4 Binary files /dev/null and b/assets/images/screenshots/vouch_screenshot.png differ diff --git a/assets/images/screenshots/zitadel_screenshot.png b/assets/images/screenshots/zitadel_screenshot.png new file mode 100644 index 0000000000..dc7bb086dc Binary files /dev/null and b/assets/images/screenshots/zitadel_screenshot.png differ diff --git a/assets/images/seaweedfs.drawio.png b/assets/images/seaweedfs.drawio.png new file mode 100644 index 0000000000..81aafc92c3 Binary files /dev/null and b/assets/images/seaweedfs.drawio.png differ diff --git a/assets/javascripts/bundle.fe8b6f2b.min.js b/assets/javascripts/bundle.fe8b6f2b.min.js new file mode 100644 index 0000000000..cf778d4288 --- /dev/null +++ b/assets/javascripts/bundle.fe8b6f2b.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Fi=Object.create;var gr=Object.defineProperty;var ji=Object.getOwnPropertyDescriptor;var Wi=Object.getOwnPropertyNames,Dt=Object.getOwnPropertySymbols,Ui=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty,no=Object.prototype.propertyIsEnumerable;var oo=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,R=(e,t)=>{for(var r in t||(t={}))xr.call(t,r)&&oo(e,r,t[r]);if(Dt)for(var r of Dt(t))no.call(t,r)&&oo(e,r,t[r]);return e};var io=(e,t)=>{var r={};for(var o in e)xr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Dt)for(var o of Dt(e))t.indexOf(o)<0&&no.call(e,o)&&(r[o]=e[o]);return r};var yr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Di=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Wi(t))!xr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=ji(t,n))||o.enumerable});return e};var Vt=(e,t,r)=>(r=e!=null?Fi(Ui(e)):{},Di(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var ao=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var co=yr((Er,so)=>{(function(e,t){typeof Er=="object"&&typeof so!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(H){return!!(H&&H!==document&&H.nodeName!=="HTML"&&H.nodeName!=="BODY"&&"classList"in H&&"contains"in H.classList)}function p(H){var mt=H.type,ze=H.tagName;return!!(ze==="INPUT"&&a[mt]&&!H.readOnly||ze==="TEXTAREA"&&!H.readOnly||H.isContentEditable)}function c(H){H.classList.contains("focus-visible")||(H.classList.add("focus-visible"),H.setAttribute("data-focus-visible-added",""))}function l(H){H.hasAttribute("data-focus-visible-added")&&(H.classList.remove("focus-visible"),H.removeAttribute("data-focus-visible-added"))}function f(H){H.metaKey||H.altKey||H.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(H){o=!1}function h(H){s(H.target)&&(o||p(H.target))&&c(H.target)}function w(H){s(H.target)&&(H.target.classList.contains("focus-visible")||H.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(H.target))}function A(H){document.visibilityState==="hidden"&&(n&&(o=!0),te())}function te(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function ie(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(H){H.target.nodeName&&H.target.nodeName.toLowerCase()==="html"||(o=!1,ie())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",A,!0),te(),r.addEventListener("focus",h,!0),r.addEventListener("blur",w,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var Yr=yr((Rt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Rt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Rt=="object"?Rt.ClipboardJS=r():t.ClipboardJS=r()})(Rt,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ii}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(V){try{return document.execCommand(V)}catch(_){return!1}}var h=function(_){var M=f()(_);return u("cut"),M},w=h;function A(V){var _=document.documentElement.getAttribute("dir")==="rtl",M=document.createElement("textarea");M.style.fontSize="12pt",M.style.border="0",M.style.padding="0",M.style.margin="0",M.style.position="absolute",M.style[_?"right":"left"]="-9999px";var j=window.pageYOffset||document.documentElement.scrollTop;return M.style.top="".concat(j,"px"),M.setAttribute("readonly",""),M.value=V,M}var te=function(_,M){var j=A(_);M.container.appendChild(j);var D=f()(j);return u("copy"),j.remove(),D},ie=function(_){var M=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},j="";return typeof _=="string"?j=te(_,M):_ instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(_==null?void 0:_.type)?j=te(_.value,M):(j=f()(_),u("copy")),j},J=ie;function H(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?H=function(M){return typeof M}:H=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},H(V)}var mt=function(){var _=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=_.action,j=M===void 0?"copy":M,D=_.container,Y=_.target,ke=_.text;if(j!=="copy"&&j!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&H(Y)==="object"&&Y.nodeType===1){if(j==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(j==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(ke)return J(ke,{container:D});if(Y)return j==="cut"?w(Y):J(Y,{container:D})},ze=mt;function Ie(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(M){return typeof M}:Ie=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},Ie(V)}function _i(V,_){if(!(V instanceof _))throw new TypeError("Cannot call a class as a function")}function ro(V,_){for(var M=0;M<_.length;M++){var j=_[M];j.enumerable=j.enumerable||!1,j.configurable=!0,"value"in j&&(j.writable=!0),Object.defineProperty(V,j.key,j)}}function Ai(V,_,M){return _&&ro(V.prototype,_),M&&ro(V,M),V}function Ci(V,_){if(typeof _!="function"&&_!==null)throw new TypeError("Super expression must either be null or a function");V.prototype=Object.create(_&&_.prototype,{constructor:{value:V,writable:!0,configurable:!0}}),_&&br(V,_)}function br(V,_){return br=Object.setPrototypeOf||function(j,D){return j.__proto__=D,j},br(V,_)}function Hi(V){var _=Pi();return function(){var j=Wt(V),D;if(_){var Y=Wt(this).constructor;D=Reflect.construct(j,arguments,Y)}else D=j.apply(this,arguments);return ki(this,D)}}function ki(V,_){return _&&(Ie(_)==="object"||typeof _=="function")?_:$i(V)}function $i(V){if(V===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return V}function Pi(){if(typeof Reflect=="undefined"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(V){return!1}}function Wt(V){return Wt=Object.setPrototypeOf?Object.getPrototypeOf:function(M){return M.__proto__||Object.getPrototypeOf(M)},Wt(V)}function vr(V,_){var M="data-clipboard-".concat(V);if(_.hasAttribute(M))return _.getAttribute(M)}var Ri=function(V){Ci(M,V);var _=Hi(M);function M(j,D){var Y;return _i(this,M),Y=_.call(this),Y.resolveOptions(D),Y.listenClick(j),Y}return Ai(M,[{key:"resolveOptions",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof D.action=="function"?D.action:this.defaultAction,this.target=typeof D.target=="function"?D.target:this.defaultTarget,this.text=typeof D.text=="function"?D.text:this.defaultText,this.container=Ie(D.container)==="object"?D.container:document.body}},{key:"listenClick",value:function(D){var Y=this;this.listener=c()(D,"click",function(ke){return Y.onClick(ke)})}},{key:"onClick",value:function(D){var Y=D.delegateTarget||D.currentTarget,ke=this.action(Y)||"copy",Ut=ze({action:ke,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Ut?"success":"error",{action:ke,text:Ut,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(D){return vr("action",D)}},{key:"defaultTarget",value:function(D){var Y=vr("target",D);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(D){return vr("text",D)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(D){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(D,Y)}},{key:"cut",value:function(D){return w(D)}},{key:"isSupported",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof D=="string"?[D]:D,ke=!!document.queryCommandSupported;return Y.forEach(function(Ut){ke=ke&&!!document.queryCommandSupported(Ut)}),ke}}]),M}(s()),Ii=Ri},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,h,w){var A=c.apply(this,arguments);return l.addEventListener(u,A,w),{destroy:function(){l.removeEventListener(u,A,w)}}}function p(l,f,u,h,w){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(A){return s(A,f,u,h,w)}))}function c(l,f,u,h){return function(w){w.delegateTarget=a(w.target,f),w.delegateTarget&&h.call(l,w)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,h,w){if(!u&&!h&&!w)throw new Error("Missing required arguments");if(!a.string(h))throw new TypeError("Second argument must be a String");if(!a.fn(w))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,h,w);if(a.nodeList(u))return l(u,h,w);if(a.string(u))return f(u,h,w);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,h,w){return u.addEventListener(h,w),{destroy:function(){u.removeEventListener(h,w)}}}function l(u,h,w){return Array.prototype.forEach.call(u,function(A){A.addEventListener(h,w)}),{destroy:function(){Array.prototype.forEach.call(u,function(A){A.removeEventListener(h,w)})}}}function f(u,h,w){return s(document.body,u,h,w)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var ts=/["'&<>]/;ei.exports=rs;function rs(e){var t=""+e,r=ts.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||s(u,h)})})}function s(u,h){try{p(o[u](h))}catch(w){f(i[0][3],w)}}function p(u){u.value instanceof nt?Promise.resolve(u.value.v).then(c,l):f(i[0][2],u)}function c(u){s("next",u)}function l(u){s("throw",u)}function f(u,h){u(h),i.shift(),i.length&&s(i[0][0],i[0][1])}}function mo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof de=="function"?de(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function k(e){return typeof e=="function"}function ft(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ft(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Fe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=de(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(A){t={error:A}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(k(l))try{l()}catch(A){i=A instanceof zt?A.errors:[A]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=de(f),h=u.next();!h.done;h=u.next()){var w=h.value;try{fo(w)}catch(A){i=i!=null?i:[],A instanceof zt?i=q(q([],N(i)),N(A.errors)):i.push(A)}}}catch(A){o={error:A}}finally{try{h&&!h.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)fo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Fe.EMPTY;function qt(e){return e instanceof Fe||e&&"closed"in e&&k(e.remove)&&k(e.add)&&k(e.unsubscribe)}function fo(e){k(e)?e():e.unsubscribe()}var $e={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var ut={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Fe(function(){o.currentObservers=null,qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,o){return new Eo(r,o)},t}(F);var Eo=function(e){re(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){re(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var Lt={now:function(){return(Lt.delegate||Date).now()},delegate:void 0};var _t=function(e){re(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=Lt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(vt);var So=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(gt);var Hr=new So(To);var Oo=function(e){re(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=bt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(bt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(vt);var Mo=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(gt);var me=new Mo(Oo);var O=new F(function(e){return e.complete()});function Yt(e){return e&&k(e.schedule)}function kr(e){return e[e.length-1]}function Xe(e){return k(kr(e))?e.pop():void 0}function He(e){return Yt(kr(e))?e.pop():void 0}function Bt(e,t){return typeof kr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return k(e==null?void 0:e.then)}function Jt(e){return k(e[ht])}function Xt(e){return Symbol.asyncIterator&&k(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Gi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Gi();function tr(e){return k(e==null?void 0:e[er])}function rr(e){return lo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return k(e==null?void 0:e.getReader)}function W(e){if(e instanceof F)return e;if(e!=null){if(Jt(e))return Ji(e);if(xt(e))return Xi(e);if(Gt(e))return Zi(e);if(Xt(e))return Lo(e);if(tr(e))return ea(e);if(or(e))return ta(e)}throw Zt(e)}function Ji(e){return new F(function(t){var r=e[ht]();if(k(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Xi(e){return new F(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,Te(1),r?Be(t):zo(function(){return new ir}))}}function Fr(e){return e<=0?function(){return O}:y(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,h=0,w=!1,A=!1,te=function(){f==null||f.unsubscribe(),f=void 0},ie=function(){te(),l=u=void 0,w=A=!1},J=function(){var H=l;ie(),H==null||H.unsubscribe()};return y(function(H,mt){h++,!A&&!w&&te();var ze=u=u!=null?u:r();mt.add(function(){h--,h===0&&!A&&!w&&(f=Wr(J,p))}),ze.subscribe(mt),!l&&h>0&&(l=new at({next:function(Ie){return ze.next(Ie)},error:function(Ie){A=!0,te(),f=Wr(ie,n,Ie),ze.error(Ie)},complete:function(){w=!0,te(),f=Wr(ie,a),ze.complete()}}),W(H).subscribe(l))})(c)}}function Wr(e,t){for(var r=[],o=2;oe.next(document)),e}function $(e,t=document){return Array.from(t.querySelectorAll(e))}function P(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Re(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var xa=S(d(document.body,"focusin"),d(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Re()||document.body),G(1));function et(e){return xa.pipe(m(t=>e.contains(t)),K())}function kt(e,t){return C(()=>S(d(e,"mouseenter").pipe(m(()=>!0)),d(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Me(+!r*t)):le,Q(e.matches(":hover"))))}function Bo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Bo(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Bo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function wt(e){let t=x("script",{src:e});return C(()=>(document.head.appendChild(t),S(d(t,"load"),d(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),L(()=>document.head.removeChild(t)),Te(1))))}var Go=new g,ya=C(()=>typeof ResizeObserver=="undefined"?wt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Go.next(t)))),v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),G(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return ya.pipe(E(r=>r.observe(t)),v(r=>Go.pipe(b(o=>o.target===t),L(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function Tt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Jo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Ue(e){return{x:e.offsetLeft,y:e.offsetTop}}function Xo(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Zo(e){return S(d(window,"load"),d(window,"resize")).pipe(Le(0,me),m(()=>Ue(e)),Q(Ue(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function De(e){return S(d(e,"scroll"),d(window,"scroll"),d(window,"resize")).pipe(Le(0,me),m(()=>pr(e)),Q(pr(e)))}var en=new g,Ea=C(()=>I(new IntersectionObserver(e=>{for(let t of e)en.next(t)},{threshold:0}))).pipe(v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),G(1));function tt(e){return Ea.pipe(E(t=>t.observe(e)),v(t=>en.pipe(b(({target:r})=>r===e),L(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function tn(e,t=16){return De(e).pipe(m(({y:r})=>{let o=ce(e),n=Tt(e);return r>=n.height-o.height-t}),K())}var lr={drawer:P("[data-md-toggle=drawer]"),search:P("[data-md-toggle=search]")};function rn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function Ve(e){let t=lr[e];return d(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function wa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ta(){return S(d(window,"compositionstart").pipe(m(()=>!0)),d(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function on(){let e=d(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:rn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Re();if(typeof o!="undefined")return!wa(o,r)}return!0}),pe());return Ta().pipe(v(t=>t?O:e))}function xe(){return new URL(location.href)}function pt(e,t=!1){if(B("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function nn(){return new g}function an(){return location.hash.slice(1)}function sn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Sa(e){return S(d(window,"hashchange"),e).pipe(m(an),Q(an()),b(t=>t.length>0),G(1))}function cn(e){return Sa(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function $t(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function pn(){let e=matchMedia("print");return S(d(window,"beforeprint").pipe(m(()=>!0)),d(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():O))}function zr(e,t){return new F(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function Ne(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),G(1))}function ln(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),G(1))}function mn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),G(1))}function fn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function un(){return S(d(window,"scroll",{passive:!0}),d(window,"resize",{passive:!0})).pipe(m(fn),Q(fn()))}function dn(){return{width:innerWidth,height:innerHeight}}function hn(){return d(window,"resize",{passive:!0}).pipe(m(dn),Q(dn()))}function bn(){return z([un(),hn()]).pipe(m(([e,t])=>({offset:e,size:t})),G(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(Z("size")),n=z([o,r]).pipe(m(()=>Ue(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function Oa(e){return d(e,"message",t=>t.data)}function Ma(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function vn(e,t=new Worker(e)){let r=Oa(t),o=Ma(t),n=new g;n.subscribe(o);let i=o.pipe(X(),ne(!0));return n.pipe(X(),Pe(r.pipe(U(i))),pe())}var La=P("#__config"),St=JSON.parse(La.textContent);St.base=`${new URL(St.base,xe())}`;function ye(){return St}function B(e){return St.features.includes(e)}function Ee(e,t){return typeof t!="undefined"?St.translations[e].replace("#",t.toString()):St.translations[e]}function Se(e,t=document){return P(`[data-md-component=${e}]`,t)}function ae(e,t=document){return $(`[data-md-component=${e}]`,t)}function _a(e){let t=P(".md-typeset > :first-child",e);return d(t,"click",{once:!0}).pipe(m(()=>P(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function gn(e){if(!B("announce.dismiss")||!e.childElementCount)return O;if(!e.hidden){let t=P(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),_a(e).pipe(E(r=>t.next(r)),L(()=>t.complete()),m(r=>R({ref:e},r)))})}function Aa(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function xn(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Aa(e,t).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))}function Pt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function yn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function En(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Pt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Pt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function wn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,x("del",null,c)," "],[]).slice(0,-1),i=ye(),a=new URL(e.location,i.base);B("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=ye();return x("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${c}`},p)}),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Ee("search.result.term.missing"),": ",...n)))}function Tn(e){let t=e[0].score,r=[...e],o=ye(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreqr(l,1)),...p.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,p.length>0&&p.length===1?Ee("search.result.more.one"):Ee("search.result.more.other",p.length))),...p.map(l=>qr(l,1)))]:[]];return x("li",{class:"md-search-result__item"},c)}function Sn(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Qr(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function On(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ca(e){var o;let t=ye(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Mn(e,t){var o;let r=ye();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Ee("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Ca)))}var Ha=0;function ka(e){let t=z([et(e),kt(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Jo(e)).pipe(oe(De),ct(1),m(()=>Xo(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function $a(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ha++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(X(),ne(!1)).subscribe(a);let s=a.pipe(Ht(c=>Me(+!c*250,Hr)),K(),v(c=>c?r:O),E(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>kt(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),ee(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),h=u.width/2;if(l.role==="tooltip")return{x:h,y:8+u.height};if(u.y>=f.height/2){let{height:w}=ce(l);return{x:h,y:-16-w}}else return{x:h,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),ee(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(P(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),be(me),ee(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ka(e).pipe(E(c=>i.next(c)),L(()=>i.complete()),m(c=>R({ref:e},c)))})}function lt(e,{viewport$:t},r=document.body){return $a(e,{content$:new F(o=>{let n=e.title,i=yn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Pa(e,t){let r=C(()=>z([Zo(e),De(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),Te(+!o||1/0))))}function Ln(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(U(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),S(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Le(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),d(n,"click").pipe(U(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),d(n,"mousedown").pipe(U(a),ee(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Re())==null||c.blur()}}),r.pipe(U(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Pa(e,t).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function Ra(e){return e.tagName==="CODE"?$(".c, .c1, .cm",e):[e]}function Ia(e){let t=[];for(let r of Ra(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function _n(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Ia(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,En(p,i)),s.replaceWith(a.get(p)))}return a.size===0?O:C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=[];for(let[l,f]of a)c.push([P(".md-typeset",f),P(`:scope > li:nth-child(${l})`,e)]);return o.pipe(U(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?_n(f,u):_n(u,f)}),S(...[...a].map(([,l])=>Ln(l,t,{target$:r}))).pipe(L(()=>s.complete()),pe())})}function An(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return An(t)}}function Cn(e,t){return C(()=>{let r=An(e);return typeof r!="undefined"?fr(r,e,t):O})}var Hn=Vt(Yr());var Fa=0;function kn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return kn(t)}}function ja(e){return ge(e).pipe(m(({width:t})=>({scrollable:Tt(e).width>t})),Z("scrollable"))}function $n(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(Fr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Hn.default.isSupported()&&(e.closest(".copy")||B("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Fa++}`;let l=wn(c.id);c.insertBefore(l,e),B("content.tooltips")&&a.push(lt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=kn(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||B("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(U(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:O)))}}return $(":scope > span[id]",e).length&&e.classList.add("md-code__content"),ja(e).pipe(E(c=>n.next(c)),L(()=>n.complete()),m(c=>R({ref:e},c)),Pe(...a))});return B("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function Wa(e,{target$:t,print$:r}){let o=!0;return S(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),E(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Pn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),Wa(e,t).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}var Rn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Br,Da=0;function Va(){return typeof mermaid=="undefined"||mermaid instanceof Element?wt("https://unpkg.com/mermaid@10/dist/mermaid.min.js"):I(void 0)}function In(e){return e.classList.remove("mermaid"),Br||(Br=Va().pipe(E(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Rn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),G(1))),Br.subscribe(()=>ao(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Da++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Br.pipe(m(()=>({ref:e})))}var Fn=x("table");function jn(e){return e.replaceWith(Fn),Fn.replaceWith(On(e)),I({ref:e})}function Na(e){let t=e.find(r=>r.checked)||e[0];return S(...e.map(r=>d(r,"change").pipe(m(()=>P(`label[for="${r.id}"]`))))).pipe(Q(P(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Wn(e,{viewport$:t,target$:r}){let o=P(".tabbed-labels",e),n=$(":scope > input",e),i=Qr("prev");e.append(i);let a=Qr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(X(),ne(!0));z([s,ge(e),tt(e)]).pipe(U(p),Le(1,me)).subscribe({next([{active:c},l]){let f=Ue(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let h=pr(o);(f.xh.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([De(o),ge(o)]).pipe(U(p)).subscribe(([c,l])=>{let f=Tt(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),S(d(i,"click").pipe(m(()=>-1)),d(a,"click").pipe(m(()=>1))).pipe(U(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(U(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=P(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),d(l.firstElementChild,"click").pipe(U(p),b(f=>!(f.metaKey||f.ctrlKey)),E(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return B("content.tabs.link")&&s.pipe(Ce(1),ee(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let w of $("[data-tabs]"))for(let A of $(":scope > input",w)){let te=P(`label[for="${A.id}"]`);if(te!==c&&te.innerText.trim()===f){te.setAttribute("data-md-switching",""),A.click();break}}window.scrollTo({top:e.offsetTop-u});let h=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...h])])}}),s.pipe(U(p)).subscribe(()=>{for(let c of $("audio, video",e))c.pause()}),Na(n).pipe(E(c=>s.next(c)),L(()=>s.complete()),m(c=>R({ref:e},c)))}).pipe(Qe(se))}function Un(e,{viewport$:t,target$:r,print$:o}){return S(...$(".annotate:not(.highlight)",e).map(n=>Cn(n,{target$:r,print$:o})),...$("pre:not(.mermaid) > code",e).map(n=>$n(n,{target$:r,print$:o})),...$("pre.mermaid",e).map(n=>In(n)),...$("table:not([class])",e).map(n=>jn(n)),...$("details",e).map(n=>Pn(n,{target$:r,print$:o})),...$("[data-tabs]",e).map(n=>Wn(n,{viewport$:t,target$:r})),...$("[title]",e).filter(()=>B("content.tooltips")).map(n=>lt(n,{viewport$:t})))}function za(e,{alert$:t}){return t.pipe(v(r=>S(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function Dn(e,t){let r=P(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),za(e,t).pipe(E(n=>o.next(n)),L(()=>o.complete()),m(n=>R({ref:e},n)))})}var qa=0;function Qa(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?De(o):I({x:0,y:0}),i=S(et(t),kt(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=Ue(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Vn(e){let t=e.title;if(!t.length)return O;let r=`__tooltip_${qa++}`,o=Pt(r,"inline"),n=P(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),S(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Le(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Qa(o,e).pipe(E(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))}).pipe(Qe(se))}function Ka({viewport$:e}){if(!B("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Ye(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=Ve("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Nn(e,t){return C(()=>z([ge(e),Ka(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),G(1))}function zn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(X(),ne(!0));o.pipe(Z("active"),We(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue($("[title]",e)).pipe(b(()=>B("content.tooltips")),oe(a=>Vn(a)));return r.subscribe(o),t.pipe(U(n),m(a=>R({ref:e},a)),Pe(i.pipe(U(n))))})}function Ya(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),Z("active"))}function qn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?O:Ya(o,t).pipe(E(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))})}function Qn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),Z("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function Ba(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(oe(o=>d(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),G(1))}function Kn(e){let t=$("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=$t("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),ee(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(be(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),Ba(t).pipe(U(n.pipe(Ce(1))),st(),E(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))})}function Yn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(E(o=>r.next({value:o})),L(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Gr=Vt(Yr());function Ga(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Bn({alert$:e}){Gr.default.isSupported()&&new F(t=>{new Gr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Ga(P(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(E(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function Gn(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function Ja(e,t){let r=new Map;for(let o of $("url",e)){let n=P("loc",o),i=[Gn(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of $("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(Gn(new URL(s),t))}}return r}function ur(e){return mn(new URL("sitemap.xml",e)).pipe(m(t=>Ja(t,new URL(e))),ve(()=>I(new Map)))}function Xa(e,t){if(!(e.target instanceof Element))return O;let r=e.target.closest("a");if(r===null)return O;if(r.target||e.metaKey||e.ctrlKey)return O;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):O}function Jn(e){let t=new Map;for(let r of $(":scope > *",e.head))t.set(r.outerHTML,r);return t}function Xn(e){for(let t of $("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function Za(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...B("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=Jn(document);for(let[o,n]of Jn(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return je($("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new F(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),O}),X(),ne(document))}function Zn({location$:e,viewport$:t,progress$:r}){let o=ye();if(location.protocol==="file:")return O;let n=ur(o.base);I(document).subscribe(Xn);let i=d(document.body,"click").pipe(We(n),v(([p,c])=>Xa(p,c)),pe()),a=d(window,"popstate").pipe(m(xe),pe());i.pipe(ee(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),S(i,a).subscribe(e);let s=e.pipe(Z("pathname"),v(p=>ln(p,{progress$:r}).pipe(ve(()=>(pt(p,!0),O)))),v(Xn),v(Za),pe());return S(s.pipe(ee(e,(p,c)=>c)),s.pipe(v(()=>e),Z("pathname"),v(()=>e),Z("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),E(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",sn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),d(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(Z("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ri=Vt(ti());function oi(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ri.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function It(e){return e.type===1}function dr(e){return e.type===3}function ni(e,t){let r=vn(e);return S(I(location.protocol!=="file:"),Ve("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:B("search.suggest")}}})),r}function ii({document$:e}){let t=ye(),r=Ne(new URL("../versions.json",t.base)).pipe(ve(()=>O)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>d(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),ee(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?O:(i.preventDefault(),I(p))}}return O}),v(i=>ur(new URL(i)).pipe(m(a=>{let p=xe().href.replace(t.base,i);return a.has(p.split("#")[0])?new URL(p):new URL(i)})))))).subscribe(n=>pt(n,!0)),z([r,o]).subscribe(([n,i])=>{P(".md-header__topic").appendChild(Mn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let p of s)for(let c of n.aliases.concat(n.version))if(new RegExp(p,"i").test(c)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ae("outdated"))s.hidden=!1})}function ns(e,{worker$:t}){let{searchParams:r}=xe();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),Ve("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=xe();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=S(t.pipe(Ae(It)),d(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),G(1))}function ai(e,{worker$:t}){let r=new g,o=r.pipe(X(),ne(!0));z([t.pipe(Ae(It)),r],(i,a)=>a).pipe(Z("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(Z("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),d(e.form,"reset").pipe(U(o)).subscribe(()=>e.focus());let n=P("header [for=__search]");return d(n,"click").subscribe(()=>e.focus()),ns(e,{worker$:t}).pipe(E(i=>r.next(i)),L(()=>r.complete()),m(i=>R({ref:e},i)),G(1))}function si(e,{worker$:t,query$:r}){let o=new g,n=tn(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=P(":scope > :first-child",e),s=P(":scope > :last-child",e);Ve("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(ee(r),Ur(t.pipe(Ae(It)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?Ee("search.result.none"):Ee("search.result.placeholder");break;case 1:a.textContent=Ee("search.result.one");break;default:let u=sr(l.length);a.textContent=Ee("search.result.other",u)}});let p=o.pipe(E(()=>s.innerHTML=""),v(({items:l})=>S(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Ye(4),Vr(n),v(([f])=>f)))),m(Tn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(oe(l=>{let f=fe("details",l);return typeof f=="undefined"?O:d(f,"toggle").pipe(U(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(E(l=>o.next(l)),L(()=>o.complete()),m(l=>R({ref:e},l)))}function is(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=xe();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function ci(e,t){let r=new g,o=r.pipe(X(),ne(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),d(e,"click").pipe(U(o)).subscribe(n=>n.preventDefault()),is(e,t).pipe(E(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))}function pi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=S(d(n,"keydown"),d(n,"focus")).pipe(be(se),m(()=>n.value),K());return o.pipe(We(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(E(s=>o.next(s)),L(()=>o.complete()),m(()=>({ref:e})))}function li(e,{index$:t,keyboard$:r}){let o=ye();try{let n=ni(o.search,t),i=Se("search-query",e),a=Se("search-result",e);d(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Re();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of $(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,h])=>h-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...$(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Re()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=ai(i,{worker$:n});return S(s,si(a,{worker$:n,query$:s})).pipe(Pe(...ae("search-share",e).map(p=>ci(p,{query$:s})),...ae("search-suggest",e).map(p=>pi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ke}}function mi(e,{index$:t,location$:r}){return z([t,r.pipe(Q(xe()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>oi(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=x("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function as(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Jr(e,o){var n=o,{header$:t}=n,r=io(n,["header$"]);let i=P(".md-sidebar__scrollwrap",e),{y:a}=Ue(i);return C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=s.pipe(Le(0,me));return c.pipe(ee(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of $(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2})}}}),ue($("label[tabindex]",e)).pipe(oe(l=>d(l,"click").pipe(be(se),m(()=>l),U(p)))).subscribe(l=>{let f=P(`[id="${l.htmlFor}"]`);P(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),as(e,r).pipe(E(l=>s.next(l)),L(()=>s.complete()),m(l=>R({ref:e},l)))})}function fi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return Ct(Ne(`${r}/releases/latest`).pipe(ve(()=>O),m(o=>({version:o.tag_name})),Be({})),Ne(r).pipe(ve(()=>O),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Be({}))).pipe(m(([o,n])=>R(R({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return Ne(r).pipe(m(o=>({repositories:o.public_repos})),Be({}))}}function ui(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Ne(r).pipe(ve(()=>O),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Be({}))}function di(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return fi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ui(r,o)}return O}var ss;function cs(e){return ss||(ss=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return O}return di(e.href).pipe(E(o=>__md_set("__source",o,sessionStorage)))}).pipe(ve(()=>O),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),G(1)))}function hi(e){let t=P(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(Sn(o)),t.classList.add("md-source__repository--active")}),cs(e).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ps(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),Z("hidden"))}function bi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(B("navigation.tabs.sticky")?I({hidden:!1}):ps(e,t)).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ls(e,{viewport$:t,header$:r}){let o=new Map,n=$(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(Z("height"),m(({height:s})=>{let p=Se("main"),c=P(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(Z("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let h=f.offsetParent;for(;h;h=h.offsetParent)u+=h.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),We(i),v(([p,c])=>t.pipe(jr(([l,f],{offset:{y:u},size:h})=>{let w=u+h.height>=Math.floor(s.height);for(;f.length;){let[,A]=f[0];if(A-c=u&&!w)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Ye(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(X(),ne(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),B("toc.follow")){let s=S(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),We(o.pipe(be(se))),ee(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2,behavior:c})}}})}return B("navigation.tracking")&&t.pipe(U(a),Z("offset"),_e(250),Ce(1),U(n.pipe(Ce(1))),st({delay:250}),ee(i)).subscribe(([,{prev:s}])=>{let p=xe(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),ls(e,{viewport$:t,header$:r}).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function ms(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Ye(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),U(o.pipe(Ce(1))),ne(!0),st({delay:250}),m(a=>({hidden:a})))}function gi(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(U(a),Z("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),d(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),ms(e,{viewport$:t,main$:o,target$:n}).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))}function xi({document$:e,viewport$:t}){e.pipe(v(()=>$(".md-ellipsis")),oe(r=>tt(r).pipe(U(e.pipe(Ce(1))),b(o=>o),m(()=>r),Te(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,B("content.tooltips")?lt(n,{viewport$:t}).pipe(U(e.pipe(Ce(1))),L(()=>n.removeAttribute("title"))):O})).subscribe(),B("content.tooltips")&&e.pipe(v(()=>$(".md-status")),oe(r=>lt(r,{viewport$:t}))).subscribe()}function yi({document$:e,tablet$:t}){e.pipe(v(()=>$(".md-toggle--indeterminate")),E(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>d(r,"change").pipe(Dr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),ee(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function fs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ei({document$:e}){e.pipe(v(()=>$("[data-md-scrollfix]")),E(t=>t.removeAttribute("data-md-scrollfix")),b(fs),oe(t=>d(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function wi({viewport$:e,tablet$:t}){z([Ve("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),ee(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function us(){return location.protocol==="file:"?wt(`${new URL("search/search_index.js",Xr.base)}`).pipe(m(()=>__index),G(1)):Ne(new URL("search/search_index.json",Xr.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Yo(),jt=nn(),Ot=cn(jt),Zr=on(),Oe=bn(),hr=$t("(min-width: 960px)"),Si=$t("(min-width: 1220px)"),Oi=pn(),Xr=ye(),Mi=document.forms.namedItem("search")?us():Ke,eo=new g;Bn({alert$:eo});var to=new g;B("navigation.instant")&&Zn({location$:jt,viewport$:Oe,progress$:to}).subscribe(ot);var Ti;((Ti=Xr.version)==null?void 0:Ti.provider)==="mike"&&ii({document$:ot});S(jt,Ot).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});Zr.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&&pt(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&&pt(r);break;case"Enter":let o=Re();o instanceof HTMLLabelElement&&o.click()}});xi({viewport$:Oe,document$:ot});yi({document$:ot,tablet$:hr});Ei({document$:ot});wi({viewport$:Oe,tablet$:hr});var rt=Nn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Qn(e,{viewport$:Oe,header$:rt})),G(1)),ds=S(...ae("consent").map(e=>xn(e,{target$:Ot})),...ae("dialog").map(e=>Dn(e,{alert$:eo})),...ae("header").map(e=>zn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("palette").map(e=>Kn(e)),...ae("progress").map(e=>Yn(e,{progress$:to})),...ae("search").map(e=>li(e,{index$:Mi,keyboard$:Zr})),...ae("source").map(e=>hi(e))),hs=C(()=>S(...ae("announce").map(e=>gn(e)),...ae("content").map(e=>Un(e,{viewport$:Oe,target$:Ot,print$:Oi})),...ae("content").map(e=>B("search.highlight")?mi(e,{index$:Mi,location$:jt}):O),...ae("header-title").map(e=>qn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Si,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>bi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>vi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})),...ae("top").map(e=>gi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})))),Li=ot.pipe(v(()=>hs),Pe(ds),G(1));Li.subscribe();window.document$=ot;window.location$=jt;window.target$=Ot;window.keyboard$=Zr;window.viewport$=Oe;window.tablet$=hr;window.screen$=Si;window.print$=Oi;window.alert$=eo;window.progress$=to;window.component$=Li;})(); +//# sourceMappingURL=bundle.fe8b6f2b.min.js.map + diff --git a/assets/javascripts/bundle.fe8b6f2b.min.js.map b/assets/javascripts/bundle.fe8b6f2b.min.js.map new file mode 100644 index 0000000000..82635852ae --- /dev/null +++ b/assets/javascripts/bundle.fe8b6f2b.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/clipboard/dist/clipboard.js", "node_modules/escape-html/index.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/rxjs/node_modules/tslib/tslib.es6.js", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an +

+ +
+

Features

+
    +
  • Deploys Argo CD by default, so you can manage your entire lab using files in open source git repos
  • +
  • Argo CD ships with a dashboard with a custom theme 💙
  • +
  • Specializes in using Bitwarden (though not required) to store sensitive values both in your Bitwwarden vault, and on your cluster as Secrets.
  • +
  • Manages all your authentication needs centrally using Zitadel and Vouch 💪
  • +
  • Supports initialization on a range of common self-hosted apps 📱
  • +
  • featured initialized apps such as Zitadel, Nextcloud, Matrix, and Home Assistant include [b]backups and restores[/b]!
  • +
  • Lots o' docs
  • +
+

Getting Started

+

Please see our Getting Started guide.

+

Under the hood

+

Note: this project is not officially affiliated with any of the below tooling or applications. We just love open source projects 💙

+

Supported k8s distributions

+

We always install the latest version of Kubernetes that is available from the distro's startup script.

+ + + + + + + + + + + + + + + + + + + + + +
DistroDescription

k3s
The certified Kubernetes distribution built for IoT & Edge computing

k3d
K3d is k3s in Docker 🐳.
In beta!

KinD
kind is a tool for running local Kubernetes clusters using Docker container “nodes”. kind was primarily designed for testing Kubernetes itself.
+

We tend to test first on k3s first, then the other distros.

+

Default Installed Applications

+

Version is the helm chart version, or manifest version. See the Default Applications tab for more info on each application.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ApplicationDescriptionInitialization Supported
metallb logo, blue arrow pointing up, with small line on one leg of arrow to show balance
metallb
Loadbalancer and IP Address pool manager for metal
nginx logo, white letter N with green background
ingress-nginx
The ingress-nginx controller allows access to the cluster remotely, needed for web traffic
cert manager logo
cert-manager
For SSL/TLS certificates
argo CD logo, an organer squid wearing a fishbowl helmet
Argo CD
Gitops - Continuous Deployment
argo CD logo, an organer squid wearing a fishbowl helmet
Argo CD Appset Secret Plugin
Gitops - Continuous Deployment
ESO logo, outline of robot with astricks in a screen in it's belly
ESO
external-secrets-operator integrates external secret management systems like Bitwarden or GitLab
ESO logo, again
Bitwarden ESO Provider
Bitwarden external-secrets-operator provider
Zitadel logo, an orange arrow pointing left
ZITADEL
An identity provider and OIDC provider to provide SSO
Vouch logo, the letter V in rainbow
Vouch
Vouch proxy allows you to secure web pages that lack authentication e.g. prometheus
Prometheus logo, a torch
Prometheus Stack
Prometheus monitoring and logging stack using loki/promtail, alert manager, and grafana
+

Minor Notes:

+
+

All Default Applications can be disabled through your ~/.config/smol-k8s-lab/config.yaml file, except Argo CD. You can still choose not to install it, but if not installed, smol-k8s-lab will only install: metallb, nginx-ingress, and cert-manager

+
+

Optionally Installed Applications

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Application/ToolDescriptionInitialization Supported
cilium logo
Ciliumdemo
Kubernetes netflow visualizer and policy editor
home assistant logo, which is a small blue house with three white tracers inside of it, making it appear as though the home is a circuit board
Home Assistant
Home Assistant, a self hosted, at home IoT management solution.
kyvero logo
Kyvernoalpha
Kubernetes native policy management to enforce policies on k8s resources
kepler logo
kepler
Kepler (Kubernetes Efficient Power Level Exporter) uses eBPF to probe energy-related system stats and exports them as Prometheus metrics.
k8up logo, a minimalist logo of a small blue hill with line starting the right going into the hill
k8up
Backups operator using restic to backup to s3 endpoints
k8tz logo, the k8s logo but with a watch in the center instead of the ship wheel
k8tz
Timezone environment variable injector for pods and cronjobs
netmaker logo, a purple letter N
Netmaker
Netmaker is a self hosted vpn management tool that uses Wiregaurd®
nextcloud logo, 3 white circles touching eachother on a blue background
Nextcloud
Nextcloud is a self hosted file server
Mastodon logo, a white M in a purple chat bubble
Mastodon
Mastodon is a self hosted federated social media network
Matrix logo
matrix
Matrix is a self hosted chat platform
minio logo, a minimalist drawing in red of a crane
minio
Self hosted S3 Object Store operator
seaweedfs logo,
seaweedfs
Self hosted S3 Object Store
k9s logo, outline of dog with ship wheels for eyesk9sTerminal based dashboard for kubernetes
+

Status

+

smol-k8s-lab is actively maintained, and in a semi-stable state. We still may introduce features that, upon major version releases, can introduce breaking changes, but we'll always include how to update your config files in the merged pull request description, and that will be linked in the release notes.

+

Development

+

smol-k8s-lab is written in Python 3.12 and built and published using Poetry. You can check out the pyproject.toml for the versions of each library we install below.

+

Core libraries

+

These are installed anytime you install smol-k8s-lab as an end user:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Default LibraryDescription
bcryptto pass a password to argocd and automatically update your Bitwarden
clickhandles arguments for the CLI
kubernetesfor using the partially functional python sdk for kubernetes
miniofor connecting to s3 and saving credentials
pyfigletuses figlet to print the ascii text banner in the tui
pyjwtused for processing tokens from zitadel
pyyamlthis is actively being removed in favor of ruamel.yaml
richmakes all the pretty formatted text in logs and --help
textualthis is the framework used for writing the TUI
ruamel.yamlto handle the k8s yamls and configs while maintaining comments)
xdg-base-dirslets us use default config and cache directories for storage accross major OSes
+

Development libraries

+

These are installed anytime you want to develop smol-k8s-lab:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Development LibraryDescription
mkdocs-materialfor the docs site
mkdocs-videofor videos on the docs site
deptryfor purging unused libraries
textual-devfor consoling textual
pytest-textual-snapshotfor taking screenshots with textual
poethepoetfor running special tasks during poetry build
coqui-ttsfor generating text to speech audio files
pydubfor converting audio files from wav to mp3, requires ffmpeg
pygamefor playing audio accross different OSes, requires alsa on linux
+

We also utilize the Bitwarden cli, for a password manager so you never have to see/know your Argo CD password.

+

Things we don't handle (yet)

+
    +
  1. +

    Port Forwarding

    +

    If you want to access an app outside of port forwarding to test, you'll need to make sure your app's ingress is setup correctly and then you'll need to setup your router to port forward 80->80 and 443->443 for your WAN. Then, setup DNS for your domain if you want the wider internet to access this remotely.

    +
  2. +
  3. +

    High-Availability

    +

    HA cluster design with K3s requires etcd or another external key-value store such as PostgreSQL. Smol-K8s-Lab deploys k3s in a single-node configuration using SQLite which can be used for multi-node configurations but is not suitable for high-availability.

    +
  4. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/installation/index.html b/installation/index.html new file mode 100644 index 0000000000..0996520d5e --- /dev/null +++ b/installation/index.html @@ -0,0 +1,2488 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Installation - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Installation

+ +
+

Note

+

smol-k8s-lab is only tested on Debian, Ubuntu, and macOS. It may run on other Linux distros and even WSL, but we do not actively test them at this time.

+
+

Install via brew

+

brew is the preferred installation method for macOS/Debian/Ubuntu, as this will also install any prerequisites you need.

+
1
+2
# tap the special homebrew repo for our formula and then install
+brew install small-hack/tap/smol-k8s-lab
+
+

Then you should be able to check the version and cli options with:

+
1
smol-k8s-lab --help
+
+

Install via pipx

+

Prerequisites

+

Required

+

smol-k8s-lab cannot function without at least the following installed:

+ +

Optional

+

All of these are not Required for core functionality of smol-k8s-lab, but they greatly enhance the experience, so they are still recommended.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
applicationdescription
dockerneeded for k3d, kind, and installing the mastodon app
bwonly if you want to use Bitwarden to store your passwords
k3donly if you want to use k3d
k9sonly if you want a k8s TUI for viewing an already installed cluster
kindonly if you want to use kind
mconly if you want smol-k8s-lab to create MinIO users and buckets for you
[restic]only if you want smol-k8s-lab to restore backups for you for select apps
+

pipx

+

smol-k8s-lab requires Python 3.11+ (and pipx). If you've already got both and other pre-reqs, you should be able to:

+
1
+2
+3
+4
+5
# install the CLI
+pipx install smol-k8s-lab
+
+# Check the help menu before proceeding
+smol-k8s-lab --help
+
+
+ Help text example + + + Output of smol-k8s-lab --help after cloning the directory and installing the prerequisites. + + +
+ +

Usage

+

Initialization

+

After you've followed the installation instructions, if you're new to smol-k8s-lab, initialize a new config file by running:

+
1
+2
+3
# we'll walk you through any configuration needed
+# before saving the config and deploying it for you
+smol-k8s-lab
+
+
+

Upgrading to v1.x, v2.x, v3.x

+ +If you've installed smol-k8s-lab prior to v3.0.0, please backup your old configuration, ~/.config/smol-k8s-lab/config.yaml (or $XDG_CONFIG_HOME/smol-k8s-lab/config.yaml), and then remove the file entirely. Then, run the following if you're using pip: + +
1
+2
+3
+4
+5
# this upgrades smol-k8s-lab
+pip install --upgrade smol-k8s-lab
+
+# this initializes a new configuration
+smol-k8s-lab
+
+ +if you're using pipx: +
1
+2
+3
+4
+5
# this upgrades smol-k8s-lab
+pipx upgrade smol-k8s-lab
+
+# this initializes a new configuration
+smol-k8s-lab
+
+ +For details on exactly what's changed, please check out the release notes in the GitHub Releases. + +
+ +

Creating a new config without running smol-k8s-lab

+

This is helpful if you just want to take a look at the default configuration before installing any Kubernetes distros. This will also allow you to disable any default applications you'd like ahead of time.

+
1
+2
+3
+4
+5
+6
+7
+8
# create the needed directory if you haven't already, NOTE: this can also be in $XDG_CONFIG_HOME/smol-k8s-lab/config.yaml
+mkdir -p ~/.config/smol-k8s-lab
+
+# download the default config file
+curl -o config.yaml https://raw.githubusercontent.com/small-hack/smol-k8s-lab/main/smol_k8s_lab/config/default_config.yaml
+
+# move the config file to the config directory (can also be $XDG_CONFIG_HOME/smol-k8s-lab/config.yaml)
+mv config.yaml ~/.config/smol-k8s-lab/config.yaml
+
+

You can now use your text editor of choice to view and edit the default config before running smol-k8s-lab :)

+

Configuration

+

You can checkout the default config file as well as our config file docs.

+

We also highly recommend checking out the TUI (Terminal User Interface) for you to jump right in :)

+

Finally, for more info on applications we install, checkout default apps.

+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/javascripts/mathjax.js b/javascripts/mathjax.js new file mode 100644 index 0000000000..a7adf3f406 --- /dev/null +++ b/javascripts/mathjax.js @@ -0,0 +1,16 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } + }; + + document$.subscribe(() => { + MathJax.typesetPromise() + }) diff --git a/k8s_apps/apps/index.html b/k8s_apps/apps/index.html new file mode 100644 index 0000000000..9e8256d765 --- /dev/null +++ b/k8s_apps/apps/index.html @@ -0,0 +1,2174 @@ + + + + + + + + + + + + + + + + + + + + + Apps - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Apps

+ +

beepboop

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/appset-secret-plugin/index.html b/k8s_apps/appset-secret-plugin/index.html new file mode 100644 index 0000000000..1befee3b69 --- /dev/null +++ b/k8s_apps/appset-secret-plugin/index.html @@ -0,0 +1,2196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Appset Secret Plugin - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/argocd/index.html b/k8s_apps/argocd/index.html new file mode 100644 index 0000000000..d1b59f0933 --- /dev/null +++ b/k8s_apps/argocd/index.html @@ -0,0 +1,2365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Argo CD App - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Argo CD App

+ +

Argo CD exclusively powers most of the applications that smol-k8s-lab supports. It should be installed by default if you use the default configuration.

+

Default Argo CD configuration

+

Argo CD is one of the most complex applications we deploy for you. We follow this procedure:

+
    +
  1. Install Argo CD first with helm using some bare minimum options that include setting up your initial admin password. + The reason we set up a password for you instead of letting Argo CD generate it for you, is so that we can store it in your password manager for later use.
  2. +
  3. Deploy the appset-secret-plugin.
  4. +
  5. Optionally deploy an OIDC provider (Zitadel)
  6. +
  7. Create an Argo CD Application for Argo to manage itself
  8. +
+

The final Application will be sourced from small-hack/argocd-apps/argocd, which you can learn more about its readme.

+

+screenshot of the Argo CD Application viewed through the Argo CD web interface in tree mode. It shows Argo CD as the parent application and then 3 child applications: appset-secret-plugin (an app), argocd-bitwarden-eso (an appset), and argocd-web-app-set (an appset). +

+

ApplicationSets

+

We make heavy use of Argo CD ApplicationSets in order to utilize generators, specifically we use a Plugin Generator called appset-secret-plugin to store variables in Kubernetes Secrets that can be passed to Argo CD ApplicationSets. This is particularly useful for data such as a specific hostname or timezone for an application.

+

Default yaml configuration

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
apps:
+  argo_cd:
+    # Set to false if you you just want a bare cluster with only the above apps"
+    enabled: true
+    description: |
+      [link=https://argo-cd.readthedocs.io/en/stable/]Argo CD[/link] is a declarative, GitOps continuous delivery tool for Kubernetes.
+
+      smol-k8s-lab installs Argo CD with helm initially to support initial configuration of your admin user and disabling of dex. After your OIDC provider is configured, Argo CD begins managing itself using the below configured Argo CD repo.
+
+      The Appset Secret Plugin is required if you want to use the default [link="https://github.com/small-hack/argocd-apps"]small-hack/argocd-apps[/link] [gold3]argo.repo[/gold3] and default enabled if Argo CD is enabled, so we can create a k8s Secret with your more private info such as hostnames, IP addresses, and emails in a deployment that runs alongside Argo CD to provide Argo CD ApplicationSets This plugin has no ingress and cannot be reached from outside the cluster.
+
+      To disable Appset Secret Plugin, please set directory recursion to false.
+
+      Learn more: [link=https://github.com/small-hack/appset-secret-plugin]https://github.com/small-hack/appset-secret-plugin[/link]
+    init:
+      enabled: true
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys:
+        # FQDN hostname for accessing the Argo CD web interface
+        hostname: "argocd.example.com"
+        # which oidc provider to use for Argo CD: defaults to Zitadel
+        oidc_provider: "zitadel"
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      # change to argocd/argocd to not use app of apps with secret plugin
+      path: "argocd/app_of_apps/"
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "argocd"
+      # recurse directories in the provided git repo, if true, we also deploy the appset secret plugin
+      directory_recursion: true
+      # source repos for Argo CD argo-cd Project (in addition to argo_cd.argo.repo)
+      project:
+        # you can change this project name :)
+        name: argo-cd
+        source_repos:
+          - https://argoproj.github.io/argo-helm
+          - https://small-hack.github.io/appset-secret-plugin
+        destination:
+          # automatically includes argocd's namespace, so you don't need to specify it here
+          namespaces:
+            - prometheus
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/bitwarden_eso_provider/index.html b/k8s_apps/bitwarden_eso_provider/index.html new file mode 100644 index 0000000000..5d1b012e96 --- /dev/null +++ b/k8s_apps/bitwarden_eso_provider/index.html @@ -0,0 +1,2358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Bitwarden ESO Provider - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Bitwarden ESO Provider

+ +

We use the Bitwarden ESO Provider along side the external-secrets-operator to pull secret data from your Bitwarden vault, into the cluster as Kubernetes Secrets.

+

+a screenshot of the Argo CD web interface showing the bitwarden-eso-provider application in tree view mode. it shows the following children of bitwarden-eso-provider: test-connection configmap, bitwarden-eso-provider service,bitwarden-eso-provider service account, bitwarden-eso-provider deployment, bitwarden-fields cluster secret store, bitwarden-login cluster secret store. the deployment then points to additonal replica sets which point to a single pod +

+

smol-k8s-lab stores any sensitive user specific data about applications in your Bitwarden vault. Some examples include admin credentials, database credentials, and OIDC credentials.

+

Head over to the Bitarden ESO Provider helm chart to learn more, and then see how it is configured at small-hack/argocd-apps.

+

Sensitive values

+

To use the Bitwarden provider, we need your Client Secret, Client ID, and your Bitwarden password. You can choose to provide these as one time values each time your run smol-k8s-lab, or you can export the following environment variables before running smol-k8s-lab:

+
    +
  • BITWARDEN_PASSWORD
  • +
  • BITWARDEN_CLIENTSECRET
  • +
  • BITWARDEN_CLIENTID
  • +
+

default yaml configuration

+

For Bitwarden, if you'd like to deploy it alongside the External Secrets Operator, you just need to set your provider for the apps_global_config.external_secrets paramter to "bitwarden" in your config.yaml. Make sure that apps.external_secrets_operator.enabled is also set to true.

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
apps_global_config:
+  # Must be a string of "" (don't use external secrets) or "bitwarden" to use bitwarden for external secrets*
+  external_secrets: "bitwarden"
+
+apps:
+  external_secrets_operator:
+    enabled: true
+    description: |
+      [link=https://external-secrets.io/latest/]External Secrets Operator[/link] is a Kubernetes operator that integrates external secret management systems like HashiCorp Vault, CyberArk Conjur, Bitwarden, Gitlab, and many more. The operator reads information from external APIs and automatically injects the values into a Kubernetes Secret.
+
+      To deploy the Bitwarden provider, please set apps_global_config.external_secrets to "bitwarden".
+
+      The [link="https://github.com/small-hack/bitwarden-eso-provider/"]Bitwarden External Secrets Provider[/link] is used to store k8s secrets in Bitwarden®. This deployment has no ingress and can't be connected to from outside the cluster. There is a networkPolicy that only allows the pod to communicate with the External Secrets Operator in the same namespaces.
+
+      smol-k8s-lab support initialization by creating a Kubernetes secret with your Bitwarden credentials so that the provider can unlock your vault. You will need to setup an [link=https://bitwarden.com/help/personal-api-key/]API key[/link] ahead of time. You can pass these credentials in by setting the following environment variables:
+
+      BITWARDEN_PASSWORD, BITWARDEN_CLIENTSECRET, BITWARDEN_CLIENTID
+    # Initialization of the app through smol-k8s-lab
+    init:
+      enabled: false
+    argo:
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      # change to external-secrets-operator/external-secrets-operator/ to deploy
+      # ONLY the external-secrets-operator, so this will not use app of apps and
+      # therefore we will not deploy the Bitwarden ESO provider. Use if you want to use
+      # a different provider
+      path: external-secrets-operator/app_of_apps/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: external-secrets
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # secret keys to provide for the Argo CD Appset secret plugin, none by default
+      secret_keys: {}
+      # source repos for Argo CD App Project (in addition to app.argo.repo)
+      project:
+        name: external-secrets-operator
+        source_repos:
+          - https://charts.external-secrets.io
+          # you can remove this one if you're not using bitwarden to store your k8s secrets
+          - https://small-hack.github.io/bitwarden-eso-provider
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces: []
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/cert_manager/index.html b/k8s_apps/cert_manager/index.html new file mode 100644 index 0000000000..a1937ecb4f --- /dev/null +++ b/k8s_apps/cert_manager/index.html @@ -0,0 +1,2509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Cert Manager - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Cert Manager

+ +

We use cert-manager to generate TLS certs for the web frontends of any apps we install.

+

+Argo CD web interface screenshot of cert manager in tree view mode showing cert-manager-helm-chart with three of its children. The screenshot does not show the entire Argo CD application because it contains well over 10 different roles and cluster roles and does not fit on one page, so instead we've chosen to show only the deployment children which are cert-manager, cert-manager-caininjector, and cert-manager-webhook each with their own replicasets and pods. +

+

By default, we create two ClusterIssuers using the HTTP01 challenge solver:

+
    +
  • letsencrypt-staging
  • +
  • letsencrypt-prod
  • +
+

All applications will use letsencrypt-staging by default, until you change this setting via the TUI or config file. We default to the staging server, because letsencrypt-prod has very tight rate limiting and when testing, as one does in a lab, you can easily exceed this, which can issue you a ban for at least a week.

+

Alternatively, you can also use the DNS01 challenge solver with cloudflare only. If you'd like to use a different DNS provider for the DNS01 challenge solver type, please submit a PR as the devs only have regular access to cloudflare and can't test other providers at this time.

+

Example configs

+

HTTP01 Challenge Solver

+

This is the default challenge solver.

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
apps:
+  # This app is installed with helm or manifests depending on what is recommended
+  # for your k8s distro. Becomes managed by Argo CD if you enable it below
+  cert_manager:
+    # ! NOTE: you currently can't set this to false. It is necessary to deploy
+    # most of our supported Argo CD apps since they often have TLS enabled either
+    # for pod connectivity or ingress. IF set to false, you need an alternative SSL pipeline
+    enabled: true
+    description: |
+      [link=https://cert-manager.io/]cert-manager[/link] let's you use LetsEncrypt to generate TLS certs for all your apps with ingress.
+
+      smol-k8s-lab supports optional initialization by creating [link=https://cert-manager.io/docs/configuration/acme/]ACME Issuer type[/link] [link=https://cert-manager.io/docs/concepts/issuer/]ClusterIssuers[/link] using either the HTTP01 or DNS01 challenge solvers. We create two ClusterIssuers: letsencrypt-staging and letsencrypt-staging.
+
+      For the DNS01 challange solver, you will need to either export $CLOUDFLARE_API_TOKEN as an env var, or fill in the sensitive value for it each time you run smol-k8s-lab.
+
+      Currently, Cloudflare is the only supported DNS provider for the DNS01 challenge solver. If you'd like to use a different DNS provider or use a different Issuer type all together, please either set one up outside of smol-k8s-lab. We also welcome [link=https://github.com/small-hack/smol-k8s-lab/pulls]PRs[/link] to add these features :)
+
+    # Initialize of the app through smol-k8s-lab
+    init:
+      # Deploys staging and prod ClusterIssuers and prompts you for
+      # values if they were not set. Switch to false if you don't want
+      # to deploy any ClusterIssuers
+      enabled: true
+      values:
+        # Used for to generate certs and alert you if they're going to expire
+        email: coolfriend@amazingdogs.dog
+        # choose between "http01" or "dns01"
+        cluster_issuer_acme_challenge_solver: http01
+        # only needed if cluster_issuer_challenge_solver set to dns01
+        # currently only cloudflare is supported
+        cluster_issuer_acme_dns01_provider: cloudflare
+    argo:
+      secret_keys: {}
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "cert-manager/"
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "cert-manager"
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for cert-manager CD App Project (in addition to argo.repo)
+      project:
+        name: cert-manager
+        source_repos:
+          - https://charts.jetstack.io
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces:
+            - kube-system
+
+

DNS01 Challenge Solver

+

For the DNS01 challange solver, you will need to either export $CERT_MANAGER_CLOUDFLARE_API_TOKEN as an env var, or fill in the sensitive value for it each time you run smol-k8s-lab.

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
apps:
+  # This app is installed with helm or manifests depending on what is recommended
+  # for your k8s distro. Becomes managed by Argo CD if you enable it below
+  cert_manager:
+    # ! NOTE: you currently can't set this to false. It is necessary to deploy
+    # most of our supported Argo CD apps since they often have TLS enabled either
+    # for pod connectivity or ingress. IF set to false, you need an alternative SSL pipeline
+    enabled: true
+    description: |
+      [link=https://cert-manager.io/]cert-manager[/link] let's you use LetsEncrypt to generate TLS certs for all your apps with ingress.
+
+      smol-k8s-lab supports optional initialization by creating [link=https://cert-manager.io/docs/configuration/acme/]ACME Issuer type[/link] [link=https://cert-manager.io/docs/concepts/issuer/]ClusterIssuers[/link] using either the HTTP01 or DNS01 challenge solvers. We create two ClusterIssuers: letsencrypt-staging and letsencrypt-staging.
+
+      For the DNS01 challange solver, you will need to either export $CLOUDFLARE_API_TOKEN as an env var, or fill in the sensitive value for it each time you run smol-k8s-lab.
+
+      Currently, Cloudflare is the only supported DNS provider for the DNS01 challenge solver. If you'd like to use a different DNS provider or use a different Issuer type all together, please either set one up outside of smol-k8s-lab. We also welcome [link=https://github.com/small-hack/smol-k8s-lab/pulls]PRs[/link] to add these features :)
+
+    # Initialize of the app through smol-k8s-lab
+    init:
+      # Deploys staging and prod ClusterIssuers and prompts you for
+      # values if they were not set. Switch to false if you don't want
+      # to deploy any ClusterIssuers
+      enabled: true
+      values:
+        # Used for to generate certs and alert you if they're going to expire
+        email: coolfriend@amazingdogs.dog
+        # choose between "http01" or "dns01"
+        cluster_issuer_acme_challenge_solver: dns01
+        # only needed if cluster_issuer_challenge_solver set to dns01
+        # currently only cloudflare is supported
+        cluster_issuer_acme_dns01_provider: cloudflare
+        # you can remove this if you're not using cloudflare as your DNS01 provider
+        cloudflare_api_token:
+          value_from:
+            env: CERT_MANAGER_CLOUDFLARE_API_TOKEN
+    argo:
+      secret_keys: {}
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "cert-manager/"
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "cert-manager"
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for cert-manager CD App Project (in addition to argo.repo)
+      project:
+        name: cert-manager
+        source_repos:
+          - https://charts.jetstack.io
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces:
+            - kube-system
+
+

Troubleshooting

+

Follow the steps in the cert-manager common error troubleshooting guide), you can also change the letsencrypt-staging value to letsencrypt-prod for any domains you own and can configure to point to your cluster via DNS.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/cnpg_operator/index.html b/k8s_apps/cnpg_operator/index.html new file mode 100644 index 0000000000..993f144391 --- /dev/null +++ b/k8s_apps/cnpg_operator/index.html @@ -0,0 +1,2300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + CloudNative Postgress Operator - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

CloudNative Postgress Operator

+ +

We use the Cloud Native PostgeSQL Operator to create postgresql clusters and manage backups to S3.

+

+Screenshot of Argo CD's web interface showing the CNPG Operator Application in tree view mode. This includes configmap s for monitoring and manager config, webhook-service, cnpg-validating-webhook-config, backups CRD, clusters CRD, poolers CRD, scheduledBackups CRD, operator deployment, and 3 cluster roles. the cnpg-webhook-service is branching to the cnpg-webhook-service endpoint. The cnpg-validating-webhook-config is branching to an endpoint slice of the same name. the deployment has two children: cnpg-webhook-cert and cnpg-operator replicaset. the replicaset feeds into a single pod called cnpg-operator +

+

In the CloudNative PostgeSQL Operator Backups for S3 are done to local s3 endpoints consistently and to a configurable remote endpoint.

+

Example yaml config

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
apps:
+  cnpg_operator:
+    description: |
+      CloudNative PostgeSQL Operator for Kubernetes. This lets you create and
+      manage many clusters of postgresql, including backups to s3.
+    enabled: true
+    argo:
+      # secret keys to provide for the argocd secret plugin app, none by default
+      secret_keys: {}
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      path: postgres/operators/cloud-native-postgres/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: cnpg-system
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: cnpg-operator
+        source_repos:
+        - https://github.com/small-hack/argocd-apps
+        - https://cloudnative-pg.github.io/charts
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces: []
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/core_dns/index.html b/k8s_apps/core_dns/index.html new file mode 100644 index 0000000000..d215a71197 --- /dev/null +++ b/k8s_apps/core_dns/index.html @@ -0,0 +1,2282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Core DNS - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Core DNS

+ +

Coredns ships by default with k3s, so it gets installed, but not really by anything we do by default 😅

+

Troubleshooting networking issues with coredns

+

Can your pod not get out to the internet? Well, first verify that it isn't the entire cluster with this: +

1
kubectl run -it --rm --image=infoblox/dnstools:latest dnstools
+
+

Check the /etc/resolv.conf and /etc/hosts that's been provided by coredns from that pod with: +

1
+2
+3
+4
+5
+6
cat /etc/resolv.conf
+cat /etc/hosts
+
+# also check if this returns linuxfoundation's info correct
+# cross check this with a computer that can hit linuxfoundation.org with no issues
+host linuxfoundation.org
+
+

If it doesn't return linuxfoundation.org's info, you should first go read this k3s issue (yes, it's present in KIND as well).

+

Then decide, "does having subdomains on my LAN spark joy?"

+

Yes it sparks joy

+

And then update your ndot option in your /etc/resolv.conf for podDNS to be 1. You can do this in a deployment. You should read this k8s doc to learn more. The search domain being more than 1-2 dots deep seems to cause all sorts of problems. You can test the resolv.conf with the infoblox/dnstools docker image from above. It already has the vi text editor, which will allow you to quickly iterate.

+

No, it does not spark joy

+

STOP USING MULTIPLE SUBDOMAINS ON YOUR LOCAL ROUTER. Get a pihole and use it for both DNS and DHCP. Message brought to you by two engineers who lost a day to troubleshooting this.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/experimental/cilium/index.html b/k8s_apps/experimental/cilium/index.html new file mode 100644 index 0000000000..3d0b9a82ec --- /dev/null +++ b/k8s_apps/experimental/cilium/index.html @@ -0,0 +1,2263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Cilium - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Cilium

+ +

cilium is an open source, cloud native solution for providing, securing, and observing network connectivity between workloads, fueled by the revolutionary Kernel technology eBPF.

+

Learn more about our cilium Argo CD ApplicationSet.

+

This application is still in an alpha state with smol-k8s-lab, but you can get started using it by just providing a hostname in the config file like this:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
apps:
+  # This app is installed with helm or manifests depending on what is recommended
+  # for your k8s distro. Becomes managed by ArgoCD if you enable it below
+  cilium:
+    enabled: false
+    description: |
+      Cilium is an open source, cloud native solution for providing, securing, and observing network connectivity between workloads, fueled by the revolutionary Kernel technology eBPF.
+
+      Learn more: [link=https://cilium.io/]https://cilium.io/[/link]
+    # Initialize of the app through smol-k8s-lab
+    init:
+      enabled: true
+    argo:
+      secret_keys:
+        hostname: "cilium.cooldogsontheinternet.com"
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "demo/cilium/"
+      # either the branch or tag to point at in the argo repo above
+      revision: "main"
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "cilium"
+      # source repos for Argo CD cilium Project
+      project:
+        name: cilium
+        source_repos:
+          - "https://helm.cilium.io/"
+        destination:
+          namespaces:
+            - cilium
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/experimental/infisical/index.html b/k8s_apps/experimental/infisical/index.html new file mode 100644 index 0000000000..a1a33d2d09 --- /dev/null +++ b/k8s_apps/experimental/infisical/index.html @@ -0,0 +1,2303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Infisical - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Infisical

+ +

Infisical is a SOC2 Type 2 Certified company that makes Infisical, an end-to-end platform to securely manage secrets and configs across your team and infrastructure, which is our most likely candidate for recommendation for a self-hosted FOSS alternative to Hashicorp's Vault.

+

smol-k8s-lab will support Infisical as a default application in the future after Infisical/infisical#873 or a similar initial user feature is available.

+

In the meantime, feel free to checkout out our first shot at an Infisical Argo CD ApplicationSet, but note that you need to manually set up a first user.

+

Example config

+

Here's an example config for Infisical:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
apps:
+  infisical:
+    enabled: false
+    description: |
+      ⚠️ [magenta]Alpha Status[/magenta]
+
+      Infisical is an open-source, end-to-end encrypted secret management platform that enables teams to easily manage and sync their env vars.
+
+      Learn more: [link=https://infisical.com/]https://infisical.com/[/link]
+    # Initialization of the app through smol-k8s-lab
+    init:
+      enabled: true
+    argo:
+      secret_keys:
+        hostname: "k8svault.cooldogs.net"
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "demo/infisical/"
+      # either the branch or tag to point at in the argo repo above
+      ref: "main"
+      # namespace to install the k8s app in
+      namespace: "infisical"
+      # source repos for Argo CD App Project (in addition to app.argo.repo)
+      project:
+        name: infisical
+        source_repos:
+          - "registry-1.docker.io"
+          - "https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/"
+        destination:
+          namespaces: []
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/experimental/kepler/index.html b/k8s_apps/experimental/kepler/index.html new file mode 100644 index 0000000000..ffbca35eea --- /dev/null +++ b/k8s_apps/experimental/kepler/index.html @@ -0,0 +1,2300 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Kepler - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Kepler

+ +

Kepler (Kubernetes-based Efficient Power Level Exporter) is a newly added and supported k8s app that uses eBPF to probe energy-related system stats and exports them as Prometheus metrics.

+

This app is still in alpha state as we learn more about how best to configure it. In the meantime, to our knowledge you can start playing with it after installing it alongside cilium.

+

You can also check out our Kepler Argo CD Application.

+

Example Configuration

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
apps:
+  kepler:
+    description: |
+      [link=https://github.com/sustainable-computing-io/kepler]Kepler[/link] (Kubernetes Efficient Power Level Exporter) uses eBPF to probe energy-related system stats and exports them as Prometheus metrics.
+    enabled: false
+    # Initialization of the app through smol-k8s-lab
+    init:
+      enabled: false
+    argo:
+      # secret keys to provide for the argocd secret plugin app, none by default
+      secret_keys: {}
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      path: demo/kepler/
+      # either the branch or tag to point at in the argo repo above
+      revision: "main"
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: kepler
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        source_repos:
+        - https://sustainable-computing-io.github.io/kepler-helm-chart
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces: []
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/experimental/kubevirt/index.html b/k8s_apps/experimental/kubevirt/index.html new file mode 100644 index 0000000000..d066748117 --- /dev/null +++ b/k8s_apps/experimental/kubevirt/index.html @@ -0,0 +1,2384 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Kubevirt - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Kubevirt

+

Kubevirt allows users to run QEMU virtual machines inside of Kubernetes.

+

Installation

+ +

Components

+

Kubevirt is made up of several pieces:

+
    +
  1. +

    Kubervirt Operator

    +

    The operator controls virtual machine instances and provides the CRDs that define them +

    +
  2. +
  3. +

    Kubevirt CDI

    +

    The Containerized Data Importer can pull virtual machine images, ISO files, and other types of bootable media from sources like S3, HTTP, or OCI images. This data is then written to PVCs which are mounted as disks. For examples of various ways to use the CDI, see the notes in the argocd-apps repo

    +
  4. +
  5. +

    Kubevirt Manager

    +

    This is a community-developed web-ui which allows users to create, manage, and interact with virtual machines running in Kubevirt. See their official docs at kubevirt-manager.io

    +
  6. +
+

+ + Screenshot showing the default page of Kubevirt-manager. The screen is devided into 2 sections. On the left, there is a vertical navigation tab with a grey background. The options in this bar are Dashboard, Virtual Machines, VM Pools, Auto Scaling, Nodes, Data Volumes, Instance Types, and Load Balancers.  On the right, there is a grid of blue rectangular icons each representing one of the option in the navigation tab, but with an icon and text representing metrics about that option. + +

+ +

Utilities

+
    +
  1. +

    libvirt-clients

    +

    This utility will audit a host machine and report what virtualisation capabilities are available

    +
      +
    • +

      Installation

      +
      1
      sudo apt-get install -y libvirt-clients
      +
      +
    • +
    • +

      Usage

      +
      1
      +2
      +3
      +4
      +5
      +6
      $ virt-host-validate qemu
      +QEMU: Checking for hardware virtualization          : PASS
      +QEMU: Checking if device /dev/kvm exists            : PASS
      +QEMU: Checking if device /dev/kvm is accessible     : PASS
      +QEMU: Checking if device /dev/vhost-net exists      : PASS
      +QEMU: Checking if device /dev/net/tun exists        : PASS
      +
      +
    • +
    +
  2. +
  3. +

    virtctl

    +

    virtctl is the command-line utility for managing Kubevirt resources. It can be installed as a standalone CLI or as a Kubectl plugin via krew.

    +
      +
    • +

      Standalone

      +
      1
      +2
      export VERSION=v0.41.0
      +wget https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/virtctl-${VERSION}-linux-amd64
      +
      +
    • +
    • +

      Plugin

      +
      1
      kubectl krew install virt
      +
      +
    • +
    +
  4. +
+

Uninstall

+

In the event that Kubevirt does not uninstall gracefully, you may need to perform the following steps:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
export RELEASE=v0.17.0
+
+# --wait=true should anyway be default
+kubectl delete -n kubevirt kubevirt kubevirt --wait=true
+
+# this needs to be deleted to avoid stuck terminating namespaces
+kubectl delete apiservices v1.subresources.kubevirt.io
+
+# not blocking but would be left over
+kubectl delete mutatingwebhookconfigurations virt-api-mutator
+
+# not blocking but would be left over
+kubectl delete validatingwebhookconfigurations virt-operator-validator
+
+# not blocking but would be left over
+kubectl delete validatingwebhookconfigurations virt-api-validator
+
+kubectl delete -f https://github.com/kubevirt/kubevirt/releases/download/${RELEASE}/kubevirt-operator.yaml --wait=false
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/experimental/kyverno/index.html b/k8s_apps/experimental/kyverno/index.html new file mode 100644 index 0000000000..f63a878b47 --- /dev/null +++ b/k8s_apps/experimental/kyverno/index.html @@ -0,0 +1,2199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Kyverno - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Kyverno

+ +

Kyverno is a policy engine designed for Kubernetes. Policies are managed as Kubernetes resources and no new language is required to write policies. This allows using familiar tools such as kubectl, git, and kustomize to manage policies. Kyverno policies can validate, mutate, generate, and cleanup Kubernetes resources, and verify image signatures and artifacts to help secure the software supply chain. The Kyverno CLI can be used to test policies and validate resources as part of a CI/CD pipeline.

+

Kyverno is still in alpha status at smol-k8s-lab, but here's what we've got so far:

+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/experimental/longhorn/index.html b/k8s_apps/experimental/longhorn/index.html new file mode 100644 index 0000000000..7b7ce9b881 --- /dev/null +++ b/k8s_apps/experimental/longhorn/index.html @@ -0,0 +1,2325 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Longhorn - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Longhorn

+ +

Longhorn is a Cloud native distributed block storage for Kubernetes.

+

Example Config

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
apps:
+  longhorn:
+    description: |
+      🐮 [link=https://longhorn.io/]Longhorn[/link] is a Cloud native distributed block storage for Kubernetes.
+    enabled: false
+    argo:
+      # secret keys to provide for the argocd secret plugin app, none by default
+      secret_keys: {}
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      path: demo/longhorn/helm/
+      # either the branch or tag to point at in the argo repo above
+      revision: "main"
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: longhorn-system
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: longhorn
+        source_repos:
+          - https://charts.longhorn.io
+          - https://github.com/longhorn/longhorn
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces: []
+
+

Troubleshooting

+

Uninstalling

+

Uninstalling through the Argo CD web interface often doesn't cut it. Try going through the docs to uninstall longhorn.

+

If you have issues deleting a namespace with longhorn, try these steps here.

+

We also found this issue useful: longhorn/longhorn#5319

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/experimental/minio/index.html b/k8s_apps/experimental/minio/index.html new file mode 100644 index 0000000000..485717f2cc --- /dev/null +++ b/k8s_apps/experimental/minio/index.html @@ -0,0 +1,2302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + MinIO - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

MinIO

+ +

MinIO is a high-performance, S3 compatible object store. It is built for large scale AI/ML, data lake and database workloads. It is software-defined and runs on any cloud or on-premises infrastructure. MinIO is dual-licensed under open source GNU AGPL v3 and a commercial enterprise license. We at smol-k8s-lab use only the AGPLv3 stuff :)

+

We currently consider MinIO to be in a demo state, but to launch it, you'll need to decide between the operator/tenant helm charts, or the vanilla helm chart.

+

Check out our MinIO Argo CD Applications.

+

Example config for vanilla helm chart

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
apps:
+  minio:
+    enabled: true
+    description: |
+      MinIO®️ is a high-performance, S3 compatible object store.
+
+      MinIO is dual-licensed under open source GNU AGPL v3 and a commercial enterprise license.
+
+      learn more: [link=https://min.io/]https://min.io/[/link]
+    argo:
+      # secrets keys to make available to ArgoCD ApplicationSets
+      secret_keys:
+        admin_console_hostname: "objectstore.dogpics.biz"
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "minio/vanilla/"
+      # either the branch or tag to point at in the argo repo above
+      revision: "main"
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "minio"
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: minio
+        source_repos:
+          - https://github.com/small-hack/argocd-apps
+        destination:
+          namespaces:
+            - argocd
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/experimental/postgres_operator/index.html b/k8s_apps/experimental/postgres_operator/index.html new file mode 100644 index 0000000000..18d6ac8645 --- /dev/null +++ b/k8s_apps/experimental/postgres_operator/index.html @@ -0,0 +1,2331 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Zalando Postgress Operator - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Zalando Postgress Operator

+ +

We are experimenting with the Zalando PostgeSQL Operator to create postgresql clusters and manage backups to S3. Our main interest here is that they support major version backups. Our main concern is the mutual TLS support.

+

In the PostgeSQL Operator, backups for S3 are done to local s3 endpoints consistently and to a configurable remote endpoint. You can see more in our Zalando Postgres Operator Argo CD Application.

+

Example yaml config

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
apps:
+  postgres_operator:
+    description: |
+      ⚠️ [magenta][i]demo[/i] status[/magenta]
+
+      postgres-operator is a Kubernetes operator for postgresql by Zalando.
+
+      smol-k8s-lab supports initialization by setting up your ingress hostnames, and then also creating a local s3 endpoint exclusively for backups with and additional configurable endpoint for backups to an external s3
+    enabled: true
+    init:
+      enabled: true
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys:
+        # FQDN to use for postgres operator web interface
+        hostname: "postgres-ui.boopthesnoot.cute"
+        s3_endpoint: "postgres-s3.boopthesnoot.cute"
+        s3_bucket: zalando-postgres-operator
+        s3_region: eu-west-1
+        backup_schedule: 30 18 * * *
+        backup_retention_time: 8 weeks
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important! This
+      # is an app of apps. Change to "monitoring/kube-prometheus-stack/" to
+      # only install kube-prometheus-stack (foregoing loki and push gateway)
+      path: postgres/operators/zalando/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: postgres-operator
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: postgres-operator
+        source_repos:
+        - https://opensource.zalando.com/postgres-operator/charts/postgres-operator
+        - https://opensource.zalando.com/postgres-operator/charts/postgres-operator-ui
+        - https://seaweedfs.github.io/seaweedfs/helm
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces:
+          - postgres-operator
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/external-secrets-operator/index.html b/k8s_apps/external-secrets-operator/index.html new file mode 100644 index 0000000000..92d87c2594 --- /dev/null +++ b/k8s_apps/external-secrets-operator/index.html @@ -0,0 +1,2342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + External Secrets Operator - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

External Secrets Operator

+ +

The External Secrets Operator (abbreviated as ESO) is a Kubernetes operator that integrates external secret management systems like AWS Secrets Manager, HashiCorp Vault, Google Secrets Manager, Azure Key Vault, IBM Cloud Secrets Manager, CyberArk Conjur and many more. The operator reads information from external APIs and automatically injects the values into a Kubernetes Secret.

+

The goal of External Secrets Operator is to synchronize secrets from external APIs into Kubernetes. ESO is a collection of custom API resources - ExternalSecret, SecretStore and ClusterSecretStore that provide a user-friendly abstraction for the external API that stores and manages the lifecycle of the secrets for you.

+

+a screenshot of the Argo CD web interface showing the External Secrets Operator app of apps which shows two child apps: external-secrets-operator-helm and external-secrets-provider-appset. the external-secrets-provider-appset has one child called bitwarden-provider-app +

+

smol-k8s-lab default makes heavy use of ESO in conjunction with the Bitwarden ESO Provider to ensure no credentials or sensitive data is stored as plain text in our git repos or in any helm values we provide. We accomplish this goal by always biasing towards using Kubernetes Secrets as sources of truth for helm charts, and those secrets come from Bitwarden by default.

+

Check out our ESO Argo CD ApplicationSet.

+

Default yaml configuration

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
apps_global_config:
+  # Must be a string of "" (don't use external secrets) or "bitwarden" to use bitwarden for external secrets*
+  external_secrets: "bitwarden"
+
+apps:
+  external_secrets_operator:
+    enabled: true
+    description: |
+      [link=https://external-secrets.io/latest/]External Secrets Operator[/link] is a Kubernetes operator that integrates external secret management systems like HashiCorp Vault, CyberArk Conjur, Bitwarden, Gitlab, and many more. The operator reads information from external APIs and automatically injects the values into a Kubernetes Secret.
+
+      To deploy the Bitwarden provider, please set apps_global_config.external_secrets to "bitwarden".
+
+      The [link="https://github.com/small-hack/bitwarden-eso-provider/"]Bitwarden External Secrets Provider[/link] is used to store k8s secrets in Bitwarden®. This deployment has no ingress and can't be connected to from outside the cluster. There is a networkPolicy that only allows the pod to communicate with the External Secrets Operator in the same namespaces.
+
+      smol-k8s-lab support initialization by creating a Kubernetes secret with your Bitwarden credentials so that the provider can unlock your vault. You will need to setup an [link=https://bitwarden.com/help/personal-api-key/]API key[/link] ahead of time. You can pass these credentials in by setting the following environment variables:
+
+      BITWARDEN_PASSWORD, BITWARDEN_CLIENTSECRET, BITWARDEN_CLIENTID
+    # Initialization of the app through smol-k8s-lab
+    init:
+      enabled: false
+    argo:
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      # change to external-secrets-operator/external-secrets-operator/ to deploy
+      # ONLY the external-secrets-operator, so this will not use app of apps and
+      # therefore we will not deploy the Bitwarden ESO provider. Use if you want to use
+      # a different provider
+      path: external-secrets-operator/app_of_apps/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: external-secrets
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # secret keys to provide for the Argo CD Appset secret plugin, none by default
+      secret_keys: {}
+      # source repos for Argo CD App Project (in addition to app.argo.repo)
+      project:
+        name: external-secrets-operator
+        source_repos:
+          - https://charts.external-secrets.io
+          # you can remove this one if you're not using bitwarden to store your k8s secrets
+          - https://small-hack.github.io/bitwarden-eso-provider
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces: []
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/generic_app/index.html b/k8s_apps/generic_app/index.html new file mode 100644 index 0000000000..1ee5f441ff --- /dev/null +++ b/k8s_apps/generic_app/index.html @@ -0,0 +1,2577 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Generic App - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Generic App

+ +

Sometimes you need a very generic app quickly to test something. This Argo CD ApplicationSet does just that: creates a basic deployment for you to use for testing a docker image that has no helm chart.

+

We have a few options in our Argo CD ApplicationSets collection:

+ +

Example configurations

+

Using a deployment with a custom app

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
apps:
+  generic_app:
+    enabled: false
+    description: |
+      A generic Argo CD ApplicationSet using a generic app helm chart:
+      [link=https://github.com/small-hack/generic-app-helm]https://github.com/small-hack/generic-app-helm[/link]
+
+      You can also use this as a template and change the name of the app to your own app name.
+    argo:
+      secret_keys:
+        # the name of the release, namespace, and project for the argocd app
+        app_name: "generic-app"
+        # change only if you need to use another image registry instead of docker.io
+        image_registry: "docker.io"
+        # change this to the image repo you want to deploy
+        image_repo: "nginx"
+        # change this the image tag you want to deploy
+        image_tag: "latest"
+      repo: https://github.com/small-hack/argocd-apps
+      path: generic-app/
+      revision: main
+      # you could change this to your app name
+      namespace: generic-app
+      directory_recursion: false
+      project:
+        # you could change this to your app name
+        name: generic-app
+        source_repos:
+          - https://small-hack.github.io/generic-app-helm
+        destination:
+          # you could change this to your app name
+          namespaces:
+            - generic-app
+
+

deployment with ingress

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
apps:
+  generic_app:
+    enabled: false
+    description: |
+      A generic Argo CD ApplicationSet using a generic app helm chart:
+      [link=https://github.com/small-hack/generic-app-helm]https://github.com/small-hack/generic-app-helm[/link]
+
+      You can also use this as a template and change the name of the app to your own app name.
+    argo:
+      secret_keys:
+        # the name of the release, namespace, and project for the argocd app
+        app_name: "generic-app"
+        # change only if you need to use another image registry instead of docker.io
+        image_registry: "docker.io"
+        # change this to the image repo you want to deploy
+        image_repo: "nginx"
+        # change this the image tag you want to deploy
+        image_tag: "latest"
+        # the hostname you want to use for this app
+        hostname: cooldogsonline.biz
+      repo: https://github.com/small-hack/argocd-apps
+      path: generic-app/deployment-ingress/
+      revision: main
+      # you could change this to your app name
+      namespace: generic-app
+      directory_recursion: false
+      project:
+        # you could change this to your app name
+        name: generic-app
+        source_repos:
+          - https://small-hack.github.io/generic-app-helm
+        destination:
+          # you could change this to your app name
+          namespaces:
+            - generic-app
+
+

job AND deployment

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
apps:
+  generic_app:
+    enabled: false
+    description: |
+      A generic Argo CD ApplicationSet using a generic app helm chart:
+      [link=https://github.com/small-hack/generic-app-helm]https://github.com/small-hack/generic-app-helm[/link]
+
+      You can also use this as a template and change the name of the app to your own app name.
+    argo:
+      secret_keys:
+        # the name of the release, namespace, and project for the argocd app
+        app_name: "generic-app"
+        # change only if you need to use another image registry instead of docker.io
+        image_registry: "docker.io"
+        # change this to the image repo you want to deploy
+        image_repo: "nginx"
+        # change this the image tag you want to deploy
+        image_tag: "latest"
+        # change only if you need to use another image registry instead of docker.io
+        job_image_registry: "docker.io"
+        # change this to the image repo you want to deploy
+        job_image_repo: "nginx"
+        # change this the image tag you want to deploy
+        job_image_tag: "latest"
+      repo: https://github.com/small-hack/argocd-apps
+      path: generic-app/job/
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # you could change this to your app name
+      namespace: generic-app
+      directory_recursion: false
+      project:
+        # you could change this to your app name
+        name: generic-app
+        source_repos:
+          - https://small-hack.github.io/generic-app-helm
+        destination:
+          # you could change this to your app name
+          namespaces:
+            - generic-app
+
+

job instead of a deployment

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
apps:
+  generic_app:
+    enabled: false
+    description: |
+      A generic Argo CD ApplicationSet using a generic app helm chart:
+      [link=https://github.com/small-hack/generic-app-helm]https://github.com/small-hack/generic-app-helm[/link]
+
+      You can also use this as a template and change the name of the app to your own app name.
+    argo:
+      secret_keys:
+        # the name of the release, namespace, and project for the argocd app
+        app_name: "generic-app"
+        # change only if you need to use another image registry instead of docker.io
+        job_image_registry: "docker.io"
+        # change this to the image repo you want to deploy
+        job_image_repo: "nginx"
+        # change this the image tag you want to deploy
+        job_image_tag: "latest"
+      repo: https://github.com/small-hack/argocd-apps
+      path: generic-app/job/
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # you could change this to your app name
+      namespace: generic-app
+      directory_recursion: false
+      project:
+        # you could change this to your app name
+        name: generic-app
+        source_repos:
+          - https://small-hack.github.io/generic-app-helm
+        destination:
+          # you could change this to your app name
+          namespaces:
+            - generic-app
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/generic_device_plugin/index.html b/k8s_apps/generic_device_plugin/index.html new file mode 100644 index 0000000000..aaa7bbcbc5 --- /dev/null +++ b/k8s_apps/generic_device_plugin/index.html @@ -0,0 +1,2274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Generic Device Plugin - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Generic Device Plugin

+ +

You'll need the Generic Device Plugin as a prereq in order to use USB devices with home assistant or any other app on k8s, so we provide a basic Argo CD app for that :)

+

Example config

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
apps:
+  generic_device_plugin:
+    enabled: true
+    description: |
+      This installs the [link=https://github.com/squat/generic-device-plugin/tree/main]squat/generic-device-plugin[/link], which is recommended for exposing generic devices such as USB devices to your k8s pods. This can useful if you have an IoT coordinator device such as the conbee 2 that you are using with deconz or home assistant. You can read more about device plugins in the [link=https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/]Kubernetes docs[/link]
+    argo:
+      secret_keys: {}
+      repo: https://github.com/small-hack/argocd-apps
+      path: generic-device-plugin/
+      revision: main
+      namespace: kube-system
+      directory_recursion: false
+      project:
+        name: generic-device-plugin
+        source_repos:
+          - https://github.com/squat/generic-device-plugin
+        destination:
+          namespaces:
+            - kube-system
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/home_assistant/index.html b/k8s_apps/home_assistant/index.html new file mode 100644 index 0000000000..5a0513fc00 --- /dev/null +++ b/k8s_apps/home_assistant/index.html @@ -0,0 +1,2761 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Home Assistant - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Home Assistant

+ +

Home Assistant is an open source IoT management solution. We deploy a small-hack maintained helm chart by default, which allows us to:

+
    +
  • specify a default configuration.yaml
  • +
  • create an initial user to disable public registration
  • +
+

When deployed via Argo CD using smol-k8s-lab, we create an app of apps like this:

+

+screenshot of the home-assistant-app in Argo CD showing a tree featuring home assistant themes config map, home assistant appset, home assistant bitwarden external secrets operator appset, and home assistant PVC appset. +

+
+ Home Assistant Argo CD helm chart app + + + screenshot of the home-assistant-app in Argo CD showing a tree featuring a configmap, service, service account, deployment, and ingress resource all called home-assistant. + + +
+ +
+

Note

+

You'll need to enable the Generic Device Plugin as a prereq in order to use USB devices with home assistant.

+
+

Secret Keys

+

The main variables you need to worry about when setting up home assistant is your hostname. The rest are the same values you'd need for a default home assistant configuration.yaml:

+
    +
  • name
  • +
  • country
  • +
  • currency
  • +
  • unit_system
  • +
  • temperature_unit
  • +
  • latitude
  • +
  • longitude
  • +
  • elevation
  • +
+

Sensitive Values

+

Sensitive values can be provided via environment variables using a value_from map on any value under init.values or backups. Example of providing the SMTP password:

+
1
+2
+3
+4
+5
+6
+7
apps:
+  home_assistant:
+    init:
+      values:
+        password:
+          value_from:
+            env: HA_PASSWORD
+
+

Sensitive values before v5.0.0

+

smol-k8s-lab did not originally support the value_from map. If you're using a version before v5.0.0, to avoid having to provide sensitive values every time you run smol-k8s-lab with home assistant enabled, set up the following environment variables:

+
1
+2
# this is for the admin user password
+export HOME_ASSISTANT_PASSWORD="mysupersecretpassword"
+
+

Backups

+

Backups are a new feature in v5.0.0 that enable backing up your cluster via restic to a configurable remote S3 bucket. If you have init.enabled set to true and you're using our pre-configured argo.repo, we support both instant backups, and scheduled backups.

+

To use the backups feature, you'll need to configure the values below.

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
apps:
+  home_assistant:
+    backups:
+      # cronjob syntax schedule to run home assistant pvc backups
+      pvc_schedule: 45 23 * * *
+      s3:
+        # these are for pushing remote backups of your local s3 storage, for speed and cost optimization
+        endpoint: s3.eu-central-003.backblazeb2.com
+        bucket: my-home-assistant-bucket
+        region: eu-central-003
+        secret_access_key:
+          value_from:
+            env: HOME_ASSISTANT_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            env: HOME_ASSISTANT_S3_BACKUP_ACCESS_ID
+      restic_repo_password:
+        value_from:
+          env: HOME_ASSISTANT_RESTIC_REPO_PASSWORD
+
+

Restores

+

Restores are a new feature in v5.0.0 that enable restoring your PVCs via restic from a configurable remote S3 bucket. If you have init.enabled set to true and you're using our pre-configured argo.repo, we support restoring your PVC. A restore is a kind of initialization process, so it lives under the init section of the config for your application, in this case, home-assistant. Here's an example:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
apps:
+  home-assistant:
+    init:
+      enabled: true
+      restore:
+        # set this to false to disable restore
+        enabled: true
+        restic_snapshot_ids:
+          # defaults to latest, but can be changed to any restic snapshot ID
+          home_assistant: latest
+
+

Example configs

+

Using tolerations and node affinity

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
apps:
+  home-assistant:
+    description: |
+      [link=https://home-assistant.io]Home Assistant[/link] is a home IOT management solution.
+
+      By default, we assume you want to use node affinity and tolerations to keep home assistant pods on certain nodes and keep other pods off said nodes. If you don't want to use either of these features but still want to use the small-hack/argocd-apps repo, first change the argo path to /home-assistant/ and then remove the 'toleration_' and 'affinity' secret_keys from the yaml file under apps.home_assistant.description.
+
+      [b]NOTE[/b]: If you want to pass in a USB device, you will need the generic device plugin (which is available as a default Argo CD app via smol-k8s-lab 💙)
+
+      This app takes one sensitive value, password for the initial owner user. You can also pass it in as an enviornment variable called $HOME_ASSISTANT_PASSWORD.
+    # Initialization of the app through smol-k8s-lab
+    init:
+      # enable the creation of an initial owner user
+      enabled: true
+      restore:
+        enabled: false
+        restic_snapshot_ids:
+          home_assistant: latest
+      values:
+        # -- owner user's name
+        name: "admin"
+        # -- owner user's username
+        user_name: "admin"
+        # -- owner user's language, default is english
+        language: "en"
+        password:
+          value_from:
+            env: HOME_ASSISTANT_PASSWORD
+    backups:
+      # cronjob syntax schedule to run home assistant pvc backups
+      pvc_schedule: 45 23 * * *
+      s3:
+        # these are for pushing remote backups of your local s3 storage, for speed and cost optimization
+        endpoint: s3.eu-central-003.backblazeb2.com
+        bucket: my-home-assistant-bucket
+        region: eu-central-003
+        secret_access_key:
+          value_from:
+            env: HOME_ASSISTANT_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            env: HOME_ASSISTANT_S3_BACKUP_ACCESS_ID
+      restic_repo_password:
+        value_from:
+          env: HOME_ASSISTANT_RESTIC_REPO_PASSWORD
+    argo:
+      secret_keys:
+        hostname: ""
+        # name of your home assistant area, users often just use "home"
+        name: "home"
+        # default alpha-2 country code, default is NL which is The Netherlands
+        country: "NL"
+        # currency code to use for calculating costs, defaults to EUR for euro
+        currency: "EUR"
+        # other option is "imperial"
+        unit_system: "metric"
+        # set to F for USA imperialist tempurature
+        temperature_unit: "C"
+        # latitude of your personal coordinates
+        latitude: ""
+        # longitude of your personal coordinates
+        longitude: ""
+        # the elevation of your house?
+        elevation: "1"
+        # you can delete these if you're not using tolerations/affinity
+        toleration_key: "reserved"
+        toleration_operator: "Equal"
+        toleration_value: "iot"
+        toleration_effect: "NoSchedule"
+        # these are for node affinity, delete if not in use
+        affinity_key: "reserved"
+        affinity_value: "iot"
+        # these are for passing in a USB device such as the conbee II
+        usb_device_path: "/dev/serial/by-id/usb-device-here"
+        usb_device_mount_path: "/dev/ttyACM0"
+        usb_device_index: "1"
+        # these are for passing in a bluetooth device
+        bluetooth_device_path: /run/dbus
+        bluetooth_device_mount_path: /run/dbus
+        bluetooth_device_index: '2'
+      repo: https://github.com/small-hack/argocd-apps
+      path: home-assistant/toleration_and_affinity/
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      namespace: home-assistant
+      directory_recursion: false
+      project:
+        name: home-assistant
+        source_repos:
+          - https://small-hack.github.io/home-assistant-chart
+        destination:
+          namespaces:
+            - argocd
+
+

Without tolerations and node affinity

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
apps:
+  home_assistant:
+    enabled: true
+    description: |
+      [link=https://home-assistant.io]Home Assistant[/link] is a home IOT management solution.
+    init:
+      enabled: true
+      restore:
+        enabled: false
+        restic_snapshot_ids:
+          home_assistant: latest
+      values:
+        # -- owner user's name
+        name: "admin"
+        # -- owner user's username
+        user_name: "admin"
+        # -- owner user's language, default is english
+        language: "en"
+        password:
+          value_from:
+            env: HOME_ASSISTANT_PASSWORD
+    backups:
+      # cronjob syntax schedule to run home assistant pvc backups
+      pvc_schedule: 45 23 * * *
+      s3:
+        # these are for pushing remote backups of your local s3 storage, for speed and cost optimization
+        endpoint: s3.eu-central-003.backblazeb2.com
+        bucket: my-home-assistant-bucket
+        region: eu-central-003
+        secret_access_key:
+          value_from:
+            env: HOME_ASSISTANT_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            env: HOME_ASSISTANT_S3_BACKUP_ACCESS_ID
+      restic_repo_password:
+        value_from:
+          env: HOME_ASSISTANT_RESTIC_REPO_PASSWORD
+    argo:
+      secret_keys:
+        hostname: ""
+        # name of your home assistant area, users often just use "home"
+        name: "home"
+        # default alpha-2 country code, default is NL which is The Netherlands
+        country: "NL"
+        # currency code to use for calculating costs, defaults to EUR for euro
+        currency: "EUR"
+        # other option is "imperial"
+        unit_system: "metric"
+        # set to F for USA imperialist tempurature
+        temperature_unit: "C"
+        # latitude of your personal coordinates
+        latitude: ""
+        # longitude of your personal coordinates
+        longitude: ""
+        # the elevation of your house?
+        elevation: ""
+      repo: https://github.com/small-hack/argocd-apps
+      path: home-assistant/
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      namespace: home-assistant
+      directory_recursion: false
+      project:
+        name: home-assistant
+        source_repos:
+          - https://small-hack.github.io/home-assistant-chart
+        destination:
+          namespaces:
+            - argocd
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/index.html b/k8s_apps/index.html new file mode 100644 index 0000000000..899b34fe4a --- /dev/null +++ b/k8s_apps/index.html @@ -0,0 +1,2332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Overview - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Overview

+ +

Default Installed Applications

+

Version is the helm chart version, or manifest version. See the Default Applications tab for more info on each application.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ApplicationDescriptionInitialization Supported
metallb logo, blue arrow pointing up, with small line on one leg of arrow to show balance
metallb
Loadbalancer and IP Address pool manager for metal
nginx logo, white letter N with green background
ingress-nginx
The ingress-nginx controller allows access to the cluster remotely, needed for web traffic
cert manager logo
cert-manager
For SSL/TLS certificates
argo CD logo, an organer squid wearing a fishbowl helmet
Argo CD
Gitops - Continuous Deployment
argo CD logo, an organer squid wearing a fishbowl helmet
Argo CD Appset Secret Plugin
Gitops - Continuous Deployment
ESO logo, outline of robot with astricks in a screen in it's belly
ESO
external-secrets-operator integrates external secret management systems like Bitwarden or GitLab
ESO logo, again
Bitwarden ESO Provider
Bitwarden external-secrets-operator provider
Zitadel logo, an orange arrow pointing left
ZITADEL
An identity provider and OIDC provider to provide SSO
Vouch logo, the letter V in rainbow
Vouch
Vouch proxy allows you to secure web pages that lack authentication e.g. prometheus
Prometheus logo, a torch
Prometheus Stack
Prometheus monitoring and logging stack using loki/promtail, alert manager, and grafana
+

Minor Notes:

+
+

All Default Applications can be disabled through your ~/.config/smol-k8s-lab/config.yaml file, except Argo CD. You can still choose not to install it, but if not installed, smol-k8s-lab will only install: metallb, nginx-ingress, and cert-manager

+
+

Optionally Installed Applications

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Application/ToolDescriptionInitialization Supported
cilium logo
Ciliumdemo
Kubernetes netflow visualizer and policy editor
home assistant logo, which is a small blue house with three white tracers inside of it, making it appear as though the home is a circuit board
Home Assistant
Home Assistant, a self hosted, at home IoT management solution.
kyvero logo
Kyvernoalpha
Kubernetes native policy management to enforce policies on k8s resources
kepler logo
kepler
Kepler (Kubernetes Efficient Power Level Exporter) uses eBPF to probe energy-related system stats and exports them as Prometheus metrics.
k8up logo, a minimalist logo of a small blue hill with line starting the right going into the hill
k8up
Backups operator using [restic] to backup to s3 endpoints
k8tz logo, the k8s logo but with a watch in the center instead of the ship wheel
k8tz
Timezone environment variable injector for pods and cronjobs
netmaker logo, a purple letter N
Netmaker
Netmaker is a self hosted vpn management tool
nextcloud logo, 3 white circles touching eachother on a blue background
Nextcloud
Nextcloud is a self hosted file server
Mastodon logo, a white M in a purple chat bubble
Mastodon
Mastodon is a self hosted federated social media network
Matrix logo
matrix
Matrix is a self hosted chat platform
minio logo, a minimalist drawing in red of a crane
minio
Self hosted S3 Object Store operator
seaweedfs logo,
seaweedfs
Self hosted S3 Object Store
+

There are plenty more on the side bar, and you can even add your own :)

+ + +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/ingress_nginx/index.html b/k8s_apps/ingress_nginx/index.html new file mode 100644 index 0000000000..d4c44840a6 --- /dev/null +++ b/k8s_apps/ingress_nginx/index.html @@ -0,0 +1,2202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Ingress Nginx - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Ingress Nginx

+ +

ingress-nginx needs no introduction, but that won't stop us! ingress-nginx is an Ingress controller for Kubernetes using NGINX as a reverse proxy and load balancer. We use it, instead of traefik, because we know nginx, you know nginx, and none of us have time to learn traefik (but that could change in the future 🤷).

+

+screenshot of the Argo CD web interface for the ingress-nginx-helm application in tree view mode. it's children include a configmap, service, and deployment all named ingress-nginx-controller, and then a service account, cluster role, cluster role binding, and role binding all called ingress-nginx. There's a lot of children, so bare with me. There's also a service called ingress-nginx-controller-admission, a validating web hook configuration called ingress-nginx-admission, and an ingress class called nginx. the deployment has two replica sets as children with one of them having a single pod as it's child. Sorry to those using screenreaders having to digest this. +

+

smol-k8s-lab will install ingress-nginx by default with no special options. If you're using kind, we install it initially via manifests, and if you're using k3d/k3s, we initially install it via helm.

+

No matter the distro, Argo CD takes over managing ingress-nginx using our ingress Argo CD Application which also bundles cert manager.

+
+

Tip

+

Do not confuse ingress-nginx with nginx-ingress. They are confusingly named, but ingress-nginx is a project by Kubernetes. nginx-ingress is a project by NGINX. They're both Ingress controllers for Kubernetes, but the latter has paid features and the former does not. Googling for docs is a bit awful 🤦

+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/k8tz/index.html b/k8s_apps/k8tz/index.html new file mode 100644 index 0000000000..1910645b23 --- /dev/null +++ b/k8s_apps/k8tz/index.html @@ -0,0 +1,2308 @@ + + + + + + + + + + + + + + + + + + + + + + + + + K8tz - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

K8tz

+ +

k8tz exists because try as we might, time is still hard. It is a Kubernetes admission controller and a CLI tool to inject timezones into Pods and CronJobs.

+

Containers do not inherit timezones from host machines and only access the clock from the kernel. The default timezone for most images is UTC, yet it is not guaranteed and may be different from container to container. With k8tz it is easy to standardize selected timezone across pods and namespaces automatically with minimal effort.

+

smol-k8s-lab uses this to ensure your cronjobs and your backups are all in the same timezone, so that if you have any special cronjobs that need to run as a specific time in a specific timezone, you can rest assured they will actually run at that time.

+

Please trust us when we say that you very likely want k8tz if you have non-standard backup processes for something like Nextcloud.

+

smol-k8s-lab requires only one variable for our default k8tz Argo CD Application: timezone, which should be a timezone from the TZ database (in the wikipedia list, you want the second column, TZ Identifier).

+

example config

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
apps:
+  k8tz:
+    enabled: false
+    description: |
+      k8tz is a kubernetes admission controller and a CLI tool to inject timezones into Pods and CronJobs.
+
+      Containers do not inherit timezones from host machines and have only access to the clock from the kernel. The default timezone for most images is UTC, yet it is not guaranteed and may be different from container to container. With k8tz it is easy to standardize selected timezone across pods and namespaces automatically with minimal effort.
+
+      You can find your timezone identifier here: [link=https://wikipedia.org/wiki/List_of_tz_database_time_zones#List]https://wikipedia.org/wiki/List_of_tz_database_time_zones[/link]
+
+      Learn more: [link=https://github.com/k8tz/k8tz]https://github.com/k8tz/k8tz[/link]
+    init:
+      enabled: true
+    argo:
+      secret_keys:
+        timezone: "Europe/Amsterdam"
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "k8tz/"
+      # either the branch or tag to point at in the argo repo above
+      revision: "main"
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "k8tz"
+      # source repos for Argo CD App Project (in addition to app.argo.repo)
+      project:
+        name: k8tz
+        source_repos:
+          - "https://k8tz.github.io/k8tz/"
+        destination:
+          namespaces:
+            - argocd
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/k8up/index.html b/k8s_apps/k8up/index.html new file mode 100644 index 0000000000..17136b87fe --- /dev/null +++ b/k8s_apps/k8up/index.html @@ -0,0 +1,2325 @@ + + + + + + + + + + + + + + + + + + + + + + + + + K8up - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

K8up

+ +

K8up is a Kubernetes app that utilizes Restic to create backups of persistent volume claims to object stores like S3, MinIO, and Backblaze B2.

+

smol-k8s-lab optionally installs K8up as one of it's supported Kubernetes applications using Argo CD repo with K8up template.

+

+screenshot of the Argo CD web interface showing the k8up app of apps in tree view mode with two children: k8up-crd and k8up-helm-appset. k8up-helm-appset has one child: k8up-helm-release +

+

One of the most important template values we require for our default Argo CD ApplicationSet is timezone, which should be a timezone from the TZ database (in the wikipedia list, you want the second column, TZ Identifier).

+

API Docs

+

The full API docs are here.

+ +

Example config

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
apps:
+  k8up:
+    enabled: true
+    description: |
+      K8up ([i]pronounced "ketchup?"[/]) is a Kubernetes Operator based on Restic for backups of Persistent Volumes in k8s into S3 compatible storage like MinIO. Backs up all PVCs marked as ReadWriteMany, ReadWriteOnce or with a specific label. Can also perform "Application Aware" backups, containing the output of any tool capable of writing to stdout.
+
+      You can also perform individual, on-demand backups, and restores from the k8up CLI tool.
+      You can find your timezone identifier here: [link=https://wikipedia.org/wiki/List_of_tz_database_time_zones#List]https://wikipedia.org/wiki/List_of_tz_database_time_zones[/link]
+
+      Learn more: [link=https://k8up.io]https://k8up.io[/link]
+    init:
+      enabled: true
+    argo:
+      secret_keys:
+        timezone: "Europe/Amsterdam"
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "k8up/"
+      # either the branch or tag to point at in the argo repo above
+      revision: "main"
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "k8up"
+      # source repos for Argo CD App Project (in addition to app.argo.repo)
+      project:
+        name: k8up
+        source_repos:
+          - "https://k8up-io.github.io/k8up"
+          - "https://github.com/k8up-io/k8up.git"
+        destination:
+          namespaces:
+            - argocd
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/keycloak/index.html b/k8s_apps/keycloak/index.html new file mode 100644 index 0000000000..57decb997b --- /dev/null +++ b/k8s_apps/keycloak/index.html @@ -0,0 +1,2249 @@ + + + + + + + + + + + + + + + + + + + + + Keycloak - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Keycloak

+ +

We've disabled keycloak at this time because we don't have the time to maintain it and it's harder to use than zitadel.

+

If you still want to use it though, it would be something like this:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
apps:
+  keycloak:
+    enabled: false
+    description: |
+      keycloak is an IAM provider that you can use with ArgoCD for user/group management and oauth2
+      smol-k8s-lab initializes keycloak by creating an initial user & clients for ArgoCD and vouch this will also prompt you for input for creating an admin user. Switch to initialization to false if you want to use your own argo repo that does not not use the appset_secret_plugin or setup an initial user/clients
+    init:
+      enabled: true
+      values:
+        # first human user to setup
+        username: ""
+        first_name: ""
+        last_name: ""
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys:
+        hostname: ""
+        mail_hostname: ""
+        default_realm: "default"
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "demo/keycloak/"
+      # either the branch or tag to point at in the argo repo above
+      revision: "main"
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "keycloak"
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: keycloak
+        source_repos:
+          - "registry-1.docker.io"
+        destination:
+          namespaces:
+            - argocd
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/mastodon/index.html b/k8s_apps/mastodon/index.html new file mode 100644 index 0000000000..0627845fbc --- /dev/null +++ b/k8s_apps/mastodon/index.html @@ -0,0 +1,2506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Mastodon - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Mastodon

+ +

Mastodon is a Free and Open Source social media networking platform based on ActivityPub.

+

We are mostly stable for running Mastodon on Kubernetes. Check out our Mastodon Argo CD ApplicationSet:

+

+screenshot of the mastodon applicationset in Argo CD's web interface using the tree mode view. the main mastodon app has 6 child apps: mastodon-redis, mastodon-app-set with child mastodon-web-app, mastodon-external-secrets-appset with child mastodon-external-secrets, mastodon-postgres-app-set with child mastodon-postgres-cluster, mastodon-s3-provider-app-set with child mastodon-seaweedfs, and mastodon-s3-pvc-appset with child mastodon-s3-pvc. +

+

This is the networking view in Argo CD:

+

+screenshot of the mastodon applicationset in Argo CD's web interface using the networking tree mode view. it shows the flow of cloud to ip address to mastodon-web-app ingress to two services mastodon-web-app-streaming and mastodon-web-app-web which each go to their respective pods. There's also additional services and pods outside of that flow. pods masotdon-web-app-media and masotdon-web-app-sidekiq have no children. 2 elastic search services have the same elastic search pod child. and then there's an additional 3 matching elastic search service and pod pairs +

+

Required Init Values

+

To use the default smol-k8s-lab Argo CD Application, you'll need to provide one time init values for:

+
    +
  • admin_user
  • +
  • admin_email
  • +
  • smtp_user
  • +
  • smtp_host
  • +
+

Required ApplicationSet Values

+

And you'll also need to provide the following values to be templated for your personal installation:

+
    +
  • hostname - the hostname for your web interface
  • +
+

Required Sensitive Values

+

If you'd like to setup SMTP, we need a bit more sensitive data. This includes your SMTP password, S3 backup credentials, and restic repo password.

+

You have two options. You can:

+
    +
  • respond to a one-time prompt for these credentials (one-time per cluster)
  • +
  • export an environment variable
  • +
+

Environment Variables

+

You can export the following env vars and we'll use them for your sensitive data:

+
    +
  • MASTODON_SMTP_PASSWORD
  • +
  • MASTODON_S3_BACKUP_ACCESS_ID
  • +
  • MASTODON_S3_BACKUP_SECRET_KEY
  • +
  • MASTODON_RESTIC_REPO_PASSWORD
  • +
+

Example Config

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
apps:
+  mastodon:
+    description: |
+       [link=https://joinmastodon.org/]Mastodon[/link] is an open source self hosted social media network.
+
+       smol-k8s-lab supports initializing mastodon, by setting up your hostname, SMTP credentials, redis credentials, postgresql credentials, and an admin user credentials. We pass all credentials as secrets in the namespace and optionally save them to Bitwarden.
+
+       smol-k8s-lab also creates a local s3 endpoint and as well as S3 bucket and credentials if you enable set mastodon.argo.secret_keys.s3_provider to "minio" or "seaweedfs". Both seaweedfs and minio require you to specify a remote s3 endpoint, bucket, region, and accessID/secretKey so that we can make sure you have remote backups.
+
+       To provide sensitive values via environment variables to smol-k8s-lab use:
+         - MASTODON_SMTP_PASSWORD
+         - MASTODON_S3_BACKUP_ACCESS_ID
+         - MASTODON_S3_BACKUP_SECRET_KEY
+         - MASTODON_RESTIC_REPO_PASSWORD
+    enabled: false
+    init:
+      enabled: true
+      restore:
+        enabled: false
+        cnpg_restore: true
+        restic_snapshot_ids:
+          seaweedfs_volume: latest
+          seaweedfs_filer: latest
+      values:
+        # admin user
+        admin_user: "tootadmin"
+        # admin user's email
+        admin_email: ""
+        # mail server to send verification and notification emails
+        smtp_host: "change@me-to-enable.mail"
+        # mail user for smtp host
+        smtp_user: "change me to enable mail"
+        smtp_password:
+          value_from:
+            env: MASTODON_SMTP_PASSWORD
+    backups:
+      # cronjob syntax schedule to run mastodon pvc backups
+      pvc_schedule: 10 0 * * *
+      # cronjob syntax (with SECONDS field) for mastodon postgres backups
+      # must happen at least 10 minutes before pvc backups, to avoid corruption
+      # due to missing files. This is because the backup shows as completed before
+      # it actually is
+      postgres_schedule: 0 0 0 * * *
+      s3:
+        # these are for pushing remote backups of your local s3 storage, for speed and cost optimization
+        endpoint: s3.eu-central-003.backblazeb2.com
+        bucket: my-mastodon-backups
+        region: eu-central-003
+        secret_access_key:
+          value_from:
+            env: MASTODON_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            env: MASTODON_S3_BACKUP_ACCESS_ID
+        restic_repo_password:
+          value_from:
+            env: MASTODON_RESTIC_REPO_PASSWORD
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys:
+        admin_user: tootadmin
+        # hostname that users go to in the browser
+        hostname: ""
+        # set the local s3 provider for mastodon's public data in one bucket
+        # and private database backups in another. can be minio or seaweedfs
+        s3_provider: seaweedfs
+        # how large the backing pvc's capacity should be for minio or seaweedfs
+        s3_pvc_capacity: 120Gi
+        # local s3 endpoint for postgresql backups, backed up constantly
+        s3_endpoint: ""
+        s3_region: eu-west-1
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      path: mastodon/small-hack/app_of_apps/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: mastodon
+      # recurse directories in the git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: mastodon
+        # depending on if you use seaweedfs or minio, you can remove the other source repo
+        source_repos:
+          - registry-1.docker.io
+          - https://small-hack.github.io/cloudnative-pg-cluster-chart
+          - https://operator.min.io/
+          - https://seaweedfs.github.io/seaweedfs/helm
+          - https://small-hack.github.io/mastodon-helm-chart
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces: []
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/matrix/index.html b/k8s_apps/matrix/index.html new file mode 100644 index 0000000000..268d5951b3 --- /dev/null +++ b/k8s_apps/matrix/index.html @@ -0,0 +1,2740 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Matrix - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Matrix

+ +

Matrix is an open protocol for decentralised, secure communications.

+

smol-k8s-lab deploys a matrix synapse server, element (a web frontend), and a turn server (voice server).

+

+screenshot of the Argo CD web interface showing the matrix app of apps in tree view mode, which shows the following children: external secrets appset, postgres appset, matrix PVC appset, s3 provider appset, s3 pvc app set, and matrix web app set. +

+
+ Matrix helm chart app in Argo CD + + + screenshot of the Argo CD web interface showing the matrix web app helm chart app in tree view mode. + + +
+ +
+ Matrix networking view in Argo CD + + + screenshot of the Argo CD web interface showing the matrix web app in networking view mode. It shows a cloud on the left flowing into a box that says 192.168.168.168 which branches off into three ingress resources: matrix stack element, matrix stack synapse, and matrix stack synapse federation. The ingress resource for element, branches off into a service of the same name and then a pod of the same name. The synapse and synapse federation ingress resources branch off into two respective services that branch off into one shared pod called matrix stack synapse. + + +
+ +
+

Note

+

We recently added support for sliding sync and matrix authentication service. To use these, please use matrix/app_of_apps_beta/ for apps.matrix.argo.path.

+
+

Required Values

+

Appset secret values

+

The main variables you need to worry about when setting up matrix are your hostname variables:

+
    +
  • hostname
  • +
  • element_hostname
  • +
  • federation_hostname
  • +
+

If using Matrix authentication Service and Sliding Sync:

+
    +
  • auth_hostname
  • +
  • sliding_sync_hostname
  • +
+

These are all storage related and you can leave them at the defaults for small servers:

+

Signing key storage: +- signing_key_pvc_enabled +- signing_key_storage +- signing_key_access_mode

+

Media storage: +- media_pvc_enabled +- media_storage +- media_access_mode

+

Synapse config storage: +- synapse_config_pvc_enabled +- synapse_config_storage +- synapse_config_access_mode

+

S3 storage: +- s3_provider +- s3_bucket +- s3_endpoint +- s3_pvc_capacity +- s3_region

+

You'll also need to provide an admin_email for users to contact someone if something goes wrong.

+

Init Values

+

These are all one time values that you need to provide, related entirely to mail setup:

+
    +
  • smtp_user
  • +
  • smtp_host +See below for providing smtp_password without putting it in plain text.
  • +
+

If you want to federate, you also need to provide: +- trusted_key_servers

+

You can provide a list of maps like this for trusted_key_servers:

+
1
+2
+3
+4
+5
+6
+7
apps:
+  matrix:
+    init:
+      values:
+        - server_name: matrix.friend.com
+          verify_keys:
+            ed25519:auto: abcdefghijklmnopqrstuvwxyzabcdefghijklmopqr
+
+

The trusted_key_servers option currently displays in the TUI, but is not editable via the TUI yet. See: #269

+

Sensitive values

+

Sensitive values can be provided via environment variables using a value_from map on any value under init.values or backups. Example of providing the SMTP password:

+
1
+2
+3
+4
+5
+6
+7
apps:
+  matrix:
+    init:
+      values:
+        smtp_password:
+          value_from:
+            env: MATRIX_SMTP_PASSWORD
+
+

Sensitive values before v5.0.0

+

smol-k8s-lab did not originally support the value_from map. If you're using a version before v5.0.0, to avoid having to provide sensitive values every time you run smol-k8s-lab with matrix enabled, set up the following environment variables:

+
    +
  • MATRIX_SMTP_PASSWORD
  • +
  • MATRIX_S3_BACKUP_ACCESS_ID
  • +
  • MATRIX_S3_BACKUP_SECRET_KEY
  • +
  • MATRIX_RESTIC_REPO_PASSWORD
  • +
+

Backups

+

Backups are a new feature in v5.0.0 that enable backing up your cluster via restic to a configurable remote S3 bucket. If you have init.enabled set to true and you're using our pre-configured argo.repo, we support both instant backups, and scheduled backups.

+

When running a backup of any kind, we will first initiate a Cloud Native Postgresql backup to your local seaweedfs cluster that we setup for you, and then wait until the last wal archive associated with that backup is complete. After that, we start a k8up backup job to backup all of your important PVCs to your configured s3 bucket.

+

To use the backups feature, you'll need to configure the values below.

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
apps:
+  matrix:
+    backups:
+      # cronjob syntax schedule to run matrix pvc backups. This example shows PVC Backups
+      # happening at 12:10 AM.
+      pvc_schedule: 10 0 * * *
+      # cronjob syntax (with SECONDS field) for matrix postgres backups
+      # must happen at least 10 minutes before pvc backups, to avoid corruption
+      # due to missing files. This is because the backup shows as completed before
+      # it actually is. This example shows postgres backups happening at exactly midnight
+      postgres_schedule: 0 0 0 * * *
+      s3:
+        endpoint: s3.eu-central-003.backblazeb2.com
+        bucket: my-matrix-backup-bucket
+        region: eu-central-003
+        secret_access_key:
+          value_from:
+            env: MATRIX_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            env: MATRIX_S3_BACKUP_ACCESS_ID
+      # restic requires this for encrypting your backups in the remote bucket
+      restic_repo_password:
+        value_from:
+          env: MATRIX_RESTIC_REPO_PASSWORD
+
+

Restores

+

Restores are a new feature in v5.0.0 that enable restoring your postgres cluster and PVCs via restic from a configurable remote S3 bucket. If you have init.enabled set to true and you're using our pre-configured argo.repo, we support restoring both your postgres cluster and PVCs. A restore is a kind of initialization process, so it lives under the init section of the config for your application, in this case, matrix. Here's an example:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
apps:
+  matrix:
+    enabled: false
+    init:
+      enabled: true
+      restore:
+        enabled: true
+        # this must be set to true to restore your postgres cluster
+        cnpg_restore: true
+        # all of these default to latest, but you can set them to any restic snapshot ID
+        restic_snapshot_ids:
+          seaweedfs_volume: latest
+          seaweedfs_filer: latest
+          matrix_media: latest
+          matrix_synapse_config: latest
+          matrix_signing_key: latest
+
+

The restore process will put your secrets into place, then restore your seaweedfs cluster first, followed by your postgresql cluster, followed by your matrix your PVCs, and then it will install your matrix argocd app as normal.

+

Full Example config

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
apps:
+  matrix:
+    description: |
+      [link=https://matrix.org/]Matrix[/link] is an open protocol for decentralised, secure communications.
+      This deploys a matrix synapse server, element (web frontend), and turn server (voice)
+
+      smol-k8s-lab supports initialization by creating initial secrets for your:
+        - matrix, element, and federation hostnames,
+        - credentials for: postgresql, admin user, S3 storage, and SMTP
+
+      smol-k8s-lab also sets up an OIDC application via Zitadel.
+
+      To provide sensitive values via environment variables to smol-k8s-lab use a value_from map in the config.yaml
+    enabled: false
+    init:
+      enabled: true
+      restore:
+        enabled: false
+        cnpg_restore: true
+        restic_snapshot_ids:
+          seaweedfs_volume: latest
+          seaweedfs_filer: latest
+          matrix_media: latest
+          matrix_synapse_config: latest
+          matrix_signing_key: latest
+      values:
+        smtp_user: change me to enable mail
+        smtp_host: enable.mail
+        smtp_password:
+          value_from:
+            env: MATRIX_SMTP_PASSWORD
+    backups:
+      # cronjob syntax schedule to run matrix pvc backups
+      pvc_schedule: 10 0 * * *
+      # cronjob syntax (with SECONDS field) for matrix postgres backups
+      # must happen at least 10 minutes before pvc backups, to avoid corruption
+      # due to missing files. This is because the backup shows as completed before
+      # it actually is
+      postgres_schedule: 0 0 0 * * *
+      s3:
+        endpoint: s3.eu-central-003.backblazeb2.com
+        bucket: my-matrix-backup-bucket
+        region: eu-central-003
+        secret_access_key:
+          value_from:
+            env: MATRIX_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            env: MATRIX_S3_BACKUP_ACCESS_ID
+      restic_repo_password:
+        value_from:
+          env: MATRIX_RESTIC_REPO_PASSWORD
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys:
+        # hostname of the synapse matrix server
+        hostname: "chat.beepboopfordogsnoots.city"
+        # the hostname of the element web interface
+        element_hostname: 'element.beepboopfordogsnoots.city'
+        # hostname for federation, that others can see you on the fediverse
+        federation_hostname: 'chat-federation.beepboopfordogsnoots.city'
+        # email for of the admin user
+        admin_email: 'notadog@coolcats.com'
+        # list of admin users for bridges, the hostnames will be appended automatically
+        admin_users:
+          - notadog
+          - realcat
+        # enable signing key backups
+        signing_key_pvc_enabled: 'true'
+        # size of signing key pvc storage
+        signing_key_storage: 1Mi
+        signing_key_access_mode: ReadWriteOnce
+        # enable persistent volume claim for matrix media storage
+        media_pvc_enabled: 'true'
+        # size of media pvc storage
+        media_storage: 20Gi
+        media_access_mode: ReadWriteOnce
+        # enable persistent volume claim for matrix synapse config storage
+        synapse_config_pvc_enabled: 'true'
+        # size of synapse config pvc storage
+        synapse_config_storage: 2Mi
+        synapse_config_access_mode: ReadWriteOnce
+        # choose S3 as the local primary object store from either: seaweedfs, or minio
+        # SeaweedFS - deploy SeaweedFS filer/s3 gateway
+        # MinIO     - deploy MinIO vanilla helm chart
+        s3_provider: seaweedfs
+        # local s3 provider bucket name
+        s3_bucket: matrix
+        # the endpoint you'd like to use for your minio or SeaweedFS instance
+        s3_endpoint: matrix-s3.social-media-for-dogs.com
+        # how large the backing pvc's capacity should be for minio or seaweedfs
+        s3_pvc_capacity: 100Gi
+        s3_region: eu-west-1
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "matrix/app_of_apps/"
+      # either the branch or tag to point at in the argo repo above
+      revision: "main"
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "matrix"
+      # recurse directories in the git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: matrix
+        source_repos:
+          - https://small-hack.github.io/cloudnative-pg-cluster-chart
+          - https://small-hack.github.io/matrix-chart
+          - https://operator.min.io/
+          - https://seaweedfs.github.io/seaweedfs/helm
+        destination:
+          namespaces:
+            - argocd
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/metallb/index.html b/k8s_apps/metallb/index.html new file mode 100644 index 0000000000..4efcc7bc4a --- /dev/null +++ b/k8s_apps/metallb/index.html @@ -0,0 +1,2342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + MetalLB - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

MetalLB

+ +

We use MetalLB as our load-balancer implementation for bare metal Kubernetes clusters, using standard routing protocols.

+

smol-k8s-lab installs MetalLB by default so that you can run your cluster locally with ease. Prior to using this feature, you need to make sure you have some free IP addresses available on your network. These can be local IPs as long as you have cleared this with your router :)

+

After smol-k8s-lab installs MetalLB on your cluster, we also create a default L2Advertisement and a default IPAddressPool using IP addresses you provide in the init section of the config file, or via the TUI. These are only configured once and won't be redeployed on subsequent runs of smol-k8s-lab on the same cluster.

+

Config file example:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
apps:
+  # This app is installed with helm or manifests depending on what is recommended
+  # for your k8s distro. Becomes managed by ArgoCD if you enable it below
+  metallb:
+    enabled: true
+    description: |
+      Helps expose IP addresses for loadbalancers on metal if you're on a vm or container where you can't get an IP.
+
+      Cloud Compatibility: [link=https://metallb.org/installation/clouds/]https://metallb.org/installation/clouds/[/link]
+
+      Learn more: [link=https://metallb.org/]https://metallb.org/[/link]
+
+      smol-k8s-lab support initialization by deploying a default l2Advertisement  IPAddressPool.
+    # Initialize of the app through smol-k8s-lab
+    init:
+      enabled: true
+      values:
+        # these addresses will be used by your ingress controller
+        address_pool:
+          - 192.168.20.23/32
+          - 192.168.20.24/32
+    argo:
+      # secret keys to provide for the argocd secret plugin app, none by default
+      secret_keys: {}
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      path: metallb/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: metallb-system
+      # source repos for Argo CD metallb Project (in addition to metallb.argo.repo)
+      project:
+        name: metallb
+        source_repos:
+        - https://github.com/metallb/metallb.git
+        destination:
+          namespaces:
+          - argocd
+          - metallb-system
+
+

TUI example:

+

terminal screenshot of smol-k8s-lab on the apps screen showing the app list on the left with metallb highlighted. On the right, there is a config panel for metallb with initialization enabled switch set to True and one init field titled address pool. The input field has the following text: 192.168.20.23/32, 192.168.20.24/32. below is the rest of the normal apps screen which is details further in the tui docs.

+

To dig a bit deeper on how we deploy the MetalLB Argo CD app, head over to small-hack/argocd-apps.

+

Why am I getting deprecation notices on certain apps?

+

If you have the krew deprecations plugin installed, then you might get something like this: +

1
+2
+3
+4
+5
+6
Deleted APIs:
+
+PodSecurityPolicy found in policy/v1beta1
+     ├─ API REMOVED FROM THE CURRENT VERSION AND SHOULD BE MIGRATED IMMEDIATELY!!
+        -> GLOBAL: metallb-controller
+        -> GLOBAL: metallb-speaker
+
+For Metallb, that's because of this issue. It'll be fixed in October of 2022.
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/netmaker/index.html b/k8s_apps/netmaker/index.html new file mode 100644 index 0000000000..839380fc04 --- /dev/null +++ b/k8s_apps/netmaker/index.html @@ -0,0 +1,2348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Netmaker - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Netmaker

+ +

Netmaker Argo CD Application

+

Netmaker is a vpn management tool wrapping WireGuard ®️.

+

+Screenshot of the Argo CD web interface showing the netmaker app of apps. The netmaker app shows three children: netmaker-persistence application in healthy state, netmaker-appset with a child of netmaker-helm app both in healthy state, and the netmaker external secrets appset with a child app of netmaker-externalsecrets also both in a healthy state +

+

+Screenshot of the Argo CD web interface showing the netmaker-helm app's networking view. From the left it shows connection from the internet to the ip 192.168.42.42 which then connects to three ingresses, which connect to three services, which connect to three pods. The ingresses are netmaker-api, netmaker-mqtt, and netmaker-ui, which similarly named services and pods. Outside of that part of the chart are two services netmaker-postgresql and netmaker-postgresql-hl. Both connect to a pod called netmaker-postgresql-0 +

+

We're currently using our own home grown helm chart as it supports existing secrets and initial super admin user creation. You can learn more about the Argo CD ApplicationSet here.

+

Initialization Features

+

If you set apps.netmaker.init.enabled to true, we will create a Zitadel app for use with oidc, and also create an initial admin user, plus disable the GUI registration for security sake.

+

Example Config

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
apps:
+  netmaker:
+    enabled: false
+    description: |
+      [link=https://www.netmaker.io/]Netmaker[/link]®️  makes networks with WireGuard. Netmaker automates fast, secure, and distributed virtual networks.
+    init:
+      enabled: true
+      values:
+        # this creates a super admin user and disables the GUI registration form
+        # if using the default config, we select a password for you and update your bitwarden
+        admin_user: admin
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys:
+        hostname: netmaker.example.com
+        admin_hostname: admin.netmaker.example.com
+        api_hostname: api.netmaker.example.com
+        broker_hostname: broker.netmaker.example.com
+        auth_provider: oidc
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      path: netmaker/app_of_apps/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: netmaker
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        source_repos:
+        - https://github.com/small-hack/netmaker-helm
+        - https://small-hack.github.io/netmaker-helm
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces: []
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/nextcloud/index.html b/k8s_apps/nextcloud/index.html new file mode 100644 index 0000000000..213dcad37d --- /dev/null +++ b/k8s_apps/nextcloud/index.html @@ -0,0 +1,2687 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Nextcloud - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Nextcloud

+ +

Nextcloud is an Open Source and self hosted personal cloud which acts as a replacement for Google Drive/Google Photos/iCloud Drive. We can deploy it for you as a featured Argo CD app (small-hack/argocd-apps/nextcloud) on Kubernetes. Here's an example of the full nextcloud app of apps in Argo CD's web interface:

+

+ screenshot of the Argo CD web interface showing the nextcloud app of apps in tree view mode, which shows the following child apps: before starting scripts, external secrets appset, maintenance mode cron appset, postgres appset, nextcloud PVC appset, s3 provider appset, s3 pvc app set, nextcloud web app set, and install nexcloud apps post install hook job. Each of these appsets then deploys a similar named app and the job deploys a pod of the same name. +

+
+ Nextcloud Argo CD helm chart app + + + screenshot of the Argo CD web interface showing the nextcloud-web-app helm chart app and all of its resources in tree view mode. + + +
+ +

Part of the smol-k8s-lab init process is that we will put the following into your Bitwarden vault:

+
    +
  • administration credentials
  • +
  • SMTP credentials
  • +
  • PostgreSQL credentials
  • +
  • s3 credentials
  • +
  • OIDC credentials
  • +
+

Required Values

+

Init Values

+

To use the default smol-k8s-lab Argo CD Application, you'll need to provide one time init values for:

+
    +
  • admin_user
  • +
  • smtp_user
  • +
  • smtp_host
  • +
+

Required ApplicationSet Values

+

And you'll also need to provide the following values to be templated for your personal installation:

+
    +
  • hostname
  • +
  • default_phone_region
  • +
+

These determine how you'd like to set up persistence for nextcloud. We recommend just files enabled for now

+
    +
  • files_pvc_enabled
  • +
  • files_storage
  • +
  • files_access_mode
  • +
  • config_pvc_enabled
  • +
  • config_storage
  • +
  • config_access_mode
  • +
+

Sensitive values

+

Sensitive values can be provided via environment variables using a value_from map on any value under init.values or backups. Example of providing the SMTP password:

+
1
+2
+3
+4
+5
+6
+7
apps:
+  nextcloud:
+    init:
+      values:
+        smtp_password:
+          value_from:
+            env: NC_SMTP_PASSWORD
+
+

Sensitive values before v5.0.0

+

smol-k8s-lab did not originally support the value_from map. If you're using a version before v5.0.0, to avoid having to provide sensitive values every time you run smol-k8s-lab with nextcloud enabled, set up the following environment variables:

+
    +
  • NEXTCLOUD_SMTP_PASSWORD
  • +
  • NEXTCLOUD_S3_BACKUP_ACCESS_KEY
  • +
  • NEXTCLOUD_S3_BACKUP_ACCESS_ID
  • +
  • NEXTCLOUD_RESTIC_REPO_PASSWORD
  • +
+

Backups

+

Backups are a new feature in v5.0.0 that enable backing up your postgres cluster and PVCs via restic to a configurable remote S3 bucket. If you have init.enabled set to true and you're using our pre-configured argo.repo, we support both instant backups, and scheduled backups.

+

When running a nextcloud backup, we will first put your cluster into maintenance mode, then initiate a Cloud Native Postgresql backup to your local seaweedfs cluster that we setup for you, and then wait until the last wal archive associated with that backup is complete. After that, we start a k8up backup job to backup all of your important PVCs to your configured s3 bucket. Finally, after the backup is done, we take your cluster our of maintenance mode.

+

To use the backups feature, you'll need to configure the values below.

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
apps:
+  nextcloud:
+    backups:
+      # cronjob syntax schedule to run nextcloud pvc backups. This example shows PVC Backups
+      # happening at 12:10 AM.
+      pvc_schedule: 10 0 * * *
+      # cronjob syntax (with SECONDS field) for nextcloud postgres backups
+      # must happen at least 10 minutes before pvc backups, to avoid corruption
+      # due to missing files. This is because the backup shows as completed before
+      # it actually is. This example shows postgres backups happening at exactly midnight
+      postgres_schedule: 0 0 0 * * *
+      s3:
+        endpoint: s3.eu-central-003.backblazeb2.com
+        bucket: my-nextcloud-backup-bucket
+        region: eu-central-003
+        secret_access_key:
+          value_from:
+            env: NC_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            env: NC_S3_BACKUP_ACCESS_ID
+      # restic requires this for encrypting your backups in the remote bucket
+      restic_repo_password:
+        value_from:
+          env: NC_RESTIC_REPO_PASSWORD
+
+

Restores

+

Restores are a new feature in v5.0.0 that enable restoring your cluster via restic from a configurable remote S3 bucket. If you have init.enabled set to true and you're using our pre-configured argo.repo, we support restoring both your postgres cluster and PVCs. A restore is a kind of initialization process, so it lives under the init section of the config for your application, in this case, nextcloud. Here's an example:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
apps:
+  nextcloud:
+    enabled: false
+    init:
+      enabled: true
+      restore:
+        enabled: false
+        # this must be set to true to restore your postgres cluster
+        cnpg_restore: true
+        # all of these default to latest, but you can set them to any restic snapshot ID
+        restic_snapshot_ids:
+          seaweedfs_volume: latest
+          seaweedfs_filer: latest
+          nextcloud_files: latest
+
+

The restore process will put your secrets into place, then restore your seaweedfs cluster first, followed by your postgresql cluster, followed by your nextcloud PVCs, and then it will install your nextcloud argocd app as normal. Just after it's installed, we'll also take your cluster out of maintenance mode :)

+

Full Example Config

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
apps:
+  nextcloud:
+    enabled: false
+    description: |
+      [link=https://nextcloud.com/]Nextcloud Hub[/link] is the industry-leading, fully open-source, on-premises content collaboration platform. Teams access, share and edit their documents, chat and participate in video calls and manage their mail and calendar and projects across mobile, desktop and web interfaces
+
+      smol-k8s-lab supports initialization by setting up your admin username, password, and SMTP username and password, as well as your redis and postgresql credentials.
+
+      To avoid providing sensitive values everytime you run smol-k8s-lab, consider exporting the following environment variables before running smol-k8s-lab:
+        - NEXTCLOUD_SMTP_PASSWORD
+        - NEXTCLOUD_S3_BACKUP_ACCESS_KEY
+        - NEXTCLOUD_S3_BACKUP_ACCESS_ID
+        - NEXTCLOUD_RESTIC_REPO_PASSWORD
+
+      Note: smol-k8s-lab is not affiliated with Nextcloud GmbH. This is a community-supported-only install method.
+    # initialize the app by setting up new k8s secrets and/or Bitwarden items
+    init:
+      enabled: true
+      restore:
+        enabled: true
+        cnpg_restore: true
+        restic_snapshot_ids:
+          seaweedfs_volume: latest
+          seaweedfs_filer: latest
+          nextcloud_files: latest
+      values:
+        admin_user: 'mycooladminuser'
+        smtp_user: 'mycoolsmtpusername'
+        smtp_host: 'mail.cooldogs.net'
+        smtp_password:
+          value_from:
+            env: NEXTCLOUD_SMTP_PASSWORD
+    backups:
+      # cronjob syntax schedule to run nextcloud pvc backups
+      pvc_schedule: 10 0 * * *
+      # cronjob syntax (with SECONDS field) for nextcloud postgres backups
+      # must happen at least 10 minutes before pvc backups, to avoid corruption
+      # due to missing files. This is because the cnpg backup shows as completed
+      # before it actually is, due to the wal archive it lists as it's end not
+      # being in the backup yet
+      postgres_schedule: 0 0 0 * * *
+      s3:
+        # these are for pushing remote backups of your local s3 storage, for speed and cost optimization
+        endpoint: s3.eu-central-003.backblazeb2.com
+        bucket: my-nextcloud-bucket
+        region: eu-central-003
+        secret_access_key:
+          value_from:
+            env: NEXTCLOUD_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            env: NEXTCLOUD_S3_BACKUP_ACCESS_ID
+      restic_repo_password:
+        value_from:
+          env: NEXTCLOUD_RESTIC_REPO_PASSWORD
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys:
+        # the FQDN that you want to host nextcloud on
+        hostname: "cloud.cooldogs.net"
+        # the default phone region for users that don't have one set
+        default_phone_region: NL
+        # the default domain to send emails to user with
+        mail_domain: "domain.com"
+        # the default user to use with mail_domain for sending emails, formats to no-reply@domain.com
+        mail_from_address: "no-reply"
+        # enable persistent volume claim for nextcloud files storage
+        files_pvc_enabled: 'true'
+        # size of files pvc storage
+        files_storage: 100Gi
+        files_access_mode: ReadWriteOnce
+        # enable persistent volume claim for nextcloud config storage
+        config_pvc_enabled: 'false'
+        # size of config pvc storage
+        config_storage: 20Gi
+        config_access_mode: ReadWriteOnce
+        # choose S3 as the local primary object store from either: seaweedfs, or minio
+        # SeaweedFS - deploy SeaweedFS filer/s3 gateway
+        # MinIO     - deploy MinIO vanilla helm chart
+        s3_provider: seaweedfs
+        # the endpoint you'd like to use for your minio or SeaweedFS instance
+        s3_endpoint: cloud-s3.cooldogs.net
+        # how large the backing pvc's capacity should be for minio or seaweedfs
+        s3_pvc_capacity: 10Gi
+        s3_region: eu-west-1
+        # cronjob schedule to turn on nextcloud maintenance mode for backups
+        maintenance_mode_on_schedule: 30 23 * * *
+        # cronjob schedule to turn off nextcloud maintenance mode after backups
+        maintenance_mode_off_schedule: 30 1 * * *
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "nextcloud/app_of_apps/"
+      # either the branch or tag to point at in the argo repo above
+      revision: "main"
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "nextcloud"
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: nextcloud
+        source_repos:
+          - registry-1.docker.io
+          - https://nextcloud.github.io/helm
+          - https://small-hack.github.io/cloudnative-pg-cluster-chart
+          - https://seaweedfs.github.io/seaweedfs/helm
+          - https://github.com/seaweedfs/seaweedfs
+        destination:
+          namespaces: []
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/prometheus/index.html b/k8s_apps/prometheus/index.html new file mode 100644 index 0000000000..2e98c6fc37 --- /dev/null +++ b/k8s_apps/prometheus/index.html @@ -0,0 +1,2440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Prometheus - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Prometheus

+ +

Prometheus is the core of our optionally installed monitoring stack. Together with Grafana and loki, we cover gathering metrics and logs as well as creating dashboards. We even deploy alert-manager for you to create your own alerts.

+

You can see an overview of the whole Prometheus Stack Argo CD Application at small-hack/argocd-apps/prometheus.

+

+screenshot of the Argo CD web interface showing the prometheus app of apps which includes the following children: loki, prometheus-crd, prometheus-appset, prometheus-pushgateway-appset +

+

Important note

+

We haven't generated new screenshots, but we've updated how we now deploy the Prometheus CRDs. They are now deployed separately, so that anything that relies on them that gets deployed earlier on, such as your identity provider, which you may want to secure the prometheus related frontends.

+

Example configs

+

Custom Resource Definitions (CRDs)

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
apps:
+  prometheus_crds:
+    description: |
+      [link=https://prometheus.io/docs/introduction/overview/]Prometheus[/link] CRDs to start with.
+      You can optionally disable this if you don't want to deploy apps with metrics.
+
+    enabled: true
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys: {}
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      path: prometheus/crds/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: prometheus
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: prometheus
+        source_repos:
+        - https://github.com/prometheus-community/helm-charts.git
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces:
+          - kube-system
+          - prometheus
+
+

kube-prometheus-stack

+

NOTE: You need to also enable the CRD app above for this to work!

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
apps:
+  prometheus:
+  prometheus:
+    description: |
+      Full monitoring stack with [link=https://prometheus.io/docs/introduction/overview/]Prometheus[/link], [link=https://grafana.com/oss/loki/]Loki[/link], [link=https://prometheus.io/docs/alerting/latest/alertmanager/]Alert Manager[/link], and [link=https://grafana.com/oss/grafana/]Grafana[/link].
+
+      smol-k8s-lab supports initialization by setting up your ingress hostnames. It will also setup Oauth2 for Grafana directly by creating an app in Zitadel for you.
+
+      For Prometheus and Alert Manager, we use vouch-proxy via Ingress resource annotations to forward users to Zitadel for auth, so the frontend is not insecure.
+    enabled: false
+    init:
+      # if init is enabled, we'll set up an app in Zitadel for using Oauth2 with Grafana
+      enabled: true
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys:
+        # FQDN to use for Prometheus web interface
+        hostname: ""
+        # FQDN to use for grafana
+        grafana_hostname: ""
+        # FQDN to use for Alert Manager web interface
+        alert_manager_hostname: ""
+        # FQDN to use for the prometheus push gateway
+        push_gateway_hostname: ""
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important! This
+      # is an app of apps. Change to "monitoring/kube-prometheus-stack/" to
+      # only install kube-prometheus-stack (foregoing loki and push gateway)
+      path: prometheus/app_of_apps/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "prometheus"
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: prometheus
+        source_repos:
+          - "registry-1.docker.io"
+          - "https://github.com/prometheus-community/helm-charts.git"
+          - "https://prometheus-community.github.io/helm-charts"
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces:
+            - kube-system
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/seaweedfs/index.html b/k8s_apps/seaweedfs/index.html new file mode 100644 index 0000000000..bba9e90666 --- /dev/null +++ b/k8s_apps/seaweedfs/index.html @@ -0,0 +1,2353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + SeaweedFS - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

SeaweedFS

+ +

SeaweedFS is a fast distributed storage system for blobs, objects, files, and data lakes with a O(1) disk seek speeds. You can find out more about them by checking their Wiki.

+

Smol-k8s-lab uses SeaweedFS for creating isolated file-systems for apps like Postgres, Mastodon, and JuiceFS. This provides a consistent storage layer across applications that allows for uniform backup and restoration processes and high-speed local-storage. This data can easily be backed up to external storage via k8up. Check it out via our SeaweedFS Argo CD Application.

+

How it works

+
    +
  • The Volume Services chunk and encrypt data on-disk.
  • +
  • The Filer Service tracks what data is stored where and answers queries about it.
  • +
  • The Control Server keeps the state of all servers and keeps everything in sync
  • +
+

+ + + +

+ +

Security

+

We enable encryption by default.

+

Example config

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
apps:
+  seaweedfs:
+    enabled: false
+    description: |
+      [link=https://github.com/seaweedfs/seaweedfs]seaweedfs[/link] is a filesystem with an exposable S3 endpoint.
+
+      This is mostly meant to be for testing, but have at it :D
+
+      If directory_recursion is set to true, we will also deploy the csi driver.
+    init:
+      enabled: true
+      values:
+        root_user: admin
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys:
+        hostname: ''
+        s3_endpoint: 'seaweedfs.cooldogs.com'
+        s3_region: eu-west-1
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      path: seaweedfs/app_of_apps/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: seaweedfs
+      # recurse directories in the provided git repo
+      # if set to false, we will not deploy the CSI driver
+      directory_recursion: true
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: seaweedfs
+        source_repos:
+        - https://seaweedfs.github.io/seaweedfs/helm
+        - https://seaweedfs.github.io/seaweedfs-csi-driver/helm
+        - https://github.com/seaweedfs/seaweedfs
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces: []
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/vouch/index.html b/k8s_apps/vouch/index.html new file mode 100644 index 0000000000..59c9d9d678 --- /dev/null +++ b/k8s_apps/vouch/index.html @@ -0,0 +1,2348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Vouch - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Vouch

+ +

vouch-proxy is an SSO solution for Nginx using the auth_request module. Vouch Proxy can protect all of your websites at once.

+

smol-k8s-lab supports a custom initialization of Vouch using Zitadel.

+

+Screenshot of the Argo CD web interface showing the vouch app of apps in tree view mode. It has two children: vouch-appset which has a children of vouch-helm, and vouch-external-secrets-appset which has a child of vouch-external-secrets +

+

Learn more about our:

+ +

Required Init Values

+

These values are required only if you're using the default smol-k8s-lab git repository for vouch.

+
    +
  • domains - these are all the domains that are allowed to be used behind vouch
  • +
  • emails - these are all the email addresses that are allowed to view websites behind vouch
  • +
+

If you're using our default Argo CD ApplicationSet, you also need to pass in hostname.

+

Example yaml config

+

Here's an example of a working vouch app config:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
apps:
+  vouch:
+    description: |
+      [link=https://github.com/vouch/vouch-proxy]vouch-proxy[/link] can help you forward requests for OIDC authentication to any ingress source that doesn't already have it. Super useful for web pages like prometheus's UI.
+
+      smol-k8s-lab supports the initialization of vouch if you also enable zitadel by creating OIDC applications and credentials and your vouch-proxy Kubernetes Secret.
+    enabled: true
+    # Initialization of the app through smol-k8s-lab using bitwarden and/or k8s secrets
+    init:
+      enabled: true
+      values:
+        # list of domains allowed to be behind vouch such as example.com
+        domains: []
+        # - example.com
+        # email addresses allowed to authenticate via vouch
+        emails: []
+        # - beep@boop.com
+    argo:
+      # secrets keys to make available to Argo CD ApplicationSets
+      secret_keys:
+        # FQDN to use for vouch
+        hostname: ""
+      # repo to install the Argo CD app from
+      # git repo to install the Argo CD app from
+      repo: "https://github.com/small-hack/argocd-apps"
+      # path in the argo repo to point to. Trailing slash very important!
+      path: "vouch-proxy/app_of_apps/"
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: "vouch"
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: vouch
+        source_repos:
+          - https://small-hack.github.io/vouch-helm-chart
+        destination:
+          # automatically includes the app's namespace and argocd's namespace
+          namespaces: []
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_apps/zitadel/index.html b/k8s_apps/zitadel/index.html new file mode 100644 index 0000000000..c920e4ba2e --- /dev/null +++ b/k8s_apps/zitadel/index.html @@ -0,0 +1,2653 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Zitadel - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Zitadel

+ +

Zitadel is an Identity Management solution that includes acting as an OIDC provider.

+

+screenshot of Argo CD web interface's tree view of a zitadel app of apps. The main app of apps branches off into the following appsets: external secrets, postgres, s3 provider, s3 PVC, and zitadel web app. Each of those then branches off into a similarly named app. +

+
+ Zitadel web app (official zitadel helm chart) screenshot + + + screenshot of Argo CD web interface's tree view of a zitadel web app in tree view mode. Includes the following child resources: zitadel config map, zitadel service, zitadel service account, zitadel deployment, zitadel init job, zitadel setup job, zitadel service monitor, zitadel ingress, zitadel role, zitadel role binding. The zitadel service then branches off into zitadel endpoint and zitadel endpointslice. The zitadel deployment branches off into a zitadel replica set which branches off into a zitadel pod. The zitadel init and setup jobs also branch off into their own completed pods, and finally, the zitadel ingress resource branches off into a zitadel TLS certificate + + +
+ +
+ Argo CD Zitadel Postgresql cluster screenshot + + + screenshot of Argo CD web interface's tree view of a zitadel postgresql cluster in tree view. It shows the following secrets and coorsponding certificates: client cert, postgres cert, server secret, zitadel cert. Each of those then has their own cert request resource. Afte rthat there's 3 tls issuers: client ca, selfsigned, and server ca. Next there is the cluster, which branches off into a pvc, pod, secret for the app, secret for the super user, service for read, service for read only, service for read write, service account, pod disruption budget for the primary, role, and role binding + + +
+ +

Zitadel is one of the more complex apps that smol-k8s-lab supports out of the box. For initialization, you need to pass in the following info:

+
    +
  • username - name of the first admin user to create
  • +
  • email - email of the first admin user
  • +
  • first name - first name of the first admin user
  • +
  • last name - last name of the first admin user
  • +
  • gender - optional - the gender of the first admin user
  • +
+

The above values are used to create an initial user. We also create Argo CD admin and users groups to be used with an Argo CD OIDC app that we prepare. If Vouch is enabled, we also create an OIDC app for that as well as a user group. You initial user is automatically added to all the groups we create.

+

Finally, we create a groupsClaim so that all queries for auth also process the user's groups.

+

In addition to those one time init values, we also require a hostname to use for the Zitadel API and web frontend.

+

Sensitive values

+

Sensitive values can be provided via environment variables using a value_from map on any value under init.values or backups. Example of providing s3 credentials and restic repo password via sensitive values:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
apps:
+  zitadel:
+    backups:
+      s3:
+        secret_access_key:
+          value_from:
+            # can be any env var
+            env: ZITADEL_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            # can be any env var
+            env: ZITADEL_S3_BACKUP_ACCESS_ID
+      restic_repo_password:
+        value_from:
+          # can be any env var
+          env: ZITADEL_RESTIC_REPO_PASSWORD
+
+

Backups

+

Backups are a new feature in v5.0.0 that enable backing up your postgres cluster and PVCs via restic to a configurable remote S3 bucket. Backups require init.enabled set to true and you must ensure you're using our pre-configured argo.repo. We support both instant backups, and scheduled backups.

+

When running a zitadel backup, we will initiate a Cloud Native Postgresql backup to your local seaweedfs cluster that we setup for you, and then wait until the last wal archive associated with that backup is complete. After that, we start a k8up backup job to backup all of your important PVCs to your configured s3 bucket.

+

To use the backups feature, you'll need to configure the values below.

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
apps:
+  zitadel:
+    backups:
+      # cronjob syntax schedule to run zitadel seaweedfs pvc backups
+      pvc_schedule: 10 0 * * *
+      # cronjob syntax (with SECONDS field) for zitadel postgres backups
+      # must happen at least 10 minutes before pvc backups, to avoid corruption
+      # due to missing files. This is because the cnpg backup shows as completed
+      # before it actually is, due to the wal archive it lists as it's end not
+      # being in the backup yet
+      postgres_schedule: 0 0 0 * * *
+      s3:
+        # these are for pushing remote backups of your local s3 storage, for speed and cost optimization
+        endpoint: s3.eu-central-003.backblazeb2.com
+        bucket: my-zitadel-backup-bucket
+        region: eu-central-003
+        secret_access_key:
+          value_from:
+            env: ZITADEL_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            env: ZITADEL_S3_BACKUP_ACCESS_ID
+      restic_repo_password:
+        value_from:
+          env: ZITADEL_RESTIC_REPO_PASSWORD
+
+

Restores

+

Restores are a new feature in v5.0.0 that enable restoring your cluster via restic from a configurable remote S3 bucket. This feature was finally tested with Zitadel in v5.6.0. If you have init.enabled set to true and you're using our pre-configured argo.repo, we support restoring both your Postgresql cluster and Persistent Volume Claims.

+

A restore is a kind of initialization process, so it lives under the init section of the config for your application, in this case, Zitadel. Here's an example you could use in your ~/.config/smol-k8s-lab/config.yaml:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
apps:
+  zitadel:
+    init:
+      enabled: true
+      restore:
+        enabled: false
+        cnpg_restore: true
+        restic_snapshot_ids:
+          # these can all be any restic snapshot ID, but default to latest
+          seaweedfs_volume: latest
+          seaweedfs_filer: latest
+
+

The restore process will put your secrets into place, then restore your seaweedfs cluster first, followed by your postgresql cluster, and then it will install your zitadel argocd app as normal.

+

Sensitive values before v5.0.0

+

smol-k8s-lab did not originally support the value_from map. If you're using a version before v5.0.0, to avoid having to provide sensitive values every time you run smol-k8s-lab with zitadel enabled, set up the following environment variables:

+
    +
  • ZITADEL_S3_BACKUP_ACCESS_ID
  • +
  • ZITADEL_S3_BACKUP_SECRET_KEY
  • +
  • ZITADEL_RESTIC_REPO_PASSWORD
  • +
+

Example config

+

Here's a full working config for zitadel. (If this isn't working, please submit an issue on our GitHub!)

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
apps:
+  zitadel:
+    enabled: false
+    description: |
+      [link=https://zitadel.com/opensource]ZITADEL[/link] is an open source self hosted IAM platform for the cloud era
+
+      smol-k8s-lab supports initialization of:
+        - an admin service account
+        - a human admin user (including an autogenerated password)
+        - a project with a name of your chosing
+        - 2 OIDC applications for Argo CD and Vouch
+        - 2 Argo CD groups (admins and users)
+        - 1 vouch groups
+        - groupsClaim action to enforce group roles on authentication
+        - updates your appset_secret_plugin secret and refreshes the pod
+
+      The default app will also deploy SeaweedFS to backup your database which in turn is backed up to a remote s3 provider of your choice.
+
+      To provide sensitive values via environment variables to smol-k8s-lab use:
+        - ZITADEL_S3_BACKUP_ACCESS_ID
+        - ZITADEL_S3_BACKUP_SECRET_KEY
+        - ZITADEL_RESTIC_REPO_PASSWORD
+    init:
+      # Switch to false if you don't want to create initial secrets or use the
+      # API via a service account to create the above described resources
+      enabled: true
+      values:
+        username: 'certainlynotadog'
+        email: 'notadog@humans.com'
+        first_name: 'Dogsy'
+        last_name: 'Dogerton'
+        # options: GENDER_UNSPECIFIED, GENDER_MALE, GENDER_FEMALE, GENDER_DIVERSE
+        # more coming soon, see: https://github.com/zitadel/zitadel/issues/6355
+        gender: GENDER_UNSPECIFIED
+        # name of the default project to create OIDC applications in
+        project: core
+        # coming soon after we refactor a bit
+        # smtp_password:
+        #   value_from:
+        #     env: ZITADEL_SMTP_PASSWORD
+      restore:
+        enabled: false
+        cnpg_restore: true
+        restic_snapshot_ids:
+          seaweedfs_volume: latest
+          seaweedfs_filer: latest
+    backups:
+      # cronjob syntax schedule to run zitadel seaweedfs pvc backups
+      pvc_schedule: 10 0 * * *
+      # cronjob syntax (with SECONDS field) for zitadel postgres backups
+      # must happen at least 10 minutes before pvc backups, to avoid corruption
+      # due to missing files. This is because the cnpg backup shows as completed
+      # before it actually is, due to the wal archive it lists as it's end not
+      # being in the backup yet
+      postgres_schedule: 0 0 0 * * *
+      # these are for pushing backups of your local s3 storage to a remote s3 bucket, which
+      # is separate from your postgresql backups, so that postgresql can backup wal archives
+      # every 5 minutes with speed and then for and then for cost optimization, only backup
+      # all achives gathered during the day to the remote s3 store AFTER the nightly
+      # postgresql backups.
+      s3:
+        endpoint: s3.eu-central-003.backblazeb2.com
+        bucket: my-zitadel-backup-bucket
+        region: eu-central-003
+        secret_access_key:
+          value_from:
+            env: ZITADEL_S3_BACKUP_SECRET_KEY
+        access_key_id:
+          value_from:
+            env: ZITADEL_S3_BACKUP_ACCESS_ID
+      restic_repo_password:
+        value_from:
+          env: ZITADEL_RESTIC_REPO_PASSWORD
+    argo:
+      # secrets keys to make available to ArgoCD ApplicationSets
+      secret_keys:
+        # FQDN to use for zitadel
+        hostname: 'zitadel.gooddogs.com'
+        # type of database to use: postgresql or cockroachdb
+        database_type: postgresql
+        # set the local s3 provider for zitadel's database backups. can be minio or seaweedfs
+        s3_provider: seaweedfs
+        # local s3 endpoint for postgresql backups, backed up constantly
+        s3_endpoint: 'zitadel-s3.gooddogs.com'
+        # capacity for the PVC backing your local s3 instance
+        s3_pvc_capacity: 2Gi
+      # repo to install the Argo CD app from
+      # git repo to install the Argo CD app from
+      repo: https://github.com/small-hack/argocd-apps
+      # path in the argo repo to point to. Trailing slash very important!
+      path: zitadel/app_of_apps/
+      # either the branch or tag to point at in the argo repo above
+      revision: main
+      # kubernetes cluster to install the k8s app into, defaults to Argo CD default
+      cluster: https://kubernetes.default.svc
+      # namespace to install the k8s app in
+      namespace: zitadel
+      # recurse directories in the provided git repo
+      directory_recursion: false
+      # source repos for Argo CD App Project (in addition to argo.repo)
+      project:
+        name: zitadel
+        source_repos:
+          - https://charts.zitadel.com
+          - https://zitadel.github.io/zitadel-charts
+          - https://small-hack.github.io/cloudnative-pg-cluster-chart
+          - https://operator.min.io/
+          - https://seaweedfs.github.io/seaweedfs/helm
+        destination:
+          namespaces: []
+
+

You can learn more about our Zitadel Argo CD Application at small-hack/argocd-apps/zitadel.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_distros/distros/index.html b/k8s_distros/distros/index.html new file mode 100644 index 0000000000..542e8e0575 --- /dev/null +++ b/k8s_distros/distros/index.html @@ -0,0 +1,2213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Intro - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Intro

+ +

For each K8s distro below, in addition to being a supported install path with smol-k8s-lab, you can also check out a full tutorial to get started from scratch, or use a preconfigured BASH script we've created. We always install the latest version of Kubernetes that is available from the distro's startup script.

+ + + + + + + + + + + + + + + + + +
DistroDescription

k3s
The certified Kubernetes distribution built for IoT & Edge computing

KinD
kind is a tool for running local Kubernetes clusters using Docker container “nodes”. kind was primarily designed for testing Kubernetes itself, but may be used for local development or CI.
+

We tend to test on k3s first, then kind.

+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_distros/k3d/k3d/index.html b/k8s_distros/k3d/k3d/index.html new file mode 100644 index 0000000000..1c028e79be --- /dev/null +++ b/k8s_distros/k3d/k3d/index.html @@ -0,0 +1,2195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + About - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

About

+ +

k3d is k3s in docker.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_distros/k3s/k3s/index.html b/k8s_distros/k3s/k3s/index.html new file mode 100644 index 0000000000..7f8058500a --- /dev/null +++ b/k8s_distros/k3s/k3s/index.html @@ -0,0 +1,2264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + About - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

About

+ +

K3s is packaged as a single <70MB binary that reduces the dependencies and steps needed to install, run and auto-update a production Kubernetes cluster. Optimized for ARM Both ARM64 and ARMv7 are supported with binaries and multiarch images available for both. If you just want to get quickly started with it, you can do:

+
1
+2
+3
+4
curl -sfL https://get.k3s.io | sh -
+
+# Check for Ready node, takes maybe 30 seconds
+k3s kubectl get node
+
+

Troubleshooting

+

Default directory for Persistent Volumes

+

Where is your persistent volume data? If you used the local path provisioner it is here: +/var/lib/rancher/k3s/storage

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_distros/k3s/quickstart/index.html b/k8s_distros/k3s/quickstart/index.html new file mode 100644 index 0000000000..2f09c97299 --- /dev/null +++ b/k8s_distros/k3s/quickstart/index.html @@ -0,0 +1,2362 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + K3s BASH Quickstart - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

QuickStart

+ +

Best for Linux on metal or a bridged VM

+

Pre-Req

+
    +
  • Have internet access.
  • +
  • clone the smol-k8s-lab repo
  • +
  • Optional: Install k9s, which is like top for kubernetes clusters, to monitor the cluster.
  • +
+

These can also be set in a .env file in this directory :)

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
# IP address pool for metallb, this is where your domains will map
+# back to if you use ingress for your cluster, defaults to 8 ip addresses
+export CIDR="192.168.42.42-192.168.42.50"
+
+# email address for lets encrypt
+export EMAIL="dogontheinternet@coolemails4dogs.com"
+
+# SECTION FOR GRAFANA AND PROMETHEUS
+#
+# this is for prometheus alert manager
+export ALERT_MANAGER_DOMAIN="alert-manager.selfhosting4dogs.com"
+# this is for your grafana instance, that is connected to prometheus
+export GRAFANA_DOMAIN="grafana.selfhosting4dogs.com"
+# this is for prometheus proper, where you'll go to verify exporters are working
+export PROMETHEUS_DOMAIN="prometheus.selfhosting4dogs.com"
+
+

Then you can run the script! :D

+
1
+2
+3
# From the cloned repo dir, This should set up k3s and dependencies
+# Will also launch k9s, like top for k8s, To exit k9s, use type :quit
+./bash_scripts/k3s/bash_full_quickstart.sh
+
+

Ready to clean up this cluster?

+

To delete the whole cluster, the above k3s install also included an uninstall script that should be in your path already:

+
1
k3s-uninstall.sh
+
+

Final Touches

+

Port Forwarding

+

If you want to access an app outside of port forwarding to test, you'll need to make sure your app's ingress is setup correctly and then you'll need to setup your router to port forward 80->80 and 443->443 for your WAN. then setup DNS for your domain if you want the wider internet to access this remotely.

+

SSL/TLS

+

After SSL is working (if it's not, follow the steps in the cert-manager common error troubleshooting guide), you can also change the letsencrypt-staging value to letsencrypt-prod for any domains you own and can configure to point to your cluster via DNS.

+

Remote cluster administration

+

You can also copy your remote k3s kubeconfig with a little script in ./bash_scripts/k3s/:

+
1
+2
+3
+4
+5
+6
+7
# CHANGE THESE TO YOUR OWN DETAILS or not ¯\_(ツ)_/¯
+export REMOTE_HOST="192.168.20.2"
+export REMOTE_SSH_PORT="22"
+export REMOTE_USER="cooluser4dogs"
+
+# this script will totally wipe your kubeconfig :) use with CAUTION
+./bash_scripts/k3s/get-remote-k3s-yaml.sh
+
+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_distros/k3s/sqllite_backend/index.html b/k8s_distros/k3s/sqllite_backend/index.html new file mode 100644 index 0000000000..750a729125 --- /dev/null +++ b/k8s_distros/k3s/sqllite_backend/index.html @@ -0,0 +1,2197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Extras - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Extras

+ +

The state file is in:

+
1
/var/lib/rancher/k3s/server/db
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_distros/k3s/tutorial/index.html b/k8s_distros/k3s/tutorial/index.html new file mode 100644 index 0000000000..88d3d64c1b --- /dev/null +++ b/k8s_distros/k3s/tutorial/index.html @@ -0,0 +1,2558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + K3s Tutorial - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Tutorial

+ +

create the k3s cluster (just one server node)

+
1
+2
+3
+4
+5
+6
+7
# skip install of traefik & servicelb
+export INSTALL_K3S_EXEC=" --no-deploy servicelb --no-deploy traefik"
+
+# make the kubeconfig copy-able for later
+export K3S_KUBECONFIG_MODE="644"
+
+curl -sfL https://get.k3s.io | sh -
+
+

Grab the kubeconfig

+

Copy the k3s kubeconfig to the right place +

1
+2
+3
+4
mkdir -p ~/.kube
+cp /etc/rancher/k3s/k3s.yaml ~/.kube/kubeconfig
+# change the permissions os that it doesn't complain
+chmod 600 ~/.kube/kubeconfig
+
+

add/update all relevant helm repos

+

Add/update helm repos for metallb, ingress-nginx, cert-manager, prometheus [operator/alert manager/push gateway], and grafana. +

1
+2
+3
+4
+5
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
+helm repo add metallb https://metallb.github.io/metallb
+helm repo add jetstack https://charts.jetstack.io
+helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
+helm repo update
+
+

MetalLb installation

+

Learn more about metallb here. +

1
helm install metallb metallb/metallb -n kube-system --wait
+
+

Wait on metallb to deploy, because sometimes helm wait doesn't do the trick: +

 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
kubectl rollout status -n kube-system deployment/metallb-controller
+
+kubectl wait --namespace kube-system \
+  --for=condition=ready pod \
+  --selector=app.kubernetes.io/instance=metallb \
+  --timeout=120s
+
+kubectl wait --namespace kube-system \
+  --for=condition=ready pod \
+  --selector=app.kubernetes.io/component=speaker \
+  --timeout=120s
+
+

Now we can apply the metallb custom resources.... see: https://metallb.universe.tf/configuration/ +Sometimes it still fails, but just keep trying, if you get an error for the custom resource not being available: +

 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
cat <<EOF | kubectl apply -f -
+  apiVersion: metallb.io/v1beta1
+  kind: IPAddressPool
+  metadata:
+    name: base-pool
+    namespace: kube-system
+  spec:
+    addresses:
+      - "$CIDR"
+EOF
+
+cat <<EOF | kubectl apply -f -
+  apiVersion: metallb.io/v1beta1
+  kind: L2Advertisement
+  metadata:
+    name: base-pool
+    namespace: kube-system
+EOF
+    sleep 3
+done
+
+

Installing nginx ingress controller

+
1
helm install nginx-ingress ingress-nginx/ingress-nginx --namespace kube-system --set hostNetwork=true --set hostPort.enabled=true
+
+

wait on nginx ingress controller to deploy +

1
+2
+3
+4
+5
+6
kubectl rollout status -n kube-system deployment/nginx-ingress-ingress-nginx-controller
+
+kubectl wait --namespace kube-system \
+  --for=condition=ready pod \
+  --selector=app.kubernetes.io/name=ingress-nginx \
+  --timeout=90s
+
+

Installing cert-manager

+
1
helm install cert-manager jetstack/cert-manager --namespace kube-system --version v1.9.1 --set installCRDs=true
+
+

Wait on cert-manager to deploy +

 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
kubectl rollout status -n kube-system deployment/cert-manager
+kubectl rollout status -n kube-system deployment/cert-manager-webhook
+
+kubectl wait --namespace kube-system \
+  --for=condition=ready pod \
+  --selector=app.kubernetes.io/name=cert-manager \
+  --timeout=90s
+kubectl wait --namespace kube-system \
+  --for=condition=ready pod \
+  --selector=app.kubernetes.io/component=webhook \
+  --timeout=90s
+
+

Install a ClusterIssuer so that cert-manager can issue certs

+

Self Signed Issuer

+
1
+2
+3
+4
+5
+6
+7
    cat <<EOF | kubectl apply -f -
+    apiVersion: cert-manager.io/v1
+    kind: ClusterIssuer
+    metadata:
+      name: selfsigned-cluster-issuer
+    spec:
+      selfSigned: {}
+
+

lets-encrypt staging Issuer

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
cat <<EOF | kubectl apply -f -
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+  name: letsencrypt-staging
+spec:
+  acme:
+    email: $EMAIL
+    server: https://acme-staging-v02.api.letsencrypt.org/directory
+    privateKeySecretRef:
+      name: letsencrypt-staging
+    solvers:
+      - http01:
+          ingress:
+            class: nginx
+EOF
+
+

Installing prometheus

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
helm install prometheus prometheus-community/kube-prometheus-stack --namespace monitoring --create-namespace \
+    --set alertmanager.ingress.enabled=true \
+    --set alertmanager.ingress.ingressClassName=nginx \
+    --set alertmanager.ingress.hosts[0]=$ALERT_MANAGER_DOMAIN \
+    --set grafana.ingress.enabled=true \
+    --set grafana.ingress.ingressClassName=nginx \
+    --set grafana.ingress.hosts[0]=$GRAFANA_DOMAIN \
+    --set prometheus.ingress.enabled=true \
+    --set prometheus.ingress.ingressClassName=nginx \
+    --set prometheus.ingress.hosts[0]=$PROMETHEUS_DOMAIN
+
+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_distros/kind/kind/index.html b/k8s_distros/kind/kind/index.html new file mode 100644 index 0000000000..bb54182550 --- /dev/null +++ b/k8s_distros/kind/kind/index.html @@ -0,0 +1,2202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Kind - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

About

+ +

Kind is a tool for running local Kubernetes clusters using Docker container "nodes". kind was primarily designed for testing Kubernetes itself, but may be used for local development or CI. If you have go ( 1.17+) and docker installed, this should be all you need to get started, in theory:

+
1
go install sigs.k8s.io/kind@v0.15. && kind create cluster
+
+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_distros/kind/quickstart/index.html b/k8s_distros/kind/quickstart/index.html new file mode 100644 index 0000000000..6e01f0ae1d --- /dev/null +++ b/k8s_distros/kind/quickstart/index.html @@ -0,0 +1,2296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Kind BASH Quickstart - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

QuickStart

+ +

Best path for non-prod testing across linux and macOS

+
1
+2
+3
+4
+5
+6
# this export can also be set in a .env file in the same dir
+export EMAIL="youremail@coolemail4dogs.com"
+
+# From the cloned repo dir, This should set up KinD for you
+# Will also launch k9s, like top for k8s, To exit k9s, use type :quit
+./bash_scripts/kind/bash_full_quickstart.sh
+
+

Ready to clean up this cluster?

+

To delete the whole cluster, run:

+
1
kind delete cluster
+
+

Final Touches

+

Port Forwarding

+

If you want to access an app outside of port forwarding to test, you'll need to make sure your app's ingress is setup correctly and then you'll need to setup your router to port forward 80->80 and 443->443 for your WAN. then setup DNS for your domain if you want the wider internet to access this remotely.

+

SSL/TLS

+

After SSL is working (if it's not, follow the steps in the cert-manager common error troubleshooting guide), you can also change the letsencrypt-staging value to letsencrypt-prod for any domains you own and can configure to point to your cluster via DNS.

+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_distros/kind/tutorial/index.html b/k8s_distros/kind/tutorial/index.html new file mode 100644 index 0000000000..0e120e3476 --- /dev/null +++ b/k8s_distros/kind/tutorial/index.html @@ -0,0 +1,2531 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Kind Tutorial - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Tutorial

+ +

Stack we install in this tutorial

+
    +
  • KinD (kubernetes in docker)
  • +
  • nginx-ingress-controller (for remote access)
  • +
  • cert-manager (automatic SSL)
  • +
+

PreReq

+ +

Optional: +- k9s (brew install k9s)

+

add/update metallb and cert-manager helm repos

+
1
+2
+3
helm repo add metallb https://metallb.github.io/metallb
+helm repo add jetstack https://charts.jetstack.io
+helm repo update
+
+

Create the kind cluster

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
cat <<EOF | kind create cluster --config=-
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+nodes:
+- role: control-plane
+  kubeadmConfigPatches:
+  - |
+    kind: InitConfiguration
+    nodeRegistration:
+      kubeletExtraArgs:
+        node-labels: "ingress-ready=true"
+  extraPortMappings:
+  - containerPort: 80
+    hostPort: 80
+    protocol: TCP
+  - containerPort: 443
+    hostPort: 443
+    protocol: TCP
+EOF
+
+

Grab your kube config

+
1
sudo kind get kubeconfig > ~/.kube/kubeconfig
+
+

MetalLb installation

+

Learn more about metallb here.

+
1
helm install metallb metallb/metallb -n kube-system --wait
+
+

Wait on metallb to deploy, because sometimes helm wait doesn't do the trick: +

 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
kubectl rollout status -n kube-system deployment/metallb-controller
+
+kubectl wait --namespace kube-system \
+  --for=condition=ready pod \
+  --selector=app.kubernetes.io/instance=metallb \
+  --timeout=120s
+
+kubectl wait --namespace kube-system \
+  --for=condition=ready pod \
+  --selector=app.kubernetes.io/component=speaker \
+  --timeout=120s
+
+

Now we can apply the metallb custom resources.... see: https://metallb.universe.tf/configuration/ +Sometimes it still fails, but just keep trying, if you get an error for the custom resource not being available: +

 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
cat <<EOF | kubectl apply -f -
+  apiVersion: metallb.io/v1beta1
+  kind: IPAddressPool
+  metadata:
+    name: base-pool
+    namespace: kube-system
+  spec:
+    addresses:
+      - "$CIDR"
+EOF
+
+cat <<EOF | kubectl apply -f -
+  apiVersion: metallb.io/v1beta1
+  kind: L2Advertisement
+  metadata:
+    name: base-pool
+    namespace: kube-system
+EOF
+    sleep 3
+done
+
+

Deploy the Nginx Ingress Controller

+

Kind has a special deploy.yml maintained by the kuberentes project that makes this really easy:

+
1
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
+
+

Wait on nginx ingress controller to deploy:

+
1
+2
kubectl rollout status deployment/ingress-nginx-controller -n ingress-nginx
+kubectl wait --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=90s -n ingress-nginx
+
+

Set up cert manager

+

Don't forget the --set installCRDs=true! +

1
+2
+3
+4
helm install cert-manager jetstack/cert-manager \
+    --namespace kube-system \
+    --version v1.9.1 \
+    --set installCRDs=true 
+
+

Wait on cert-manager to deploy +

 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
kubectl rollout status -n kube-system deployment/cert-manager
+
+kubectl rollout status -n kube-system deployment/cert-manager-webhook
+
+kubectl wait --namespace kube-system \
+  --for=condition=ready pod \
+  --selector=app.kubernetes.io/name=cert-manager \
+  --timeout=90s
+
+kubectl wait --namespace kube-system \
+  --for=condition=ready pod \
+  --selector=app.kubernetes.io/component=webhook \
+  --timeout=90s
+
+

You can also use k9s to monitor the cluster and wait for resources to come up.

+

Installing ClusterIssuer Resource

+

After cert-manager is completely up, you can apply this if you're going to test with external internet facing hosts: +

 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
    cat <<EOF | kubectl apply -f -
+    apiVersion: cert-manager.io/v1
+    kind: ClusterIssuer
+    metadata:
+      name: letsencrypt-staging
+    spec:
+      acme:
+        email: $EMAIL
+        server: https://acme-staging-v02.api.letsencrypt.org/directory
+        privateKeySecretRef:
+          name: letsencrypt-staging
+        solvers:
+          - http01:
+              ingress:
+                class: nginx
+EOF
+
+

That's it :)

+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_toolbox/helm/index.html b/k8s_toolbox/helm/index.html new file mode 100644 index 0000000000..90b210be4a --- /dev/null +++ b/k8s_toolbox/helm/index.html @@ -0,0 +1,2280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Helm - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Helm

+ +

Helm is a popular package manager for k8s and is generally the default standard, alongside kustomize.

+

Installation on Debian

+
1
+2
+3
+4
+5
curl https://baltocdn.com/helm/signing.asc | sudo apt-key add -
+sudo apt-get install apt-transport-https --yes
+echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
+sudo apt-get update
+sudo apt-get install helm
+
+

Troubleshooting

+

Helm3 template errors

+
+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_toolbox/k9s/index.html b/k8s_toolbox/k9s/index.html new file mode 100644 index 0000000000..f529395d21 --- /dev/null +++ b/k8s_toolbox/k9s/index.html @@ -0,0 +1,2244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + K9s - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

K9s

+ +

K9s

+

K9s is a CLI to manager your clusters from the terminal. I personally never deploy the default kubernetes dashboard anymore and instead just use a combination of k9s and kubectl with plugins installed with krew.

+

K9s also has plugins of its own, and integrates with additional k8s tooling, like Popeye, a utility that scans live Kubernetes cluster and reports potential issues with deployed resources and configurations.

+

Also, check it out, their mascot is a dog. Get it? K9s? :D

+

+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_toolbox/kubectl/index.html b/k8s_toolbox/kubectl/index.html new file mode 100644 index 0000000000..4266d64371 --- /dev/null +++ b/k8s_toolbox/kubectl/index.html @@ -0,0 +1,2348 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Kubectl - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Kubectl

+ +

kubectl is the default CLI for Kuberenetes. I use it mostly to apply things directly, or within simple BASH scripts for automation.

+

Krew

+

krew is a package manager for kubectl plugins.

+

Installation

+

Check out the current installation docs here, but you should be able to run (on macOS/Linux):

+
1
brew install krew
+
+

Installing Plugins with Krew

+

Example for installing the ctx plugin:

+
1
kubectl krew install ctx
+
+

Plugins I actually use

+ + + + + + + + + + + + + + + + + + + + + + + + + +
PluginWhy/What
ctxkubeconfig context switching to switch to other clusters
nsswitch to different namespaces in the current kubeconfig cluster/context
exampleoutputs example yaml files for a given cluster resource
deprecationscheck which cluster resources are deprecated/will be deprecated soon
+

todo: fill these in with ascinemas. +Check out the examples below to see how they're used.

+

ns

+

example

+

deprecations

+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/k8s_toolbox/toolbox/index.html b/k8s_toolbox/toolbox/index.html new file mode 100644 index 0000000000..7b03871d4c --- /dev/null +++ b/k8s_toolbox/toolbox/index.html @@ -0,0 +1,2301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + About - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

About

+ +

Toolbox

+

Notes on tools for interacting with k8s.

+

Install kubectl plugins with krew

+

Krew is a plugin manager for kubectl plugins. You can install it with brew and update plugins with kubectl krew update

+

These together make namespace switching better. Learn more about kubectx + kubens.

+
1
+2
kubectl krew install ctx
+kubectl krew install ns
+
+

This will help with generating example k8s resources:

+
1
kubectl krew install example
+
+

This one helps find deprecated stuff in your cluster:

+
1
kubectl krew install deprecations
+
+

To install plugins from a krew file, you just need a file with one plugin per line. You can use this one:

+
1
+2
+3
curl -O https://raw.githubusercontent.com/jessebot/smol-k8s-lab/main/deps/kubectl_krew_plugins
+
+kubectl krew install < kubectl_krew_plugins
+
+

k8s shell aliases

+

Add some helpful k8s aliases:

+
1
+2
+3
+4
+5
# copy the file
+curl -O https://raw.githubusercontent.com/jessebot/dot_files/main/.bashrc_k8s
+
+# load the file for your current shell
+source ~/.bashrc_k8s
+
+

To have the above file sourced every new shell, copy this into your .bashrc or .bash_profile:

+
1
+2
+3
+4
# include external .bashrc_k8s if it exists
+if [ -f "$HOME/.bashrc_k8s" ]; then
+    . $HOME/.bashrc_k8s
+fi
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mathjax.js b/mathjax.js new file mode 100644 index 0000000000..bf37d74537 --- /dev/null +++ b/mathjax.js @@ -0,0 +1,16 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } + }; + + document$.subscribe(() => { + MathJax.typesetPromise() + }) \ No newline at end of file diff --git a/notes/notes/index.html b/notes/notes/index.html new file mode 100644 index 0000000000..98ec670e12 --- /dev/null +++ b/notes/notes/index.html @@ -0,0 +1,2195 @@ + + + + + + + + + + + + + + + + + + + + + + + Notes - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Notes

+ +

Here's where I dump various notes on different apps you can host on k8s, as well as notes on various tools for kubernetes.

+

Port Forwarding

+

If you want to access an app outside of port forwarding to test, you'll need to make sure your app's ingress is setup correctly and then you'll need to setup your router to port forward 80->80 and 443->443 for your WAN. then setup DNS for your domain if you want the wider internet to access this remotely.

+ +
+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/roadmap/index.html b/roadmap/index.html new file mode 100644 index 0000000000..fdeaf4f5f0 --- /dev/null +++ b/roadmap/index.html @@ -0,0 +1,2296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + RoadMap - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

RoadMap

+ +

RoadMap

+

Here we'll document our general task list going forward.

+

Major Features

+
    +
  • Support OpenBao in place of Bitwarden and the Appset Secrets Plugin
  • +
  • Support setting up an initial cluster via SSH (this would prevent you needing to log into a node to initially setup k8s before you can manage it remotely as normal via smol-k8s-lab)
  • +
  • Make the bitwarden feature a generic password manager feature. See small-hack/smol-k8s-lab:issues#45
  • +
  • support local keyring
  • +
  • support 1password
  • +
  • support OpenBao
  • +
  • improve make_screenshots.py. See small-hack/smol-k8s-lab:issue#101
  • +
  • handle both existing and non-existing clusters
  • +
  • create gif with screenshots for the README, docs, and pypi
  • +
  • Thorough Diagrams, see: small-hack/smol-k8s-lab:issues#34:
  • +
  • Diagram showing each phase of smol-k8s-lab
  • +
  • Diagram describing backups
  • +
  • Diagram describing restores
  • +
  • support disabling backups
  • +
  • support a generic backup section for all custom apps
  • +
  • support a security scan of all k8s resources
  • +
  • update docs for our monitoring stack
  • +
+

Minor Features

+
    +
  • Support a dropdown menu in the TUI for sensitive values to select from environment variable or bitwarden
  • +
  • Support a dropdown menu in the TUI for restic snapshot IDs to choose from, maybe this could also have a calendar feature to choose a date?
  • +
  • Flesh out sensitive values from bitwarden and OpenBao
  • +
  • Fix the issue where clicking an app on the apps screen causes it to be disabled/enabled. See Textualize/textual:discussions#4478 for more info.
  • +
+

Contributing to smol-k8s-lab

+

If you'd like to help with smol-k8s-lab, please see our contributing doc

+ + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000000..ccc051d423 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,293 @@ + + + + https://small-hack.github.io/smol-k8s-lab/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/cli/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/config_file/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/installation/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/roadmap/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/apps/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/appset-secret-plugin/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/argocd/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/bitwarden_eso_provider/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/cert_manager/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/cnpg_operator/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/core_dns/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/external-secrets-operator/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/generic_app/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/generic_device_plugin/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/home_assistant/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/ingress_nginx/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/k8tz/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/k8up/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/keycloak/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/mastodon/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/matrix/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/metallb/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/netmaker/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/nextcloud/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/prometheus/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/seaweedfs/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/vouch/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/zitadel/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/experimental/cilium/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/experimental/infisical/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/experimental/kepler/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/experimental/kubevirt/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/experimental/kyverno/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/experimental/longhorn/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/experimental/minio/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_apps/experimental/postgres_operator/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_distros/distros/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_distros/k3d/k3d/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_distros/k3s/k3s/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_distros/k3s/quickstart/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_distros/k3s/sqllite_backend/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_distros/k3s/tutorial/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_distros/kind/kind/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_distros/kind/quickstart/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_distros/kind/tutorial/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_toolbox/helm/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_toolbox/k9s/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_toolbox/kubectl/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/k8s_toolbox/toolbox/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/notes/notes/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/tui/apps_screen/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/tui/confirmation_screen/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/tui/create_modify_screens/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/tui/distro_screen/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/tui/help_screen/ + 2024-07-12 + daily + + + https://small-hack.github.io/smol-k8s-lab/tui/tui_config/ + 2024-07-12 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000000..fd20d7a704 Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/stylesheets/extra.css b/stylesheets/extra.css new file mode 100644 index 0000000000..feb25f996a --- /dev/null +++ b/stylesheets/extra.css @@ -0,0 +1,379 @@ +:root { + /* set global font */ + --md-text-font: "Firacode"; + --md-code-font: "Mononoki"; + + --gloabl-padding: .5%; + --header-padding: 0; + --list-padding: .5%; + --list-decorator: "> "; + --margins: 0%; + + --letter-spacing-f1: .5px; + --letter-spacing-f2: 1px; + + --font-size: 18px; + --line-height: 20px; + + --title-line-height: 1; + --elipsis-multiplier: 1; + --header1-multiplier: 1.2; + --header2-multiplier: 1.1; + --header3-multiplier: 1; + --header4-multiplier: 1; + --link-multiplier: 1.0; + --emp-multiplier: 1.0; + --blockquote-multiplier: 1; + --list-multiplier: 1; + + --font-weight-h1: 600; + --font-weight-h2: 600; + --font-weight-h3: 500; + --font-weight-h4: 500; + --font-weight-link: 400; + --font-weight-text: 400; + --font-weight-emp: 400; + --font-weight-list: 400; + + --shadow-x-max: .3; + --shadow-y-max: 1; + --shadow-radius: 1px; + --brightness: 90%; + + --text-y: -2px; + --text-x: -5px; + --text-z: -0px; +} + +[data-md-color-scheme="spacechalk"] { + + --bg: #232336; + --header: #23213b; + --header-contrast: rgb(58,58,58); + --code-bg: #323232; + --primary: rgb(255,175,249); + --pop: rgb(122,162,247); + + --shadow-x: 1px; + --shadow-x-max: .3; + --shadow-y: 1px; + --shadow-y-max: 1; + --shadow-radius: 2px; + --shadow: Black; + + + --text: #c0caf5; + --border-radius: 10px; + --border-style: solid; + + /* search bar cursor and active text */ + --md-default-fg-color: var(--text); + /* h1 headers and >'s, link highlights, TOC nav highlight, hover1 */ + --md-default-fg-color--light: var(--pop); + /* scroll bars and markers un-hovered */ + --md-default-fg-color--lighter: var(--pop); + /* horizontal lines and icons unhovered, search header background */ + --md-default-fg-color--lightest: var(--text); + /* HTML page Background color */ + --md-default-bg-color: var(--bg); + /*?*/ + --md-default-bg-color--light: rgba(49, 218, 7, .8); + /*?*/ + --md-default-bg-color--lighter: rgb(253, 99, 10); + /*?*/ + --md-default-bg-color--lightest: rgb(235, 14, 243); + /*header background*/ + --md-primary-fg-color: var(--header); + /*?*/ + --md-primary-fg-color--light: var(--header); + /*?*/ + --md-primary-fg-color--dark: var(--header); + /* header text and icon color */ + --md-primary-bg-color: #7dcfff; + /* search bar initial text */ + --md-primary-bg-color--light: var(--header-contrast); + /* Hover color2 + search syntax highlight */ + --md-accent-fg-color: var(--pop); + /* search result highlight */ + --md-accent-fg-color--transparent: var(--primary); + /*?*/ + --md-accent-bg-color: rgb(85, 95, 137); + /*?*/ + --md-accent-bg-color--light: rgba(235, 14, 243, 1); + /* default text color in a code block */ + --md-code-fg-color: #bdd8ff; + --md-code-bg-color: var(--code-bg); + --md-code-hl-color: rgb(207, 62, 200); + --md-code-hl-number-color: rgb(252, 17, 173); + --md-code-hl-special-color: red; + --md-code-hl-function-color: #6Df2E5; + --md-code-hl-constant-color: rgb(89, 214, 245); + --md-code-hl-keyword-color: #f7fb53; + --md-code-hl-string-color: #C1FF87; + --md-code-hl-name-color: red; + --md-code-hl-operator-color: #fdcd36; + --md-code-hl-punctuation-color: #f289f9; + --md-code-hl-comment-color: #7aa2f7; + --md-code-hl-generic-color: red; + --md-code-hl-variable-color: #5cc9fd; + /* Main Font Color */ + --md-typeset-color: var(--text); + --md-typeset-a-color: var(--pop); + /*?*/ + --md-typeset-mark-color: rgba(212,2,212,1); + /*?*/ + --md-typeset-del-color: rgba(255,25,255,1); + /*?*/ + --md-typeset-ins-color: rgba(255,25,255,1); + /* this is for keyboard keys - the color of the text */ + --md-typeset-kbd-color: #448; + /* this is for keyboard keys - the color of the accent around the text */ + --md-typeset-kbd-accent-color: rgb(86,95,137); + /* this is for keyboard keys - the buttom part of the keyboard button */ + --md-typeset-kbd-border-color: rgb(86,95,137); + /* color for table borders */ + --md-typeset-table-color: rgb(86,95,137); + /*?*/ + --md-admonition-fg-color: var(--text); + /*?*/ + --md-admonition-bg-color: #323232; +} + +a:active, a:link { + color: #a3a8f8; + font-size: calc(var(--link-multiplier) * var(--font-size)); + line-height: var(--title-line-height); + } + +a:hover { + color: var(--text); + filter: drop-shadow(var(--shadow-x) var(--shadow-y) var(--shadow-radius) var(--pop)); + } + +strong, emp { + color: var(--text); + font-size: calc(var(--emp-multiplier) * var(--font-size)); + line-height: var(--title-line-height); + font-weight: var(--font-weight-emp)!important; +} + +/* top links in the header */ +.md-tabs__link { + background: var(--transparent); + font-size: calc(var(--link-multiplier) * var(--font-size)); + font-weight: var(--font-weight-link); + line-height: var(--title-line-height); + color: #aea8f8!important; +} + +.md-header, .md-header--lifted{ + background-color: var(--header); +} + +/* the logo on the left of the title */ +.md-header__button md-logo { + color: red; +} + +/* for the site title next to the logo */ +.md-header__topic .md-ellipsis { + color: #5cc9fd; +} + +/* TOC nav links on the left */ +.md-nav__link, a.md-button:active { + color: #a3a8f8; + font-weight: var(--font-weight-text); +} + + +/* nav title in the collapsible left hand menu when screen is small */ +.md-nav__title { + background: var(--bg)!important; + color: #7aa2f7!important; + padding-left: 5%!important; + padding-top: 2%; + padding-bottom: 2%; + font-family: var(--md-code-font); +} + +.md-nav__list, .md-nav, .md-nav--primary, .md-nav--lifted, .md-nav--integrated { + background: var(--bg)!important; + padding: var(--global-padding); +} + +.highlight { + background: rgba(0,0,0,1); +} + +.md-ellipsis { + font-size: calc(var(--elipsis-multiplier) * var(--font-size)); + font-weight: var(--font-weight-link); +} + +.md-nav__link--active { + color: var(--text); +} + +h1, h2, h3, h4 { + line-height: var(--title-line-height)!important; + color: #7aa2f7; + font-family: var(--md-code-font)!important; +} + +h1 { + font-size: calc(var(--header1-multiplier) * var(--font-size)); + font-weight: var(--font-weight-h1)!important; +} + +h2 { + font-size: calc(var(--header2-multiplier) * var(--font-size)); + font-weight: var(--font-weight-h2)!important; +} + +h3 { + font-size: calc(var(--header3-multiplier) * var(--font-size)); + font-weight: var(--font-weight-h3)!important; +} + +h4 { + font-size: calc(var(--header4-multiplier) * var(--font-size)); + font-weight: var(--font-weight-h4)!important; +} + +h5 { + font-size: calc(var(--header4-multiplier) * var(--font-size)); + font-weight: var(--font-weight-h4)!important; +} + +ol, li { + font-size: calc(var(--list-multiplier) * var(--font-size)); + font-weight: var(--font-weight-list)!important; +} + +strong { + font-size: calc(var(--list-multiplier) * var(--font-size)); + font-weight: var(--font-weight-list)!important; + list-style: none; +} + +blockquote { + color: var(--text); + font-size: calc(var(--font-size) * var(--blockquote-multiplier)); + font-weight: var(--font-weight-text)!important; +} + +.wrap { + width: 200px; + margin: 0 auto; + background: white; +} + +.md-grid { + max-width: 80vw; +} + +.md-button { + filter: drop-shadow(var(--shadow-x) var(--shadow-y) var(--shadow-radius) var(--header)); +} + +.md-icon { + color: var(--pop)!important; +} + +.md-footer { + background: var(--header)!important; +} + +.md-typeset .admonition.note { + border-color: #3d59a1; + background-color: var(--header); +} + +.md-typeset blockquote { + border-left: .2rem solid var(--md-default-fg-color--lighter); + color: var(--text); + padding-left: .6rem; +} + +body { + padding: var(--global-padding); + font-size: var(--font-size); + margin-top: var(--margin); + margin-bottom: var(--margin); + margin-right: var(--margin); + margin-left: var(--margin); + margin: 0; + box-shadow: inset 0 0 100% black; + height: 100vh; + width: 100%; + z-index: -100; +} + +p { + font-size: var(--font-size)!important; + text-indent: 0%; + line-height: var(--line-height); + letter-spacing: var(--letter-spacing-f1); + color: var(--text); + font-weight: var(--font-weight-text)!important; + } + +.card-container { + display: flex; + justify-content: center; + flex-direction: row; + font-family: var(--md-code-font); + color: var(--header-contrast); + z-index: 100; + margin-top: auto; + filter: drop-shadow(2px 2px 5px rgb(0 0 0 / 0.8)); + border-radius: var(--border-radius); + background-color: var(--card-bg); + } + +.md-typeset details{ + padding-left: 1%; + padding-right: 1%; + overflow: hidden; + border-left: .1rem solid #448aff; + border-right: .1rem solid #448aff; + border-bottom: .1rem solid #448aff; +} + +.md-typeset .example > summary::before { + background-color: var(--header-contrast); + padding: var(--global-padding); +} + +.md-typeset .example > summary { + background-color: var(--header); + border-color: var(--header); + color: var(--header-contrast); +} + +.md-typeset details.example { + border-color: var(--header); +} + +.md-typeset details{ + filter: drop-shadow(2px 2px 5px rgb(0 0 0 / 0.8)); +} + +.md-footer-meta { + display: none; + +} + +.code, .lineos, .highlighttable { + font-size: var(--font-size)!important; +} + +.md-typeset__table table:not([class]) td, +.md-typeset__table table:not([class]) th { + padding: 9px; +} + +.md-typeset__table th { + color: #448aff; +} diff --git a/stylesheets/fonts.css b/stylesheets/fonts.css new file mode 100644 index 0000000000..723892c81b --- /dev/null +++ b/stylesheets/fonts.css @@ -0,0 +1,23 @@ +@font-face { + font-family: "Mononoki"; + src: url("../assets/fonts/MononokiNerdFont-Regular.ttf"); + font-weight: normal; +} + +@font-face { + font-family: "Mononoki Bold"; + src: url("/assets/fonts/MononokiNerdFontMono-Bold.ttf"); + font-weight: bold; +} + +@font-face { + font-family: "Mononoki Italic"; + src: url("/assets/fonts/MononokiNerdFont-Italic.ttf"); + font-weight: italic; +} + +@font-face { + font-family: "Mononoki Bold Italic"; + src: url("/assets/fonts/MononokiNerdFont-BoldItalic.ttf"); + font-weight: bold italic; +} diff --git a/tui/apps_screen/index.html b/tui/apps_screen/index.html new file mode 100644 index 0000000000..5c0ac462f6 --- /dev/null +++ b/tui/apps_screen/index.html @@ -0,0 +1,2500 @@ + + + + + + + + + + + + + + + + + + + + + + + + + K8s Apps - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

K8s Apps

+ +

The TUI features an applications screen to modify or create new Argo CD Applications for your cluster.

+

screenshot of the application configuration screen for the smol-k8s-lab TUI. On the top left-hand side, there is a list of applications (titled Select apps) that can be scrolled through with arrow keys once selected. It will always have one item in the list highlighted. On the top right-hand side, there is a configuration menu (titled Configure parameters for argo-cd) for the highlighted application from the left hand list. The configuration shows options for initialization with a switch to enable or disable it, and then headers and inputs for the following: Argo CD Application Configuration which has inputs for: repo, path, ref, and namespace. Template values for Argo CD ApplicationSets which has inputs for parameters that can passed to Argo CD ApplicationSets and in this screenshot shows an input for hostname. There are more headers that will be discussed further on in the docs. On the left-hand side below the apps list are two buttons: top button is ✨ New App, bottom button is ✏️ Modify Globals. Final box on the screen below all previously described elements is the App Description which shows a description and link to https://argo-cd.readthedocs.io/en/stable/

+

+

Selecting Applications

+

The left hand side [SelectionList] can be clicked or navigated using your arrow keys. To enable an app, you can either click it, or you can use the spacebar or enter keys. Only selected apps will be installed on your cluster.

+

Modifying a simple or custom Application

+

To modify an application, ensure it's highlighted in the left hand list and then you can modify the parameters under each section described below:

+

Argo CD Application Configuration

+

This section contains parameters to configure a directory-type Argo CD Application which includes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
parameterdescription
repogit repository to use for your Argo CD Application
pathpath in git repo to your kubernetes manifest files you'd like to deploy
revisiongit branch or git tag to point to in the git repo
clusterKubernetes cluster to deploy this Argo CD Application to
namespaceKubernetes namespace to deploy your Argo CD Application in
directory_recursionEnabled recursive directory crawl of Argo CD repo for applying nested manifests
+

Template Values for Argo CD ApplicationSet

+

This section for modifying and/or adding values for this the currently selected ApplicationSet using the [appset secret plugin] to provide variables to the Argo CD Application at creation time.

+

Argo CD Project Configuration

+

This section is for modifying the [Argo CD Project] parameters, which currently includes the following:

+ + + + + + + + + + + + + + + + + + + + + +
parameterdescription
namename of the Argo CD Project
destionation.namespacesnamespaces that the Argo CD Applications are allowed to operate in
source reposallow list of repos that Argo CD applications can be sourced from
+

Modifying an init-supported application

+

Some applications, such as nextcloud, matrix, and mastodon, support a special init phase for credentials creation, or restoration if backups are supported as well. When this is possible, you'll see a tabbed view for the configuration panel on the right hand side of the apps screen. It will include 4 tabs. We'll go through each below.

+

Initialization Configuration

+

This phase includes setting up one time passwords in your password manager and in a kubernetes secret for credentials such as your admin credentials or SMTP credentials. To use the same name for an app without using our custom initialization process, please click the switch next to "Initialization Enabled" to set it to disabled. This will then treat this app as a normal custom app.

+

The values pictured in the screenshot below translate to the following YAML: +

 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
apps:
+  nextcloud:
+    # initialize the app by setting up new k8s secrets and/or bitwarden items
+    init:
+      enabled: true
+      values:
+        # change the name of your admin user to whatever you like. This is used in an admin credentials k8s secret
+        admin_user: my_nextcloud_admin
+        smtp_user: my_smtp_nextcloud_username
+        smtp_host: smtp-server.com
+        # this value is taken from an environment variable
+        smtp_password:
+          value_from:
+            env: NC_SMTP_PASSWORD
+
+

terminal screenshot of smol-k8s-lab on the apps screen with the nextcloud app selected. It shows a tabbed configuration panel on the right hand side that shows the following tabs: initialization config, argo cd app config, backup, restore. Init config is selected which shows a switch to enable or disable init, followed by a header that says init values. below that are input fields specific to nextcloud's init phase. They include: admin user set to my_nextcloud_admin, smtp user set to my_smtp_nextcloud_username, smtp host set to smtp-server.com, and smtp password, which is currently blank. Because the last field is blank, it has a pink border showing the input is invalid. At the bottom of the configuration panel are two border links: sync, delete

+

Backups Configuration

+

Backups are done via k8up which is a wrapper around restic. For apps using cloud native postgres operator created clusters, we support both backup and restore of the database. For nextcloud specifically, we also put the database into maintainence mode. Here's an example of what you'll see in the TUI:

+

terminal screenshot of smol-k8s-lab on the apps sceen with the nextcloud app selected. it shows a tabbed config panel on the right hand side that shows the following tabs: initialization config, argo cd app config, backup, restore. Backup is currently selected. The backup tab starts with a centered button called 💾 Backup Now. Below that is a header that says 📆 Scheduled Backups. Below that are two input fields. First input is PVC schedule set to 10 0 astrik astrik astrik. Second input is DB schedule set to 0 0 0 astrik, astrik, astrik. Below that is a collapsible header called S3 Configuration. In the collapsible there are three input fields visible: endpoint, set to backblaze endpoint, bucket set to my-remote-s3-bucket, and region set to eu-central-003. The backup tab is long enough that it features a scrollbar.

+

For more on backups, see Config File > Backups.

+

Restoring from Backup Configuration

+

To restore from a backup, you'll need to configure if you'd like to restore PVCs and the CNPG postgresql database or just the PVC. To do just the PVC set the "Restore 🐘 CNPG cluster enabled" switch to disabled by clicking it.

+

By default, we always use the latest restic snapshot ID to restore your cluster. If you'd like to use different snapshot IDs, please change the word "latest" for each PVC.

+

terminal screenshot of smol-k8s-lab on the apps sceen with the nextcloud app selected. it shows a tabbed config panel on the right hand side that shows the following tabs: initialization config, argo cd app config, backup, restore. The restore tab is selected and it features three header rows followed by some fields. First header row says restore from backup enabled and has a switch set to on, second header row says restore cnpg cluster enabled and has a switch set to on, and the third header row says Restic Snapshot IDs with the following input fields below, all set to latest: seaweedfs_volume, seaweedfs_filer, and nextcloud_files.

+

For more on restores, see Config File > Restores.

+

Deleting an Application

+

To delete an existing Argo CD Application, you can click the delete link at the bottom of the app configuration panel on the right. After you click it, you will see the following modal screen. If you click the checkbox next to word Force, you will pass in --force to the argocd app delete command when we run it. If/when you click the delete button, we will delete the app, associated appsets, and everything in the namespace.

+

terminal screenshot of smol

+
+

Note

+

The delete link will only be present if you're modifying an existing cluster, and the app is already present in Argo CD.

+
+

Syncing an Application

+

To sync an existing Application, click the "sync" link at the bottom of the app configuration panel on the right.

+
+

Note

+

The sync link will only be present if you're modifying an existing cluster, and the app is already present in Argo CD.

+
+

Adding new Applications

+

To add a new application, select the "✨ New App" button under the Select apps list, which will display this modal screen:

+

terminal screenshot of smol-k8s-lab new app modal screen, which shows a header that says Please enter a name and description for your application and two input fields and a Submit button: input 1: Name of your Argo CD Application, input 2: (optional) Description of your Argo CD Application

+

Enter a name for your app, and an optional description and select Submit. To cancel this action, you can either click the cancel link in the bottom border, or you can hit the esc key.

+

Modifying Globally Available Templating Parameters for Argo CD ApplicationSets

+

To modify globally available templating parameters for all Argo CD ApplicationSets, select the second button the left hand side called "✏️ Modify Globals" which will launch a modal screen like this:

+

terminal screenshot of smol-k8s-lab modify globals modal screen. Shows a box with a blue border around it and a header that says Modify globally available Argo CD ApplicationSet templating values and then by default one input field called cluster issuer with pre-populated text: 'letsencrypt-staging'. Below that is a row with an add button featuring a plus sign and another input field that says new key name. Below that is a link in the border that says close.

+

To close this modal screen, you can either click the close link in the bottom border, or you can hit the esc key.

+

Invalid Apps

+

If you have any Applications enabled that have invalid fields (empty fields), you'll see a screen like this:

+

terminal screenshot of smol-k8s-lab invalid apps screen, a box with an orange border and border title of The following app fields are empty. below it says Click the app links below to fix the errors or disable them. Below that there is a datatable with columns Application and Invalid fields. The rows read: argo-cd, hostname. cert_manager, email. k8up, timezone. metallb, address_pool. vouch, domains, emails, hostname. zitadel, username, email, first_name, last_name, hostname

+

To fix this, just click each app and either disable them by clicking the heart next app you don't want to use, or fill in any field that is empty, which should also be highlighted in pink.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tui/confirmation_screen/index.html b/tui/confirmation_screen/index.html new file mode 100644 index 0000000000..2c86397e52 --- /dev/null +++ b/tui/confirmation_screen/index.html @@ -0,0 +1,2249 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Confirmation - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Confirmation

+ +

Confirming your configuration

+

After you've taken a look through all the configuration screens, you'll be blessed by this final overview screen that lets you check out each section of the config file without the TUI:

+

<img src="../../assets/images/screenshots/confirm_screen.svg" alt="terminal screenshot of the smol-k8s-lab confirmation screen. At the top it says smol k8s lab - Review your configuration (last step!) and then there is one main large box titled Review All Values that contains 4 tabs: Core config, K8s Distro Config, Apps Config, and Global Parameters Config. Under each tab is that section of the smol-k8s-lab config file with syntax highlighting. Below the main box on the screen are two buttons: 🚆 Let's roll!, ✋Go Back.">

+

Bitwarden screen

+

If you haven't exported your Bitwarden credentials as env vars (BW_PASSWORD, BW_CLIENTID, and BW_CLIENTSECRET), then after you hit the "Let's Go" button, you'll see this screen:

+

<img src="../../assets/images/screenshots/bitwarden_prompt.svg" alt="terminal screenshot of the smol-k8s-lab confirmation screen. At the top it says smol k8s lab - Review your configuration (last step!). Below that is a large modal for filling out your bitwarden credentials. Behind that modal is the confirmation screen which is detailed above in the alt text for the first image on this page">

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tui/create_modify_screens/index.html b/tui/create_modify_screens/index.html new file mode 100644 index 0000000000..ae96633584 --- /dev/null +++ b/tui/create_modify_screens/index.html @@ -0,0 +1,2333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Create and Modify Clusters - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Create and Modify Clusters

+ +

This will launch the TUI by default, which will guide you through how to proceed via a series of tooltips:

+
1
smol-k8s-lab
+
+
+

Note

+

More accessibility features are on the roadmap for textual down the line, but please drop us a line if you'd like us to help with anything on our end in the meantime.

+
+

Create a New Cluster

+

To create a new cluster, fill in the name of your cluster (or use the randomized pre-populated name) and either click the submit button, or if there is only one box on the screen, you can hit enter. If there are two boxes on the screen, and the input is not selected, you also use the n key (for new/next).

+

That will bring you to the distro configuration screen to begin your configuration journey.

+

terminal screenshot of the smol-k8s-lab start screen. The screenshot shows smol-k8s-lab spelled out in block letters followed by one box containing two elements: an input field, pre-populated with a random cluster name, and a submit button for that input field.

+

Modify or Delete a Kubernetes Cluster

+

The start screen will look like this:

+

terminal screenshot of the smol-k8s-lab start screen. The screen shows a smol-k8s-lab banner spelled out in blocky letters followed by two boxes. The first box is for modifying or deleting an existing cluster with an example cluster in a table. The example cluster has the following fields: cluster: k3s-lovely-bunny, distro: k3s, version: v1.29.3+k3s1, platform: linux/amd64. The second box shows an input field for the name of a new cluster, which currently says

+

The top section will only be present if you already have (a) Kubernetes cluster(s) in your $KUBECONFIG.

+

Modify an Existing Cluster

+

To modify an existing cluster, select your cluster from the list of clusters (stored in a DataTable) in the top box.

+
+

Note

+

If you don't see a box with your clusters, the cluster is either not available in your $KUBECONFIG, not reachable, or you do not have any at this time.

+
+

You can use the tab key to scroll down the list of clusters and shift+tab to scroll up the list and then the enter key to select a cluster. You can also use your mouse to click on a cluster. After you select a cluster, you should see this "modal" (AKA pop-up) screen:

+

terminal screenshot showing smol-k8s-lab after selecting a cluster from the list. This shows the previous screen dimmed in the background with an overlaid modal screen featuring the text 'What would you like to do with k3s-lovely-bunny' and 3 buttons. Button 1: ✏️ Modify Apps, Button 2: 🖥️Modify Nodes, Button 3: Delete. At the bottom of the border for the modal screen, it has a link that says cancel.

+

To exit this screen, can either:

+
    +
  • select one of the buttons (modify apps, modify nodes, or delete)
  • +
  • click the cancel link at the bottom of the modal border
  • +
  • press B or Esc
  • +
+

Delete an existing Cluster

+

To delete a cluster, you can either click the Delete button, or use your tab key to select it and then the enter key to press the button. Then, you will get another modal screen asking you to confirm the deletion. If you select the yes button, which is the first button, smol-k8s-lab will attempt to delete the cluster if it is one of the following distros: k3s, k3d, or kind.

+

terminal screenshot showing smol-k8s-lab after selecting delete button. Shows a deletion confirmation modal screen that says 'Are you sure?' and it has two buttons: button 1: yes, button 2: cancel

+

Modify a Cluster's Apps

+

To modify a cluster's apps, from the start screen, select your cluster from the list in the first box, and then on the modal screen that appears, select the modify apps button. This will then automatically send you to the apps config screen where you can modify or add applications to your cluster.

+

Modify a Cluster's Nodes (k3s only)

+

For k3s clusters, we support deleting nodes, or adding new nodes. To do this, fromt he start screen, select the cluster you'd like to modify from the list in the first box, and then on the modal screen that appears, select Modify Nodes. That will launch this screen:

+

terminal screenshot showing smol-k8s-lab after selecting modify nodes button on cluster modal screen. The screen shows a title that says smol-k8s-lab - Kubernetes nodes config for k3s-lovely-bunny. Below that is a datatable with nodes. There is one node in the table with the following fields: node: smol-node, status: ready, type: worker, ssh port: 2222, ssh key: id_rsa, labels: reserved=iot, taints: reserved=iot:NoSchedule. Below the table is a header that says 🖥️Add a new node. Below that are a series of fields for a new node. The fields are: host input, node type drop down set to worker by default, ssh port input set to 22 by default, ssh key set to id_rsa by default, node labels input, node taints input. Below that is a footer with key binding hints. The hints are B for back, ctrl+n for add a new node, ? for help, c for config, f for hide footer, f5 for speak, and n for new cluster

+

There you can select a node by clicking on it or hitting the Enter key. When you do that it will display another modal screen like this: +terminal screenshot showing smol-k8s-lab after selecting a node. It is a small modal overlayed ontop of the previous screen. The text says: What would you like to do with smol-node? Below that text are three buttons. Buttons from the left: modify, delete, cancel.

+

You can select the modify or delete buttons to proceed. If you select delete you'll see this screen: +terminal screenshot showing smol-k8s-lab after selecting a node to delete. it is a small modal that says are you sure you want to delete smol-node? and below that it has a yes button and a cancel button.

+

To add a new node, just fill in the fields at the bottom and then hit Ctrl + N.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tui/distro_screen/index.html b/tui/distro_screen/index.html new file mode 100644 index 0000000000..7baae7011a --- /dev/null +++ b/tui/distro_screen/index.html @@ -0,0 +1,2330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + K8s Distros - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

K8s Distros

+ +

The "k8s distro config" screen is the first screen you will see in the TUI when creating a new cluster:

+

terminal screenshot of smol-k8s-lab k8s distro configuration screen . Features three vertically stacked boxes. Box 1: title: 🌱 Select a k8s distro. Contains a drop down menu on the left hand side that defaults to k3d on macOS and k3s on Linux. On the right hand side there is a description of the distro, which in this case is k3d is a lightweight wrapper to run k3s (Rancher Lab's minimal Kubernetes distro) in Docker containers. Subtitle: Inputs below are optional. Box 2: Title: Adjust how many of each node type to deploy. Contains two input fields: input 1: control plane is set to 1. input 2: workers is set to 0. Box 3: There are two tabs: k3s.yaml and Kubelet config Options. k3s.yaml is selected and the help text reads Add extra options for the k3s install script. Contains text: Add extra k3s options to pass to the k3s install script via a config file stored in $XDG_CACHE_HOME/smol-k8s-lab/k3s.yaml. Please use the second tab for extra kubelet args. Box 3 also contains input fields for: secrets encryption: true, disable: traefik, kubelet arg: max_pods=150, node label: ingress-ready=true. Each input field has a 🚮 button next to it. Box 3 subtitle: ➕ k3s option

+

+

Select Kubernetes Distro

+

To choose a distro, select one from the drop down Select in the first box. You can do this either with your mouse, or by hitting enter and then using the arrow keys to select a distro before hitting enter.

+

Configuring control plane and worker nodes

+

If you're using k3d or kind, you can deploy clusters with more than one node. By default we will always deploy one control plane node, but you can adjust this number as well as how many workers we use.

+

Extra options for k3s or k3d

+

If you're using k3d or k3s, you can adjust what k3s options are passed into the k3s config file.

+

Extra options for kind

+

If you're using kind, you can modify both the networking and kubelet configuration options.

+

terminal screenshot showing smol-k8s-lab k8s distro config screen with kind selected this time. The top two boxes are the same as the last screenshot, but the bottom box shows two tabs: networking, kubelet

+

Modify existing options

+

To modify an option, just change the text in any of the input boxes (you can click them, or navigate them with the tab/shift+tab keys).

+
+

Note

+

If you'd like to pass in multiple values for an option, use a comma separated list, e.g. disable: traefik, servicelb

+
+

To delete an option, use click the 🚮 button next to the input field.

+

Add new options

+

To add a new option, you can click the "➕ k3s option" link at the bottom of the box, or you can use the a hot key. This will bring up a modal screen with an input field for the new option.

+

terminal screenshot showing smol-k8s-lab "add new option" modal screen with a blue border. Header: Add *new* k3s option. The second row has an input field with placeholder text that says "new k3s option" with a button on the right hand side that says "➕ add option". The bottom border has a link that says cancel - which can be clicked or you can use the escape key

+

Enter the name of the new option you'd like to add.

+
+

Note

+

If you try to input an option that already exists, it will throw an error and make a bell sound. If you want to add an option that is already there, consider instead adding the extra values as a comma separated list.

+
+
+

Tip

+

To turn off bells, visit the TUI config screen

+
+

Add Nodes to k3s clusters (🆕 in v3.0)

+

The ability to other metal nodes to your cluster is exclusively to k3s in smol-k8s-lab. To add a new node in the TUI, make sure k3s is selected via the dropdown on the distro config screen. In the second box on the screen, there are three tabs, the final tab called "Add Remote Nodes" is the one you want to click. You can also use the left and right arrow keys to navigate the tabs.

+

Once there, you should see a list of any existing nodes you've added via your config file.

+

If you haven't added any clusters to your config file, you will see a random ascii art from a time in the past. The second half of the tab has a small form for you to add new nodes:

+

terminal screenshot showing smol-k8s-lab's add remote nodes tab for the k3s distro config screen. At the top is a selection field for which distro you'd like to select. Below that is a box allowing you to customize nodes. The Add Remote Nodes tab is selcted. Within the tab it says 'Add a node below for something to appear here...' and then there is an ascii text art of little totoros fromt he movie My neighbor totoro by duke lee. Below that is a header that says Add a new node, and below that are a series of inputs and dropdowns for adding a new node. from left to right, starting at th e top, the fields are: host input, node type (drop down set to worker), ssh port input set to 22, ssh key input set to id_rsa, node label input, node taints input. At the bottom of the box is a link that says ➕ node. Below that is a footer that has hints on keys to press. It says control + N. key adds new, B is back, N is next, C is config, f is hide footer, and f5 is speak.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tui/help_screen/index.html b/tui/help_screen/index.html new file mode 100644 index 0000000000..735b588da9 --- /dev/null +++ b/tui/help_screen/index.html @@ -0,0 +1,2274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Getting Help - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Getting Help

+ +

Using the TUI

+

smol-k8s-lab uses the textual framework to create an interactive graphical interface in the terminal.

+

To get tips on how to navigate the TUI you can press ? from within the TUI to get a help screen that looks like this:

+

To get help while in the TUI, press the ? key and you will see this:

+

terminal screenshot showing the smol-k8s-lab help screen with a blue border. The top border title says Welcome to smol-k8s-lab. The bottom border title says made with 💙 + 🐍 + textual. The text at the top of the box says Use your 🐁 to click anything in the UI ✨ Or use these key bindings. There is a table with the following rows: →: complete suggestion in input field, ⬆/⬇: navigate up and down the app selection list, tab: focus next element, shift+tab: focus previous element, ↩ enter: save input and/or press button, ?: toggle help screen, spacebar: select selection option, meta+click: open link; terminal dependent, so meta can be shift,\n option, windowsKey, command, or control, escape,q: leave current screen and go home

+

Some helpful tips:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
key bindingdescription
?display the help screen
Cdisplay the TUI config screen
Fshow or hide the footer with key hints
Non the start screen, n will create a "new" cluster. All other screens this is "next" screen
B / Esc / QBack a screen. If on the start screen, these quit the application
Tab / Shift+Tabchange focus to different elements on the page
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tui/tui_config/index.html b/tui/tui_config/index.html new file mode 100644 index 0000000000..fbfc8942d9 --- /dev/null +++ b/tui/tui_config/index.html @@ -0,0 +1,2359 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Config - smol-k8s-lab docs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + + + +
+
+
+ + + + +
+
+ + + + + + + +

Config

+ +

TUI and Accessibility configuration

+

You can configure the TUI (Terminal User Interface), including accessibility features, either via the config file, or via the TUI itself.

+

From any screen in the TUI, you can press C and it will bring up the Accessibility and TUI config screen.

+

terminal screenshot showing the smol-k8s-lab configure accessibility options and tui config screen. There are two boxes. Box one: Configure accessibility features. The first header in the box says Terminal Bell config and features a section with two switches: bell on focus, and bell on error. The second header in the first box says Text to speech Config. Below are three rows. The first row has a input field for speech program with placeholder text that says name of program for speech. The 2nd row has two switches. Switch one is on key press, and the switch two is on focus. The final row in the 1st box has two switches. Switch one is screen titles, and switch two is screen descriptions. The second box on this screen is titled configure terminal UI and it features two switches. Box two's first switch is TUI enabled, and the second switch is footer enabaled.

+

Some options may not take effect until you return to the start screen or restart the program.

+

To exit the TUI config screen, just press Q or Esc.

+

To checkout more information on configuring accessibility options via the config file, check out our examples here.

+

Disabling the TUI

+

There are two ways to disable the TUI, but both accomplish the same thing: modifying the $XDG_CONFIG_HOME/smol-k8s-lab/config.yaml.

+

Here's a short video showing how to do this. I don't know how to add subtitles, but the voice says "Welcome to smol-k8s-lab. Press Tab, then C, to configure accessibility options." If you have an existing cluster, you can just just press C without needing to press tab first.

+

+

Disable TUI via TUI

+

Launch the TUI with smol-k8s-lab and then press C. Click the switch next to the word "enabled", and this will disable the TUI from launching automatically. You can still launch the tui with smol-k8s-lab -i or smol-k8s-lab --interactive. Once disabled by default, you can only re-enable it by default from the config file.

+

Disable TUI via the config file

+

In $XDG_CONFIG_HOME/smol-k8s-lab/config.yaml, set smol_k8s_lab.tui.enabled to false like this:

+
1
+2
+3
smol_k8s_lab:
+  tui:
+    enabled: false
+
+

To re-enable the tui, set smol_k8s_lab.tui.enabled to true

+

Logging, Password Management, and Run Command

+

There's a special screen that pops up just before the confirmation screen where we ask for a few last minute run-specific options.

+

terminal screenshot showing the smol-k8s-lab configure logging, password management, and run command config screen. The screen features three main boxes. Box one title is configure logging. It says configure logging for all of smol-k8s-lab. Below the text it has a level dropdown set to debug and a file input with no value. The seond box title is configure password manager. The text reads Save app credentials to a local password manager vault. Only Bitwarden is supported at this time, but if enabled, Bitwarden can be used as your k8s external secret provider. To avoid a password prompt, export the following env vars: BW_PASSWORD, BW_CLIENTID, BW_CLIENTSECRET. Below that is a row with an enabled switch and a duplicate strategy drop down menu set to edit. The final box on the screen is titled configure command to run after config. The text reads If window behavior is set to same window, command runs after smol-k8s-lab has completed. The first row in the box features a teminal dropdown set wezterm, and a window behavior drop down menu set to split right. The final row in the box is a command input field and it is set to k9s --command applications.argoproj.io

+

FAQ

+

I'm getting Alsa errors whenever I launch the TUI over SSH or on a machine without audio drivers.

+

This is because we use your terminal bell or we're trying to use text to speech. You can disable text to speech and bell by hitting ++C++ anywhere in the TUI and then switching all the switches under Accessibility to the off position. Alternatively, you can disable all of this in the $XDG_CONFIG_HOME/smol-k8s-lab/config.yaml (If you don't have the $XDG_CONFIG_HOME env var configured, we will default to ~/.config/smol-k8s-lab/config.yaml). See the TUI and Accessibility config file docs for more info on how to do this via the config file.

+

Why does the smol-k8s-lab look weird in the default macOS terminal?

+

Please see the official textual docs for this, but the gist of it is:

+
+

You can (mostly) fix this by opening settings -> profiles > Text tab, and changing the font settings. We have found that Menlo Regular font, with a character spacing of 1 and line spacing of 0.805 produces reasonable results. If you want to use another font, you may have to tweak the line spacing until you get good results.

+
+

What terminal do you recommend for using the smol-k8s-lab TUI?

+

We use wezterm, because it works on both Linux and macOS. Before we used wezterm, on macOS, we used iTerm2. Both are great terminals with a lot of love put into them.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file

Here's the same video with captions.