diff --git a/.vscode/settings.json b/.vscode/settings.json index 1e07351..88b3d47 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,10 @@ { - "cSpell.words": [ - "istio", - "kubectl", - "kustomization", - "kustomize" - ], - "cSpell.ignoreWords": [ - "istio" - ] -} \ No newline at end of file + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/**": true, + "**/.hg/store/**": true, + "**/mkdocs-site/**": true + }, + "files.useExperimentalFileWatcher": true +} diff --git a/Labs/00-VerifyCluster/README.md b/Labs/00-VerifyCluster/README.md index 9ef2997..fe44bf9 100644 --- a/Labs/00-VerifyCluster/README.md +++ b/Labs/00-VerifyCluster/README.md @@ -9,21 +9,8 @@ - **`kubectl`** - short for Kubernetes Controller - is the CLI for Kubernetes cluster and is required in order to be able to run the labs. - In order to install `kubectl` and if required creating a local cluster, please refer to [Kubernetes - Install Tools](https://kubernetes.io/docs/tasks/tools/) - - ---- -## Lab Highlights: - - [01. Installing minikube](#01-Installing-minikube) - - [02. Start minikube](#02-Start-minikube) - - [03. Check the minikube status](#03-Check-the-minikube-status) - - [04. Verify that the cluster is up and running](#04-Verify-that-the-cluster-is-up-and-running) - - [05. Verify that you can "talk" to your cluster](#05-Verify-that-you-can-talk-to-your-cluster) - - [05.01. Verify that you can "talk" to your cluster](#0501-Verify-that-you-can-talk-to-your-cluster) - --- - - ### 01. Installing minikube - If you don't have an existing cluster you can use google cloud for the labs hands-on @@ -96,12 +83,13 @@ Kubernetes control plane is running at https://192.168.49.2:8443 KubeDNS is running at https://192.168.49.2:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy ``` -- Verify that `kubectl` is installed and configured (You should get something like the following) +- Verify that `kubectl` is installed and configured ```sh kubectl config view ``` +- You should get something like the following ```yaml apiVersion: v1 clusters: @@ -132,31 +120,4 @@ users: $ kubectl get nodes NAME STATUS ROLES AGE VERSION minikube Ready control-plane,master 3m9s v1.20.0 -``` - -### 05.01. Verify that you can "talk" to your cluster - -```sh -# In this sample we have minikube pod running -$ kubectl get nodes -NAME STATUS ROLES AGE VERSION -minikube Ready control-plane,master 3m9s v1.20.0 -``` - - - ---- - -
- 01-Namespace -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - \ No newline at end of file +``` \ No newline at end of file diff --git a/Labs/01-Namespace/README.md b/Labs/01-Namespace/README.md index 15aa47b..0231512 100644 --- a/Labs/01-Namespace/README.md +++ b/Labs/01-Namespace/README.md @@ -9,28 +9,13 @@ # Namespaces - Kubernetes supports **multiple virtual clusters** backed by the same **physical cluster**. -- These virtual clusters are called namespaces. -- Namespaces are the default way for Kubernetes to separate resources. -- Using name spaces we can isolate the development, improve security and more. -- Kubernetes clusters has a build in namespace called **default** and might contain more namespaces like like `kube-system` for example. - - - ---- -## Lab Highlights: -- [K8S Hands-on](#k8s-hands-on) -- [Namespaces](#namespaces) - - [Lab Highlights:](#lab-highlights) - - [Pre-Requirements](#pre-requirements) - - [01. Create Namespace](#01-create-namespace) - - [01.01. Create Namespace](#0101-create-namespace) - - [02. Setting the default Namespace for `kubectl`](#02-setting-the-default-namespace-for-kubectl) - - [03. Verify that you've updated the namespace](#03-verify-that-youve-updated-the-namespace) - - [Note:](#note) +- These virtual clusters are called `namespaces`. +- `Namespaces` are the default way for Kubernetes to separate resources. +- Using `namespaces` we can isolate the development, improve security and much more. +- Kubernetes clusters has a builtin `namespace` called **default** and might contain more `namespaces`, like `kube-system`, for example. --- - ### Pre-Requirements @@ -43,17 +28,17 @@ ### 01. Create Namespace -### 01.01. Create Namespace - ```sh -# In this sample `codewizard` is the desired name space +# In this sample `codewizard` is the desired namespace $ kubectl create namespace codewizard -namespace "codewizard" created +namespace/codewizard created -### !!! Try to create the following name space (with _ & -): +### !!! Try to create the following namespace (with _ & -), and see what happens: $ kubectl create namespace my_namespace- ``` +--- + ### 02. Setting the default Namespace for `kubectl` - To set the default namespace run: @@ -64,6 +49,8 @@ $ kubectl config set-context $(kubectl config current-context) --namespace=codew Context minikube modified. ``` +--- + ### 03. Verify that you've updated the namespace ```sh @@ -74,32 +61,14 @@ CURRENT NAME CLUSTER AUTHINFO NAMESPACE * minikube minikube minikube codewizard ``` -### Note: +--- -- When using `kubectl` you can pass the `-n` flag in order to execute the `kubectl` command on a desired namespace +### 0.4 Using the `-n` Flag: + +- When using `kubectl` you can pass the `-n` flag in order to execute the `kubectl` command on a desired `namespace`. - For example: ```sh # get resources of a specific workspace $ kubectl get pods -n ``` - - - ---- - -
- - [:arrow_left:   00-VerifyCluster](../00-VerifyCluster)  ||   - [02-Deployments-Imperative   :arrow_right:](../02-Deployments-Imperative) -
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - diff --git a/Labs/02-Deployments-Imperative/README.md b/Labs/02-Deployments-Imperative/README.md index b481c16..87d01a6 100644 --- a/Labs/02-Deployments-Imperative/README.md +++ b/Labs/02-Deployments-Imperative/README.md @@ -6,14 +6,6 @@ --- # Deployment - Imperative -### Pre-Requirements - -- K8S cluster - Setting up minikube cluster instruction - -[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) -**CTRL + click to open in new window** - ---- ## Creating deployments using `kubectl create` @@ -21,37 +13,32 @@ [praqma/network-multitool](https://github.com/Praqma/Network-MultiTool) - This is a multitool for container/network testing and troubleshooting. - - --- -## Lab Highlights: -- [K8S Hands-on](#k8s-hands-on) -- [Deployment - Imperative](#deployment---imperative) - - [Pre-Requirements](#pre-requirements) - - [Creating deployments using `kubectl create`](#creating-deployments-using-kubectl-create) - - [Lab Highlights:](#lab-highlights) - - [01. Create namespace](#01-create-namespace) - - [02. Deploy multitool image](#02-deploy-multitool-image) - - [03. Test the deployment](#03-test-the-deployment) - - [03.01. Create a Service using `kubectl expose`](#0301-create-a-service-using-kubectl-expose) - - [03.02. Find the port \& the IP which was assigned to our pod by the cluster.](#0302-find-the-port--the-ip-which-was-assigned-to-our-pod-by-the-cluster) - - [03.03. Test the deployment](#0303-test-the-deployment) + +### Pre-Requirements + +- K8S cluster - Setting up minikube cluster instruction + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) +**CTRL + click to open in new window** --- - -### 01. Create namespace +### 01. Create Namespace + +- As completed in the previous lab, create the desired namespace [codewizard]: ```sh -# Create the desired namespace [codewizard] $ kubectl create namespace codewizard namespace/codewizard created ``` -- In order to set this is as your default namespace refer to: set default namespace +- In order to set this is as the default namespace, please refer to set default namespace. -### 02. Deploy multitool image +--- + +### 02. Deploy Multitool Image ```sh # Deploy the first container @@ -60,10 +47,12 @@ deployment.apps/multitool created ``` - `kubectl create deployment` actually creating a replica set for us. -- We can verify it: +- We can verify it by running: ``` $ kubectl get all -n codewizard + +## Expected output: NAME READY UP-TO-DATE AVAILABLE deployment.apps/multitool 1/1 1 1 @@ -74,21 +63,24 @@ NAME READY STATUS RESTARTS pod/multitool-7885b5f94f-9s7xh 1/1 Running 0 ``` -## 03. Test the deployment +--- + +## 03. Test the Deployment -- The above deployment contains a container [`multitool`] -- In order of us to be able to access this `multitool` container we need to create a resource of type `Service` which will "open" the server for incoming traffic +- The above deployment contains a container named, `multitool`. +- In order for us to be able to access this `multitool` container, we need to create a resource of type `Service` which will "open" the server for incoming traffic. -### 03.01. Create a Service using `kubectl expose` +#### Create a service using `kubectl expose` ```sh # "Expose" the desired port for incoming traffic -# This command is equivalent to declare a `kind: Service` im yaml file +# This command is equivalent to declare a `kind: Service` im YAML file + $ kubectl expose deployment -n codewizard multitool --port 80 --type NodePort service/multitool exposed ``` -- Verify that the service have been created +- Verify that the service have been created by running: ```sh $ kubectl get service -n codewizard @@ -97,12 +89,13 @@ $ kubectl get service -n codewizard NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/multitool NodePort 10.102.73.248 80:31418/TCP 3s ``` +
-### 03.02. Find the port & the IP which was assigned to our pod by the cluster. +#### Find the port & the IP which was assigned to our pod by the cluster. - Grab the port from the previous output. - Port: In the above sample its `31418` [`80:31418/TCP`] - - IP: we will need to gtrab the cluster ip using `kubectl cluster-info` + - IP: we will need to grab the cluster ip using `kubectl cluster-info` ```sh @@ -125,10 +118,12 @@ NODE_PORT=$(kubectl get -o \ - In this sample the cluster-ip is `192.168.49.2` -### 03.03. Test the deployment +
-- Test to see if the deployment worked using the `ip & port` you got above -- Execute `curl` with the following prameters: `http://${CLUSTER_IP}:${NODE_PORT}` +#### Test the deployment + +- Test to see if the deployment worked using the `ip address and port number` we have retrieved above. +- Execute `curl` with the following parameters: `http://${CLUSTER_IP}:${NODE_PORT}` ```sh curl http://${CLUSTER_IP}:${NODE_PORT} @@ -139,22 +134,4 @@ curl 192.168.49.2:30436 # The output should be similar to this: Praqma Network MultiTool (with NGINX) ... ``` - - ---- - -
-:arrow_left:  - 01-Namespace -  ||   03-Deployments-Declarative -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - \ No newline at end of file +- If you get the above output, congratulations! You have successfully created a deployment using imperative commands. \ No newline at end of file diff --git a/Labs/03-Deployments-Declarative/README.md b/Labs/03-Deployments-Declarative/README.md index df5cd60..a59f84d 100644 --- a/Labs/03-Deployments-Declarative/README.md +++ b/Labs/03-Deployments-Declarative/README.md @@ -13,36 +13,26 @@ [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) **CTRL + click to open in new window** - - --- -## Lab Highlights: - - [01. Create namespace](#01-Create-namespace) - - [02. Deploy nginx using yaml file (declarative)](#02-Deploy-nginx-using-yaml-file-declarative) - - [03. Verify that the deployment is created:](#03-Verify-that-the-deployment-is-created) - - [04. Check if the pods are running:](#04-Check-if-the-pods-are-running) - - [05. Update the yaml file with replica's value of 5](#05-Update-the-yaml-file-with-replicas-value-of-5) - - [06. Update the deployment using `kubectl apply`](#06-Update-the-deployment-using-kubectl-apply) - - [07. Scaling down with `kubectl scale`](#07-Scaling-down-with-kubectl-scale) ---- - +### 01. Create Namespace -### 01. Create namespace +- As completed in the previous lab, create the desired namespace [codewizard]: ```sh -# Create the desired namespace [codewizard] $ kubectl create namespace codewizard namespace/codewizard created ``` -- In order to set this is as your default namespace refer to: set default namespace +- In order to set this is as the default namespace, please refer to set default namespace. + +--- ### 02. Deploy nginx using yaml file (declarative) -- Lets create the yaml file for the deployment. -- If this is your first k8s yaml file its recommended that you will type it to get the feeling of the structure +- Let's create the `YAML` file for the deployment. +- If this is your first `k8s` `YAML` file, its advisable that you type it in order to get the feeling of the structure. - Save the file with the following name: `nginx.yaml` ```yaml @@ -78,7 +68,9 @@ spec: deployment.extensions/nginx created ``` -### 03. Verify that the deployment is created: +--- + +### 03. Verify that the deployment has been created: ``` $ kubectl get deployments -n codewizard @@ -87,6 +79,8 @@ multitool 1 1 1 1 nginx 1 1 1 1 ``` +--- + ### 04. Check if the pods are running: ``` @@ -96,30 +90,35 @@ multitool-7885b5f94f-9s7xh 1/1 Running 0 nginx-647fb5956d-v8d2w 1/1 Running 0 ``` -### 0Playing with K8S replicas +### 05. Playing with K8S replicas -- Lets play with the replica and see K8S in action +- Let's play with the replica and see K8S in action. - Open a second terminal and execute: ```sh $ kubectl get pods -n codewizard --watch ``` -### 05. Update the yaml file with replica's value of 5 +--- + +### 05. Update the `nginx.yaml` file with replica's value of 5: ```yaml spec: replicas: 5 ``` +--- + ### 06. Update the deployment using `kubectl apply` ``` $ kubectl apply -n codewizard -f nginx.yaml --record=true deployment.apps/nginx configured ``` +
-- Switch to the second terminal and you should see something like +- Switch to the second terminal and you should see something like the following: ```sh $ kubectl get pods --watch -n codewizard @@ -144,18 +143,23 @@ nginx-dc8bb9b45-x7j4g 1/1 Running 0 3s nginx-dc8bb9b45-wkc68 1/1 Running 0 3s ``` -> Can you explain what do you see? +
+- Can you explain what do you see? + + `Why are there more containers than requested?` - Whey there are more containers that requested? +--- ### 07. Scaling down with `kubectl scale` + +- Scaling down using `kubectl`, and not by editing the `YAML` file: + ```sh -# Scaling using kubectl and not by editing the yaml file kubectl scale -n codewizard --replicas=1 deployment/nginx ``` -- Switch to the second terminal and now you should see something like: +- Switch to the second terminal. The current output should show something like this: ``` NAME READY STATUS RESTARTS AGE @@ -182,23 +186,3 @@ nginx-dc8bb9b45-wkc68 0/1 Terminating 0 6m27s nginx-dc8bb9b45-x7j4g 0/1 Terminating 0 6m27s nginx-dc8bb9b45-x7j4g 0/1 Terminating 0 6m27s ``` - - - ---- - -
-:arrow_left:  - 02-Deployments-Imperative -  ||   04-Rollout -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - \ No newline at end of file diff --git a/Labs/04-Rollout/README.md b/Labs/04-Rollout/README.md index 16ad864..05a0217 100644 --- a/Labs/04-Rollout/README.md +++ b/Labs/04-Rollout/README.md @@ -5,10 +5,10 @@ --- -# Rollout (RollingUpdate) +# Rollout (Rolling Update) -- In this step we will deploy the same application with several different versions and we will "switch" between them -- For learning purposes we will play a little bit with the cli +- In this step we will deploy the same application with several different versions and we will "switch" between them. +- For learning purposes we will play a little with the `CLI`. --- ### Pre-Requirements @@ -17,36 +17,20 @@ [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) **CTRL + click to open in new window** - - ---- -## Lab Highlights: - - [01. Create namespace](#01-Create-namespace) - - [02. Create the desired deployment](#02-Create-the-desired-deployment) - - [03. Expose nginx as service](#03-Expose-nginx-as-service) - - [04. Verify that the pods and the service are running](#04-Verify-that-the-pods-and-the-service-are-running) - - [05. Change the number of replicas to 3](#05-Change-the-number-of-replicas-to-3) - - [06. Verify that now we have 3 replicas](#06-Verify-that-now-we-have-3-replicas) - - [07. Test the deployment](#07-Test-the-deployment) - - [08. Deploy another version of nginx](#08-Deploy-another-version-of-nginx) - - [09. Investigate rollout history:](#09-Investigate-rollout-history) - - [10. Lets see what was changed during the previous updates:](#10-Lets-see-what-was-changed-during-the-previous-updates) - - [11. Undo the version upgrade by rolling back and restoring previous version](#11-Undo-the-version-upgrade-by-rolling-back-and-restoring-previous-version) - - [12. Rolling Restart](#12-Rolling-Restart) - --- - - ### 01. Create namespace +- As completed in the previous lab, create the desired namespace [codewizard]: + ```sh -# Create the desired namespace [codewizard] $ kubectl create namespace codewizard namespace/codewizard created ``` -- In order to set this is as your default namespace refer to: set default namespace +- In order to set this is as the default namespace, please refer to set default namespace. + +--- ### 02. Create the desired deployment @@ -54,20 +38,27 @@ namespace/codewizard created > `save-config` > If true, the configuration of current object will be saved in its annotation. > Otherwise, the annotation will be unchanged. - > This flag is useful when you want to perform kubectl apply on this object in the future. + > This flag is useful when you want to perform `kubectl apply` on this object in the future. +- Let's run the following: ```sh -# In case we already have this delployed we will get an error message + $ kubectl create deployment -n codewizard nginx --image=nginx:1.17 --save-config ``` +Note that in case we already have this deployed, we will get an error message. -### 03. Expose nginx as service +--- + +### 03. Expose nginx as a service ```sh -# Again: In case we already have this service we will get an error message as well + $ kubectl expose deployment -n codewizard nginx --port 80 --type NodePort service/nginx exposed ``` +Again, note that in case we already have this service we will get an error message as well. + +--- ### 04. Verify that the pods and the service are running @@ -88,6 +79,8 @@ NAME DESIRED CURRENT READY AGE replicaset.apps/nginx-db749865c 1 1 1 66s ``` +--- + ### 05. Change the number of replicas to 3 ```sh @@ -95,6 +88,8 @@ $ kubectl scale deployment -n codewizard nginx --replicas=3 deployment.apps/nginx scaled ``` +--- + ### 06. Verify that now we have 3 replicas ```sh @@ -105,15 +100,20 @@ nginx-db749865c-jgcvb 1/1 Running 0 86s nginx-db749865c-lmgtv 1/1 Running 0 4m44s ``` +--- + ### 07. Test the deployment ```sh # !!! Get the Ip & port for this service -$ kubectl get services -n codewizard -o wide # Write down the port number +$ kubectl get services -n codewizard -o wide + +# Write down the port number NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR nginx NodePort 10.102.79.9 80:31204/TCP 7m7s app=nginx -$ kubectl cluster-info # Get the cluster IP +# Get the cluster IP and port +$ kubectl cluster-info Kubernetes control plane is running at https://192.168.49.2:8443 # Using the above : test the nginx @@ -135,6 +135,8 @@ Accept-Ranges: bytes ... ``` +--- + ### 08. Deploy another version of nginx ```sh @@ -157,9 +159,11 @@ ETag: "5d528b4c-264" Accept-Ranges: bytes ``` +--- + ### 09. Investigate rollout history: -- The rollout history command print out all the saved records +- The rollout history command print out all the saved records: ```sh $ kubectl rollout history deployment nginx -n codewizard @@ -170,13 +174,15 @@ REVISION CHANGE-CAUSE 3 kubectl set image deployment nginx nginx=nginx:1.15 --record=true ``` -### 10. Lets see what was changed during the previous updates: +--- + +### 10. Let's see what was changed during the previous updates: -- Print out the rollout changes +- Print out the rollout changes: ```sh # replace the X with 1 or 2 or any number revision id -$ kubectl rollout history deployment nginx -n codewizard --revision=1 +$ kubectl rollout history deployment nginx -n codewizard --revision= # replace here deployment.apps/nginx with revision #1 Pod Template: Labels: app=nginx @@ -191,6 +197,8 @@ Pod Template: Volumes: ``` +--- + ### 11. Undo the version upgrade by rolling back and restoring previous version ``` @@ -205,34 +213,14 @@ deployment.apps/nginx rolled back $ curl -sI : ``` -### 12. Rolling Restart +--- -**Note:** +### 12. Rolling Restart -- If we deploy out deplymnet with `imagePullPolicy: always` we can use `rollout restart` to force K8S to grab the latest image -- **This is the fastest restart method those days** +- If we deploy using `imagePullPolicy: always` set in the `YAML` file, we can use `rollout restart` to force `K8S` to grab the latest image. +- **This is the fastest restart method these days** ``` # Force pods restart kubectl rollout restart deployment [deployment_name] ``` - - - ---- - -
-:arrow_left:  - 03-Deployments-Declarative -  ||   05-Services -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - \ No newline at end of file diff --git a/Labs/05-Services/README.md b/Labs/05-Services/README.md index 838fab5..9d07f0c 100644 --- a/Labs/05-Services/README.md +++ b/Labs/05-Services/README.md @@ -7,7 +7,7 @@ # Service Discovery -- In the following labs we will learn what is `Service`, we will go over the different `Service` types. +- In the following lab we will learn what is a `Service` and go over the different `Service` types. --- ### Pre-Requirements @@ -18,46 +18,24 @@ --- -## What is a `Service` +## 01. Some general notes on what is a `Service` + -### Few general notes: - `Service` is a unit of application behavior bound to a unique name in a `service registry`. - `Service` consist of multiple `network endpoints` implemented by workload instances running on pods, containers, VMs etc. -- `Service` allow us to gain access any given pod / container (e.g. web service). -- A service is (normally) created on top of an existing and exposing the deployment to the world using ip(s) & port(s). -- K8S define 3 main ways (+FQDN internally) to define a service, which means that we have 4 different ways to access Pods. -- There are several proxy mode which inplements diffrent behaviour, for example in `user proxy mode` for each `Service` `kube-proxy` opens a port (randomly chosen) on the local node. Any connections to this "proxy port" are proxied to one of the Service's backend Pods (as reported via Endpoints) -- All the service types are assigned with a Cluster-IP -- Every service also creates `Endoint(s)`, which point to the actual pods. Endpoint are usually referred to as back-ends of a particular service. - - +- `Service` allow us to gain access to any given pod or container (e.g., a web service). +- A `service` is (normally) created on top of an existing deployment and exposing it to the "world", using IP(s) & port(s). +- `K8S` define 3 main ways (+FQDN internally) to define a service, which means that we have 4 different ways to access Pods. +- There are several proxy mode which inplements diffrent behaviour, for example in `user proxy mode` for each `Service` `kube-proxy` opens a port (randomly chosen) on the local node. Any connections to this "proxy port" are proxied to one of the Service's backend Pods (as reported via Endpoints). +- All the service types are assigned with a `Cluster-IP`. +- Every service also creates `Endoint(s)`, which point to the actual pods. `Endpoints` are usually referred to as `back-ends` of a particular service. --- -## Lab Highlights: - - [01. Create namespace and clear previous data if there is any](#01-Create-namespace-and-clear-previous-data-if-there-is-any) - - [02. Create the required resources for this hand-on](#02-Create-the-required-resources-for-this-hand-on) - - [03. Expose the nginx with ClusterIP](#03-Expose-the-nginx-with-ClusterIP) - - [04. Test the nginx with ClusterIP](#04-Test-the-nginx-with-ClusterIP) - - [04.01. Test the nginx with ClusterIP](#0401-Test-the-nginx-with-ClusterIP) - - [04.02. Test the nginx using the deployment name](#0402-Test-the-nginx-using-the-deployment-name) - - [04.03. using the full DNS name](#0403-using-the-full-DNS-name) - - [05. Create NodePort](#05-Create-NodePort) - - [05.01. Delete previous service](#0501-Delete-previous-service) - - [05.02. Create `NodePort` Service](#0502-Create-NodePort-Service) - - [05.03. Test the `NodePort` Service](#0503-Test-the-NodePort-Service) - - [06. Create LoadBalancer (only if you are on real cloud)](#06-Create-LoadBalancer-only-if-you-are-on-real-cloud) - - [06.01. Delete previous service](#0601-Delete-previous-service) - - [06.02. Create `LoadBalancer` Service](#0602-Create-LoadBalancer-Service) - - [06.03. Test the `LoadBalancer` Service](#0603-Test-the-LoadBalancer-Service) - ---- - - ### 01. Create namespace and clear previous data if there is any ```sh -# If the namespace already exist and contains data form previous steps, lets clean it +# If the namespace already exists and contains data form previous steps, let's clean it kubectl delete namespace codewizard # Create the desired namespace [codewizard] @@ -65,6 +43,8 @@ $ kubectl create namespace codewizard namespace/codewizard created ``` +--- + ### 02. Create the required resources for this hand-on ```sh @@ -89,18 +69,21 @@ NAME DESIRED CURRENT READY AGE replicaset.apps/multitool-74477484b8 1 1 1 30s replicaset.apps/nginx-6799fc88d8 1 1 1 8s ``` +
+
-## Service types +# Service types -- As learned in the lecture there are several services type. - Lets practice them +- As previously mentioned, there are several services type. Let's practice them: ### Service type: ClusterIP -- If not specified, the default service type id 'ClusterIP` -- Expose the deployment as a service `--type=ClusterIP` -- `ClusterIP` will expose the pods within the cluster and since we don't have an external ip it will not be reached from outside the cluster. -- When the service is created K8S attach DNs record to the service with the following format: `..svc.cluster.local` +- If not specified, the default service type is `ClusterIP`. +- In order to expose the deployment as a service, use: `--type=ClusterIP` +- `ClusterIP` will expose the pods within the cluster. Since we don't have an `external IP`, it will not be reachable from outside the cluster. +- When the service is created `K8S` attaches a DNS record to the service in the following format: `..svc.cluster.local` + +--- ### 03. Expose the nginx with ClusterIP @@ -109,7 +92,7 @@ replicaset.apps/nginx-6799fc88d8 1 1 1 8s $ kubectl expose deployment nginx -n codewizard --port 80 --type ClusterIP service/nginx exposed -# Check the services and see the type +# Check the services and see it's type # Grab the ClusterIP - we will use it in the next steps $ kubectl get services -n codewizard @@ -117,47 +100,69 @@ NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) nginx ClusterIP 10.109.78.182 80/TCP ``` +--- + ### 04. Test the nginx with ClusterIP -- Since the Service is a cluster ip we will test if we can access the service using the multitool pod +- Since the service is a `ClusterIP`, we will test if we can access the service using the multitool pod. ```sh -# Get the name of the multitool pod which we will use +# Get the name of the multitool pod to be used $ kubectl get pods -n codewizard NAME multitool-XXXXXX-XXXXX -# Run an interactive shell inside the network-multitool-container -# (same as with docker) +# Run an interactive shell inside the network-multitool-container (same concept as with Docker) $ kubectl exec -it -n codewizard -- sh ``` - Connect to the service in **any** of the following ways: -#### 04.01. Test the nginx with ClusterIP +#### Test the nginx with ClusterIP + +##### 1. using the IP from the services output. grab the server response: ```sh -# 1. using the ip from the services output grab the server response bash-5.0# curl -s -HTTP/1.1 200 OK -Server: nginx/1.19.6 -Date: Fri, 15 Jan 2021 23:10:30 GMT -Content-Type: text/html -Content-Length: 612 -Last-Modified: Tue, 15 Dec 2020 13:59:38 GMT -Connection: keep-alive -ETag: "5fd8c14a-264" -Accept-Ranges: bytes ``` -#### 04.02. Test the nginx using the deployment name +```html +# Expected output: + + + +Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+ +

For online documentation and support please refer to +nginx.org.
+Commercial support is available at +nginx.com.

+ +

Thank you for using nginx.

+ + +``` + +
+ +##### 2. Test the nginx using the deployment name - using the service name since its the DNS name behind the scenes ```sh -# 2. using the service name since its the DNS name behind the scenes bash-5.0# curl -s nginx ``` ```html +# Expected output: @@ -187,32 +192,36 @@ bash-5.0# curl -s nginx ``` -#### 04.03. using the full DNS name +
-- For every service we have a full FQDN (Fully qualified domain name) so we can use it as well +##### 3. using the full DNS name - for every service we have a full `FQDN` (Fully qualified domain name) so we can use it as well ```sh # bash-5.0# curl -s ..svc.cluster.local bash-5.0# curl -s nginx.codewizard.svc.cluster.local ``` +
+--- -### Service type: NodePort +# Service type: NodePort -- `NodePort`: Exposes the Service on each Node's IP at a **static port** (the NodePort). -- A ClusterIP Service, to which the NodePort Service routes, **is automatically created**. -- NodePort Service is reachable from outside the cluster, by requesting :. +- `NodePort`: Exposes the Service on each Node's IP at a **static port** (the `NodePort`). +- A `ClusterIP` Service, to which the `NodePort` Service routes, **is automatically created**. +- `NodePort` service is reachable from outside the cluster, by requesting `:`. ### 05. Create NodePort -#### 05.01. Delete previous service +##### 1. Delete previous service ```sh # Delete the existing service from previous steps $ kubectl delete svc nginx -n codewizard -service "nginx" deleted +service "nginx" deleted from codewizard namespace ``` -#### 05.02. Create `NodePort` Service +
+ +##### 2. Create `NodePort` service ```sh # As before but this time the type is a NodePort @@ -225,12 +234,13 @@ $ kubectl get svc -n codewizard NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) nginx NodePort 100.65.29.172 80:32593/TCP ``` +
-#### 05.03. Test the `NodePort` Service +##### 3. Test the `NodePort` service -- Now if we can find the host and the nodePort we can connect directly to the pod +- If we have the host IP and the node port number, we can connect directly to the pod. -- If you followed the previous labs, you should be able to do it your self by now...... +- If you followed the previous labs, you should be able to do it yourself by now...... ```sh # Tiny clue.... @@ -243,25 +253,32 @@ Welcome to nginx! Thank you for using nginx. ``` -### Service type: LoadBalancer - --- -Note: **We cannot test LoadBalancer locally on localhost only on real cluster which can create LoadBalancer)** ---- +# Service type: LoadBalancer + + + +!!! warning "Note" + **We cannot test a `LoadBalancer` service locally on a localhost, but only on a cluster which can provide an `external-IP`** + + ### 06. Create LoadBalancer (only if you are on real cloud) -#### 06.01. Delete previous service +
+ +##### 1. Delete previous service ```sh # Delete the existing service from previous steps $ kubectl delete svc nginx -n codewizard service "nginx" deleted ``` +
-#### 06.02. Create `LoadBalancer` Service +##### 2. Create `LoadBalancer` Service ```sh # As before this time the type is a LoadBalancer @@ -274,30 +291,11 @@ $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) nginx LoadBalancer 100.69.15.89 35.205.60.29 80:31354/TCP ``` +
-#### 06.03. Test the `LoadBalancer` Service +##### 3. Test the `LoadBalancer` Service ```sh # Testing load balancer only require us to use the EXTERNAL-IP $ curl -s ``` - - - ---- - -
-:arrow_left:  - 04-Rollout -  ||   06-DataStore -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - \ No newline at end of file diff --git a/Labs/06-DataStore/README.md b/Labs/06-DataStore/README.md index 0e03737..3225e95 100644 --- a/Labs/06-DataStore/README.md +++ b/Labs/06-DataStore/README.md @@ -6,47 +6,28 @@ --- # Data Store -### Pre-Requirements -- K8S cluster - Setting up minikube cluster instruction - -[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) -**CTRL + click to open in new window** - ---- - ## Secrets and ConfigMaps -- Secrets/ConfigMap are ways to store and inject configurations into your deployments. -- Secrets usually store passwords,certificates, API keys and more. +- Secrets & ConfigMap are ways to store and inject configurations into your deployments. +- Secrets usually store passwords, certificates, API keys and more. - ConfigMap usually store configuration (data). - --- -## Lab Highlights: - - [01. Create namespace and clear previous data if there is any](#01-Create-namespace-and-clear-previous-data-if-there-is-any) - - [02. Build the docker container](#02-Build-the-docker-container) - - [02.01. write the server code](#0201-write-the-server-code) - - [02.02. Write the DockerFile](#0202-Write-the-DockerFile) - - [02.03. Build the docker container](#0203-Build-the-docker-container) - - [02.04. Test the container](#0204-Test-the-container) - - [03. Using K8S deployment & Secrets/ConfigMap](#03-Using-K8S-deployment--SecretsConfigMap) - - [03.01. Writing the deployment & Service file](#0301-Writing-the-deployment--Service-file) - - [03.02. Deploy to cluster](#0302-Deploy-to-cluster) - - [03.03. Test the app](#0303-Test-the-app) - - [04. Using Secrets & config maps](#04-Using-Secrets--config-maps) - - [04.01. Create the desired secret and config map for this lab](#0401-Create-the-desired-secret-and-config-map-for-this-lab) - - [04.02. Updating the Deployment to read the values from Secrets & ConfigMap](#0402-Updating-the-Deployment-to-read-the-values-from-Secrets--ConfigMap) - - [04.03. Update the deployment to read values from K8S resources](#0403-Update-the-deployment-to-read-values-from-K8S-resources) - - [04.04. Test the changes](#0404-Test-the-changes) + + +### Pre-Requirements +- K8S cluster - Setting up minikube cluster instruction + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) +**CTRL + click to open in new window** --- - -## Lets play with Secrets first +# First, Let's play a bit with Secrets -### 01. Create namespace and clear previous data if there is any +### 01. Create namespace and clear previous data (if there is any) ```sh # If the namespace already exist and contains data from previous steps, lets clean it @@ -57,13 +38,16 @@ $ kubectl create namespace codewizard namespace/codewizard created ``` -### You can skip section #2 if you don't wish to build and push your docker container +!!! warning "Note" + **You can skip section number 02. if you don't wish to build and push your docker container** + +--- ### 02. Build the docker container -#### 02.01. write the server code +##### 1. write the server code - For this demo we will use a tiny NodeJS server which will consume the desired configuration values from the secret -- This is the code of our server [server.js](server.js) +- This is the code of our server [server.js](server.js): ```js // @@ -85,11 +69,14 @@ require("http") .listen(process.env.PORT || 5000 ); ``` -#### 02.02. Write the DockerFile +
+ -- First lets wrap it up as docker container -- If you wish you can skip this and use the existing docker image: `nirgeier/k8s-secrets-sample` -- In the docker file we will set the `ENV` for or variables +##### 2. Write the DockerFile + +- First, let's wrap it up as `docker container` +- If you wish, you can skip this and use the existing docker image: `nirgeier/k8s-secrets-sample` +- In the `Dockerfile` we will set the `ENV` for or variables ```Dockerfile # Base Image @@ -110,11 +97,13 @@ COPY server.js . ENTRYPOINT node server.js ``` -#### 02.03. Build the docker container +
+ +##### 3. Build the docker container ```sh -# The container name is prefixed withthe Dockerhub account +# The container name is prefixed with the Dockerhub account # !!! You should replace the prefix to your dockerhub account -# In the sample the username wis `nirgeier` +# In the sample the username is `nirgeier` $ docker build . -t nirgeier/k8s-secrets-sample # The output should be similar to this @@ -154,13 +143,16 @@ Removing intermediate container 223629e16589 Successfully built f5cbb1895d66 Successfully tagged nirgeier/k8s-secrets-sample:latest ``` -#### 02.04. Test the container + +
+ +##### 4. Test the container ```sh # Run the docker container which you build earlier, # replace the name if you used your own name -# and check the response from the serrver -# It should print out the variables which were defined in side the DockerFile +# and check the response from the server. +# It should print out the variables which were defined inside the DockerFile $ docker run -d -p5000:5000 nirgeier/k8s-secrets-sample --name server # Get the response from the container @@ -172,20 +164,28 @@ Language: Hebrew Token : Hard-To-Guess ``` +
+ - Stop the container + ```sh -# Stop the running contatiner -# We are using the name which we passed in the `docker run` command --name +# Stop the running container +# We are using the name which we passed in the `docker run` command --name docker stop server ``` + - Push the container to your docker hub account if you wish +--- + + ### 03. Using K8S deployment & Secrets/ConfigMap -### 03.01. Writing the deployment & Service file +##### 1. Writing the deployment & service file + +- Deploy the docker container that you have prepared in the previous step with the following `Deployment` file. +- In this sample we will define the values in the `YAML` file, later on we will use Secrets/ConfigMap [variables-from-yaml.yaml](./variables-from-yaml.yaml) -- Deploy the docker container you prepared in the previous step with the following `Deployment` file. -- In this sample we will define the values in the yaml file,later on we will use Secrets/ConfigMap [variables-from-yaml.yaml](./variables-from-yaml.yaml) ```yaml apiVersion: v1 kind: Namespace @@ -236,17 +236,21 @@ spec: port: 5000 targetPort: 5000 ``` +
-### 03.02. Deploy to cluster -``` +##### 2. Deploy to cluster +```sh $ kubectl apply -n codewizard -f variables-from-yaml.yaml deployment.apps/codewizard-secrets configured service/codewizard-secrets created ``` -### 03.03. Test the app +
+ +##### 3. Test the app - We will need a second container for executing the curl request. -- We will us a busyBox image for this purpose +- We will use a `busyBox image` for this purpose. + ```sh # grab the name of the pod $ kubectl get pods -n codewizard @@ -269,9 +273,11 @@ Token : Hard-To-Guess2 ``` +--- + ### 04. Using Secrets & config maps -### 04.01. Create the desired secret and config map for this lab +##### 1. Create the desired secret and config map for this lab ```sh # Create the secret @@ -322,9 +328,12 @@ English Events: ``` -### 04.02. Updating the Deployment to read the values from Secrets & ConfigMap +
+ +##### 2. Update the deployment to read the values from Secrets & ConfigMap - Change the `env` section to the following: + ```yaml env: - name: LANGUAGE @@ -339,57 +348,50 @@ Events: key: TOKEN # The key in the secret ``` -### 04.03. Update the deployment to read values from K8S resources +
+ +##### 3. Update the deployment to read values from K8S resources ```sh $ kubectl apply -n codewizard -f variables-from-secrets.yaml deployment.apps/codewizard-secrets configured service/codewizard-secrets unchanged ``` -### 04.04. Test the changes -- Refer to step 3.3 for testing your server -```sh -# Login to the server -# In this sample this is the pod name: codewizard-secrets-76d99bdc54-s66vl -kubectl exec -it codewizard-secrets-76d99bdc54-s66vl -n codewizard -- sh +
-# Test the changes to verify that they are set from the Secret/ConfigMap -curl localhost:5000 +##### 4. Test the changes -# Out put should be -Language: English -Token : Hard-To-Guess3 -``` ---- -### !!! Note -> Pods are not recreated or updated automatically when secrets or ConfigMaps change so you will have to restart your pods +- Refer to [step 3.3](#3-test-the-app) for testing your server -- To update existing secrets or ConfigMap: -``` -$ kubectl create secret generic token -n codewizard --from-literal=Token=Token3 -o yaml --dry-run=client | kubectl replace -f - -secret/token replaced -``` -- Test your server to and verify that you see the old values -- Delete the old pods so they can come back to life with the new values -- Test your server again, now you should see view the changes + ```sh + # Login to the server + # In this sample, the pod name is: codewizard-secrets-76d99bdc54-s66vl + kubectl exec -it codewizard-secrets-76d99bdc54-s66vl -n codewizard -- sh - + # Test the changes to verify that they are set from the Secret/ConfigMap + curl localhost:5000 + + # Out put should be + Language: English + Token : Hard-To-Guess3 + ``` --- -
-:arrow_left:  - 05-Services -  ||   07-nginx-Ingress -   :arrow_right:
+
---- +!!! warning "Note" + Pods are not recreated or updated automatically when `Secrets` or `ConfigMaps` change, so you will have to restart your pods manually -
- ©CodeWizard LTD -
+- To update existing secrets or ConfigMap: -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) +``` +$ kubectl create secret generic token -n codewizard --from-literal=Token=Token3 -o yaml --dry-run=client | kubectl replace -f - +secret/token replaced +``` + +- Test your server and verify that you see the old values. +- Delete the old pods so they can come back to life with the new values. +- Test your server again, now you should be able to see the changes. - \ No newline at end of file diff --git a/Labs/07-nginx-Ingress/README.md b/Labs/07-nginx-Ingress/README.md index 2ce0c5d..8037616 100644 --- a/Labs/07-nginx-Ingress/README.md +++ b/Labs/07-nginx-Ingress/README.md @@ -7,150 +7,176 @@ # Nginx-Ingress -### ✅ !!! Important - -> We cannot see it in action on local host (meaning that it will not get external ip) unless we use the explicit -http://host:port - - -- Kubernetes ingress object is a DNS -- To enable an ingress object, we need an ingress controller -- In this demo we will use nginx-ingress -- To get started with nginx-ingress, we will deploy out previous app - ```sh - # Create 3 containers - $ kubectl run ingress-pods --image=nirgeier/k8s-secrets-sample --replicas=3 - - # Expose the service - $ kubectl expose deployment ingress-pods --port=5000 - ``` -- Now lets deploy the nginx-ingress (grabbed from the official site) - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: default-http-backend - spec: - replicas: 1 - selector: - matchLabels: - app: default-http-backend - template: - metadata: - labels: - app: default-http-backend - spec: - terminationGracePeriodSeconds: 60 - containers: - - name: default-http-backend - # Any image is permissable as long as: - # 1. It serves a 404 page at / - # 2. It serves 200 on a /healthz endpoint - image: gcr.io/google_containers/defaultbackend:1.0 - livenessProbe: - httpGet: - path: /healthz - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - timeoutSeconds: 5 - ports: - - containerPort: 8080 - resources: - limits: - cpu: 10m - memory: 20Mi - requests: - cpu: 10m - memory: 20Mi - ``` -- Next create the service - ```yaml - apiVersion: v1 - kind: Service - metadata: - name: default-http-backend - spec: - selector: + +!!! Important - + We cannot see it in action on a `localhost` (meaning that it will not get an external IP) unless we use the explicit `http://host:port` format. + + +- Kubernetes `ingress` object is a `DNS` +- To enable an `ingress object`, we need an `ingress controller` +- In this demo we will use `Nginx-Ingress` + + +--- + + +### Pre-Requirements +- K8S cluster - Setting up minikube cluster instruction + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) +**CTRL + click to open in new window** + +--- + +### 01. Deploy sample app + +- To get started with `Nginx-Ingress`, we will deploy out previous app: + +```sh +# Create 3 containers +$ kubectl create deployment ingress-pods --image=nirgeier/k8s-secrets-sample --replicas=3 + +# Expose the service +$ kubectl expose deployment ingress-pods --port=5000 +``` + + +--- + +### 02. Deploy default backend + +- Now lets deploy the `Nginx-Ingress` (grabbed from the official site): + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: default-http-backend +spec: + replicas: 1 + selector: + matchLabels: app: default-http-backend - ports: - - protocol: TCP - port: 80 - targetPort: 8080 - type: NodePort - ``` -### Import ssl certificate + template: + metadata: + labels: + app: default-http-backend + spec: + terminationGracePeriodSeconds: 60 + containers: + - name: default-http-backend + # Any image is permissable as long as: + # 1. It serves a 404 page at / + # 2. It serves 200 on a /healthz endpoint + image: gcr.io/google_containers/defaultbackend:1.0 + livenessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + initialDelaySeconds: 30 + timeoutSeconds: 5 + ports: + - containerPort: 8080 + resources: + limits: + cpu: 10m + memory: 20Mi + requests: + cpu: 10m + memory: 20Mi +``` + +--- + + +### 03. Create service + +- Next, let's create the service: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: default-http-backend +spec: + selector: + app: default-http-backend + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: NodePort +``` + +--- + + + +### 04. Import `ssl` certificate - In this demo we will use certificate. - The certificate is in the same folder as this file -- The certificate is for the host name: `ingress.local` - ```sh - # If you wish to create the certificate use this script - ### ---> The common Name fiels is your host for later on - ### Common Name (e.g. server FQDN or YOUR name) []: - $ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout certificate.key -out certificate.crt - - # Create a pem file - # The purpose of the DH parameters is to exchange secrets - $ openssl dhparam -out certificate.pem 2048 - ``` -- Store the certificate in secret - ```sh - # Store the certificate - $ kubectl create secret tls tls-certificate --key certificate.key --cert certificate.crt - secret/tls-certificate created - - # Store the DH parameters - $ kubectl create secret generic tls-dhparam --from-file=certificate.pem - secret/tls-dhparam created - ``` -### Deploy the ingress -- Now that we have the certificate we can deploy the Ingress - ```yaml - # Ingress.yaml - apiVersion: extensions/v1beta1 - kind: Ingress - metadata: - name: my-first-ingress - annotations: - kubernetes.io/ingress.class: "nginx" - nginx.org/ssl-services: "my-service" - spec: - tls: - - hosts: - - myapp.local - secretName: tls-certificate - rules: - - host: myapp.local - http: - paths: - - path: / - backend: - serviceName: ingress-pods - servicePort: 5000 - ``` - -### Enable the ingress addon -- The ingress is not enabled by default and we have to "turn it on" - ```sh - $ minikube addons enable ingress - ✅ ingress was successfully enabled - ``` - - +- The certificate is for the hostname: `ingress.local` + +```sh +# If you wish to create the certificate use this script +### ---> The common Name fiels is your host for later on +### Common Name (e.g. server FQDN or YOUR name) []: +$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout certificate.key -out certificate.crt + +# Create a pem file +# The purpose of the DH parameters is to exchange secrets +$ openssl dhparam -out certificate.pem 2048 +``` + +- Store the certificate in secret: + +```sh +# Store the certificate +$ kubectl create secret tls tls-certificate --key certificate.key --cert certificate.crt +secret/tls-certificate created + +# Store the DH parameters +$ kubectl create secret generic tls-dhparam --from-file=certificate.pem +secret/tls-dhparam created +``` --- -
-:arrow_left:  - 06-DataStore -  ||   08-Kustomization -   :arrow_right:
+### 05. Deploy the ingress +- Now that we have the certificate, we can deploy the `Ingress`: + +```yaml +# Ingress.yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: my-first-ingress +annotations: + kubernetes.io/ingress.class: "nginx" + nginx.org/ssl-services: "my-service" +spec: + tls: + - hosts: + - myapp.local + secretName: tls-certificate + rules: + - host: myapp.local + http: + paths: + - path: / + backend: + serviceName: ingress-pods + servicePort: 5000 +``` --- -
- ©CodeWizard LTD -
+### 06. Enable the ingress addon -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) +- The `Ingress` is not enabled by default, so we have to "turn it on": - \ No newline at end of file +```sh +$ minikube addons enable ingress +✅ ingress was successfully enabled +``` diff --git a/Labs/08-Kustomization/README.md b/Labs/08-Kustomization/README.md index 5a01fa8..a427047 100644 --- a/Labs/08-Kustomization/README.md +++ b/Labs/08-Kustomization/README.md @@ -10,16 +10,15 @@ ## Declarative Configuration in Kubernetes - `Kustomize` is a very powerful too for customizing and building Kubernetes resources. -- `Kustomize` started at 2017. Added to `kubectl` since version 1.14. +- `Kustomize` started at 2017, and added to `kubectl` since version 1.14. - `Kustomize` has many useful features for managing and deploying resource. -- When you execute a Kustomization beside using the build-in features it will also re-order the resources in a logical way for the K8S to be deployed. +- When you execute a Kustomization beside using the builtin features, it will also re-order the resources in a logical way for the K8S to be deployed. -### Re-order the resources for +### 01. Re-order the resources -- `Kustomization` re-order the `Kind` for optimization, for example we will need an existing `namespace` before using it. +- `Kustomization` re-orders the `Kind` for optimization. For this demo, we will need an existing `namespace` before using it. -- The order of the resources is defined in the source code: - Source: https://github.com/kubernetes-sigs/kustomize/blob/master/api/resid/gvk.go +- The order of the resources is defined in the [source code](https://github.com/kubernetes-sigs/kustomize/blob/master/api/resid/gvk.go) ```go // An attempt to order things to help k8s, e.g. @@ -59,8 +58,8 @@ var orderLast = []string{ --- -### Base resource for our demo -- In the following samples we will refer to the following `base.yaml` file +### 02. Base resource for our demo +- In the following samples we will refer to the following `base.yaml` file: ```yaml # base.yaml @@ -84,24 +83,24 @@ spec: ``` --- -## Common Features +### 03. Common Features -- [commonAnnotation](#commonannotation) -- [commonLabels](#commonlabels) +- [common Annotation](#commonannotation) +- [common Labels](#commonlabels) - [Generators](#Generators) - - [ConfigMapGenerator](#configMapGenerator) - - [FromEnv](#fromenv) - - [FromFile](#fromfile) - - [FromLiteral](#fromliteral) - - [SecretGenerator](#secret-generator) + - [Config Map Generator](#configMapGenerator) + - [From Env](#fromenv) + - [From File](#fromfile) + - [From Literal](#fromliteral) + - [Secret Generator](#secret-generator) - [images](#images) - [Namespaces](#Namespaces) - [Prefix / Suffix](#prefix-suffix) - [Replicas](#replicas) -- [Patches](#Patches) - - [Patch Add/Update](#patch-addupdate) - - [Patch Delete](#Patch-Delete) - - [Patch Replace](#Patch-Replace) + - [Patches](#Patches) + - [Patch Add/Update](#patch-addupdate) + - [Patch Delete](#Patch-Delete) + - [Patch Replace](#Patch-Replace) --- @@ -150,6 +149,8 @@ commonAnnotations: name: myapp ``` +--- + ## `commonLabels` ```sh @@ -171,59 +172,67 @@ bases: ``` - Output: - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + # Labels added .... + labels: + author: nirgeier@gmail.com + env: codeWizard-cluster + name: myapp +spec: + selector: + matchLabels: + app: myapp # Labels added .... - labels: author: nirgeier@gmail.com env: codeWizard-cluster - name: myapp - spec: - selector: - matchLabels: + template: + metadata: + labels: app: myapp # Labels added .... author: nirgeier@gmail.com env: codeWizard-cluster - template: - metadata: - labels: - app: myapp - # Labels added .... - author: nirgeier@gmail.com - env: codeWizard-cluster - spec: - containers: - - image: __image__ - name: myapp - ``` + spec: + containers: + - image: __image__ + name: myapp +``` + +--- ## Generators - Kustomization also support generate `ConfigMap` / `Secret` in several ways. -- The default behavior is adding the output hash value as suffix to the name, - ex: `secretMapFromFile-495dtcb64g` - ```yaml - apiVersion: v1 - data: - APP_ENV: ZGV2ZWxvcG1lbnQ= - LOG_DEBUG: dHJ1ZQ== - NODE_ENV: ZGV2 - REGION: d2V1 - kind: Secret - metadata: - name: secretMapFromFile-495dtcb64g # <-------------------------- - type: Opaque - ``` -- We can disable the suffix with the following addition to the `kustomization.yaml` +- The default behavior is adding the output hash value as suffix to the name, e.g.: `secretMapFromFile-495dtcb64g` + ```yaml - generatorOptions: - disableNameSuffixHash: true + apiVersion: v1 + data: + APP_ENV: ZGV2ZWxvcG1lbnQ= + LOG_DEBUG: dHJ1ZQ== + NODE_ENV: ZGV2 + REGION: d2V1 + kind: Secret + metadata: + name: secretMapFromFile-495dtcb64g # <-------------------------- + type: Opaque ``` + +- We can disable the suffix with the following addition to the `kustomization.yaml` + +```yaml +generatorOptions: + disableNameSuffixHash: true +``` + +--- + ### `configMapGenerator` - - #### FromEnv + - #### From Env - `.env` ```sh key1=value1 @@ -247,7 +256,8 @@ bases: metadata: name: configMapFromEnv-c9655hf97k ``` - - #### FromFile + + - #### From File - `.env` ```sh key1=value1 @@ -271,7 +281,8 @@ bases: metadata: name: configFromFile-dfhmctd84d ``` - - #### FromLiteral + + - #### From Literal - `.env` ```sh key1=value1 @@ -296,11 +307,13 @@ bases: metadata: name: configFromLiterals-h777b4gdf5 ``` + --- ## `Secret` Generator + ```yaml -# Similar to configMap but wit additional type field +# Similar to configMap but with an additional type field secretGenerator: # Generate secret from env file - name: secretMapFromFile @@ -309,6 +322,9 @@ secretGenerator: generatorOptions: disableNameSuffixHash: true ``` + +--- + ## `images` - Modify the name, tags and/or digest for images. @@ -335,25 +351,26 @@ images: ``` - Output: - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: myapp - spec: - selector: - matchLabels: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp +spec: + selector: + matchLabels: + app: myapp + template: + metadata: + labels: app: myapp - template: - metadata: - labels: - app: myapp - spec: - containers: - # --- This image was updated - - image: my-registry/my-image:v1 - name: myapp - ``` + spec: + containers: + # --- This image was updated + - image: my-registry/my-image:v1 + name: myapp +``` --- @@ -376,14 +393,15 @@ bases: ``` - Output: - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: myapp - # Namespace added here - namespace: kustomize-namespace - ``` + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp + # Namespace added here + namespace: kustomize-namespace +``` --- @@ -407,17 +425,20 @@ bases: ``` - Output: - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: prefix-codeWizard-myapp-suffix-codeWizard - ``` + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prefix-codeWizard-myapp-suffix-codeWizard +``` --- ## `Replicas` +- deployment + ```yaml # deployment.yaml apiVersion: apps/v1 @@ -434,6 +455,8 @@ spec: image: registry/conatiner:latest ``` +- kustomization + ```yaml # kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 @@ -449,7 +472,8 @@ resources: - Output: -**Note**: There is a bug with the `replicas` entries which return error for some reason. +!!! warning "**Note**" + There is a bug with the `replicas` entries which return error for some reason. ```sh $ kubectl kustomize . @@ -535,6 +559,7 @@ patchesStrategicMerge: ``` - Output: + ```yaml apiVersion: apps/v1 kind: Deployment @@ -561,6 +586,8 @@ spec: name: myapp ``` +--- + ### Patch-Delete ```sh kubectl kustomize samples/08-Patches/patch-delete @@ -596,6 +623,7 @@ spec: ``` - Output: + ```yaml apiVersion: apps/v1 kind: Deployment @@ -615,6 +643,8 @@ spec: name: nginx ``` +--- + ### Patch Replace ```sh kubectl kustomize samples/08-Patches/patch-replace/ @@ -653,6 +683,7 @@ spec: ``` - Output: + ```yaml apiVersion: apps/v1 kind: Deployment @@ -674,22 +705,4 @@ spec: image: nginx:latest name: myapp ``` - - ---- - -
-:arrow_left:  - 07-nginx-Ingress -  ||   09-StatefulSet -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - \ No newline at end of file diff --git a/Labs/09-StatefulSet/README.md b/Labs/09-StatefulSet/README.md index 1b08e7d..ad7725d 100644 --- a/Labs/09-StatefulSet/README.md +++ b/Labs/09-StatefulSet/README.md @@ -8,52 +8,56 @@ # StatefulSets + + + ## The Difference Between a `Statefulset` And a `Deployment` #### `Stateless` application -- A stateless application is one that does not care which network it is using, and it does not need permanent storage and can be scaled up and down without the need to re-use the same Network or persistance. -- Deployment is the suitable Kind for Stateless applications -- The most trivial example of stateless app is a Web Server +- A stateless application is one that does not care which network it is using, and it does not need permanent storage and can be scaled up and down without the need to re-use the same network or persistence. +- Deployment is the suitable kind for Stateless applications. +- The most trivial example of stateless app is a `Web Server`. #### `Stateful` application -- Stateful application are apps which in order to work properly need to use the same resources like Network, Storage etc). -- Usually with Stateful applications you’ll need to ensure that pods can reach each other through a **unique identity that does not change** (examples: hostnames, IP). -- The most trivial example of Stateful app is a Database of any kind +- Stateful applications are apps which in order to work properly need to use the same resources, such as network, storage etc. +- Usually with `Stateful` applications you will need to ensure that pods can reach each other through a **unique identity that does not change** (e.g., hostnames, IP). +- The most trivial example of Stateful app is a database of any kind. --- -## `Stateful` Notes +!!! warning "Stateful Notes" + - Like a Deployment, a `StatefulSet` manages Pods that are based on an **identical container spec**. + - Unlike a Deployment, **a `StatefulSet` maintains a sticky identity for each of their Pods**. + - These pods are created from the same spec, but are not interchangeable: each has a persistent identifier that it maintains across any rescheduling. + - Deleting and/or scaling down a `StatefulSet` will not delete the volumes associated with the `StatefulSet`. This is done to ensure data safety. + - `StatefulSet` keeps a unique identity for each Pod and assign the same identity to those pods when they are rescheduled (update, restart etc). + - The storage for a given Pod must either be provisioned by a `PersistentVolume` provisioner, based on the requested storage class, or pre-provisioned by an admin. + - `StatefulSet` manages the deployment and scaling of a set of Pods, and **provides guarantees about the ordering and uniqueness of these Pods**. + - A `stateful` app needs to use a dedicated storage. -- Like a Deployment, a StatefulSet manages Pods that are based on an **identical container spec**. -- Unlike a Deployment, **a StatefulSet maintains a sticky identity for each of their Pods**. -- These pods are created from the same spec, but are not interchangeable: each has a persistent identifier that it maintains across any rescheduling. -- Deleting and/or scaling a `StatefulSet` down will not delete the volumes associated with the `StatefulSet`. This is done to ensure data safety. -- `StatefulSet` keeps a unique identity for each Pod and assign the same identity to those pods when they rescheduled (update, restart etc). -- The storage for a given Pod must either be provisioned by a `PersistentVolume` Provisioner based on the requested storage class, or pre-provisioned by an admin. -- `StatefulSet` Manages the deployment and scaling of a set of Pods, and **provides guarantees about the ordering and uniqueness of these Pods**. -- A stateful app needs use dedicated storage +--- ### Stable Network Identity -- A Stateful application node **must** have a unique hostname and IP address so that other nodes in the same application knows how to reach it. -- A `ReplicaSet` assign **a random hostname and IP address** to each Pod and we use a Service which expose those Pods for us. +- A `Stateful` application node **must** have a unique hostname and IP address so that other nodes in the same application know how to reach it. +- A `ReplicaSet` assign **a random hostname and IP address** to each Pod. In such a case, we must use a service which exposes those Pods for us. ### Start and Termination Order -- Each `StatefulSet` follow this naming pattern: `$(statefulSet name)-$(ordinal)` -- Stateful applications restarted or re-created following the creation order. +- Each `StatefulSet` follows this naming pattern: `$(statefulSet name)-$(ordinal)` +- `Stateful` applications restarted or re-created, following the creation order. - A `ReplicaSet` does not follow a specific order when starting or killing its pods. ### StatefulSet Volumes -- StatefulSet **does not create a volume for you**. -- When a StatefulSet is deleted, the respective volumes **are not deleted with it**. +- `StatefulSet` **does not create a volume for you**. +- When a `StatefulSet` is deleted, the respective volumes **are not deleted with it**. --- -### To address all those requirements, Kubernetes offers the StatefulSet primitive. +### To address all these requirements, Kubernetes offers the `StatefulSet primitive`. --- @@ -64,39 +68,6 @@ [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) **CTRL + click to open in new window** - - ---- -## Lab Highlights: -- [K8S Hands-on](#k8s-hands-on) -- [StatefulSets](#statefulsets) - - [The Difference Between a `Statefulset` And a `Deployment`](#the-difference-between-a-statefulset-and-a-deployment) - - [`Stateless` application](#stateless-application) - - [`Stateful` application](#stateful-application) - - [`Stateful` Notes](#stateful-notes) - - [Stable Network Identity](#stable-network-identity) - - [Start and Termination Order](#start-and-termination-order) - - [StatefulSet Volumes](#statefulset-volumes) - - [To address all those requirements, Kubernetes offers the StatefulSet primitive.](#to-address-all-those-requirements-kubernetes-offers-the-statefulset-primitive) - - [Pre-Requirements](#pre-requirements) - - [Lab Highlights:](#lab-highlights) - - [01. Create namespace and clear previous data if there is any](#01-create-namespace-and-clear-previous-data-if-there-is-any) - - [02. Create and test the Stateful application](#02-create-and-test-the-stateful-application) - - [03. Test the Stateful application](#03-test-the-stateful-application) - - [04. Scale down the StatefulSet and check that its down](#04-scale-down-the-statefulset-and-check-that-its-down) - - [04.01. Scale down the `Statefulset` to 0](#0401-scale-down-the-statefulset-to-0) - - [04.02. Verify that the pods Terminated](#0402-verify-that-the-pods-terminated) - - [04.03. Verify that the DB is not reachable](#0403-verify-that-the-db-is-not-reachable) - - [05. Scale up again and verify that we still have the prevoius data](#05-scale-up-again-and-verify-that-we-still-have-the-prevoius-data) - - [05.01. scale up the `Statefulset` to 1 or more](#0501-scale-up-the-statefulset-to-1-or-more) - - [05.02. Verify that the pods is in Running status](#0502-verify-that-the-pods-is-in-running-status) - - [05.03. Verify that the pods is using the previous data](#0503-verify-that-the-pods-is-using-the-previous-data) - ---- - - - - --- @@ -360,22 +331,3 @@ psql \ (1 row) ``` - - ---- - -
-:arrow_left:  - 08-Kustomization -  ||   10-Istio -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - \ No newline at end of file diff --git a/Labs/10-Istio/README.md b/Labs/10-Istio/README.md index d3d70da..1f5edb9 100644 --- a/Labs/10-Istio/README.md +++ b/Labs/10-Istio/README.md @@ -1,64 +1,40 @@ -![](../../resources/k8s-logos.png) - - # K8S Hands-on ![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) --- - -## PreRequirements +# Istio -- [Helm](https://helm.sh/docs/intro/install/) -- K8S cluster -- **kubectl** configured to interact with your cluster. +Istio is an open-source service mesh that provides a way to manage microservices traffic, security, and observability in a Kubernetes cluster. --- -[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) +## Pre Requirements - -### **CTRL + click to open in new window** +- [Helm](https://helm.sh/docs/intro/install/) installed +- K8S cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/)) configured to interact with your cluster ---- -- [Introduction](#introduction) - - [`Istio`](#istio) - - [`Kiali`](#kiali) -- [Part 01 - Installing Istio and Kiali](#part-01---installing-istio-and-kiali) - - [Step 01: Install Istio Using Istioctl](#step-01-install-istio-using-istioctl) - - [Step 02: Verify Istio installation](#step-02-verify-istio-installation) - - [Step 03: Install Kiali](#step-03-install-kiali) - - [Step 04: Verify Kiali installation](#step-04-verify-kiali-installation) -- [Part 02 - Viewing the Network with Istio](#part-02---viewing-the-network-with-istio) - - [Step 01: Enable Istio Injection](#step-01-enable-istio-injection) - - [Step 2: Deploy Sample Application](#step-2-deploy-sample-application) - - [Step 03: Verify the Sample Application](#step-03-verify-the-sample-application) - - [Step 04: Expose the Application](#step-04-expose-the-application) -- [Part 03 - Visualizing the Network with Kiali](#part-03---visualizing-the-network-with-kiali) - - [Step 01: Access Kiali Dashboard](#step-01-access-kiali-dashboard) - - [Step 02: Explore the Service Mesh Topology](#step-02-explore-the-service-mesh-topology) -- [Part 04: Creating a Demo Istio VirtualService](#part-04-creating-a-demo-istio-virtualservice) - - [Step 1: Define a VirtualService](#step-1-define-a-virtualservice) - - [Step 02: Apply the VirtualService](#step-02-apply-the-virtualservice) - - [Step 03: Verify the Routing](#step-03-verify-the-routing) -- [Conclusion](#conclusion) + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) +### **CTRL + click to open in new window** + --- - -# Istio and Kiali -- This guide provides a detailed walkthrough on installing and configuring **Istio** and **Kiali** in a Kubernetes cluster. -- You will also learn how to visualize your service mesh with Istio and Kiali, create a demo **Istio VirtualService**. +## Istio and Kiali +- This guide provides a detailed walkthrough on installing and configuring **Istio** and **Kiali** on a Kubernetes cluster. +- We will also learn how to visualize your service mesh with Istio and Kiali and create a demo **Istio VirtualService**. --- -## Introduction +## 01. Introduction ### `Istio` @@ -75,8 +51,8 @@ | **Fault Injection** | Supports fault injection to simulate network failures, latency, or errors in microservices to test resilience and robustness of the application. | | **Mutual TLS (mTLS)** | Istio can automatically encrypt traffic between services using mutual TLS (mTLS) to ensure secure communication and provide strong identity-based access control. | | **Authentication & Authorization** | Provides identity and access management through role-based access control (RBAC) and integration with external identity providers (e.g., OAuth, JWT). | - | **Telemetry & Observability** | Istio collects metrics, logs, and traces for monitoring service performance and behavior. Integrates with tools like Prometheus, Grafana, and Jaeger. | - | **Distributed Tracing** | Integrates with tracing systems like **Jaeger** and **Zipkin** to provide end-to-end tracing for debugging and monitoring service interactions. | + | **Telemetry & Observability** | Istio collects metrics, logs, and traces for monitoring service's performance and behavior. It integrates with tools like Prometheus, Grafana, and Jaeger. | + | **Distributed Tracing** | Istio integrates with tracing systems like **Jaeger** and **Zipkin** to provide end-to-end tracing for debugging and monitoring service interactions. | | **Policy Enforcement** | Istio provides fine-grained control over traffic policies, such as rate limiting, quotas, and security policies, using its Policy and Telemetry components. | | **Resilience & Retries** | Istio can retry failed requests, set timeouts, and apply circuit breakers to prevent cascading failures and enhance the reliability of services. | | **Sidecar Proxy (Envoy)** | Istio uses **Envoy** as a sidecar proxy to intercept and manage network traffic, providing a transparent proxy between microservices. | @@ -89,6 +65,7 @@ - Istio core components: + | **Components** | **Description** | | --------------- | ------------------------------------------------------------------------ | | **Envoy Proxy** | A sidecar proxy that intercepts traffic to and from microservices. | @@ -96,18 +73,19 @@ | **Mixer** | Provides policy enforcement and telemetry data collection. | | **Citadel** | Handles security-related tasks like identity and certificate management. | +--- -### `Kiali` +### `Kiali` - **Kiali** is a graphical user interface (GUI) for `Istio`. -- It helps you visualize the service mesh and provides insights into how microservices are interacting with each other. +- It helps you visualize the service mesh and provides insights on how microservices are interacting with each other. - `Kiali` integrates deeply with Istio, allowing you to view: - - The **service mesh topology** showing services, traffic flow, and dependencies. - - Metrics like request rates, latencies, and error rates. - - **Distributed tracing** (if enabled) for better debugging and troubleshooting. - - **Istio configuration** to visualize resources like VirtualServices, DestinationRules, and more. + - The **service mesh topology**, showing services, traffic flow, and dependencies. + - Metrics like request rates, latencies, and error rates. + - **Distributed tracing** (if enabled) for better debugging and troubleshooting. + - **Istio configuration** to visualize resources like VirtualServices, DestinationRules, and more. -- `Kiali` simplifies the operation of Istio by offering an intuitive way to manage and visualize your service mesh. +- `Kiali` simplifies the operation of `Istio` by offering an intuitive way to manage and visualize your service mesh. --- @@ -116,9 +94,9 @@ ### Step 01: Install Istio Using Istioctl -- Istio supply an installer +- Istio supplies an installer - Go to the [Istio releases page](https://github.com/istio/istio/releases) and download the latest version of Istio. -- Alternatively, use `istioctl` to install Istio. +- Alternatively, use `istioctl` to install Istio, as follows: ```bash # Install Istio using istioctl @@ -129,7 +107,7 @@ # Add bin directory to your $PATH export PATH=$PWD/bin:$PATH - # Install istio will all features enabled (demo profile) + # Install istio with all features enabled (demo profile) istioctl install --set profile=demo -y ``` @@ -138,28 +116,33 @@ ### Step 02: Verify Istio installation - Check the Istio system components in the `istio-system` namespace: - ```bash - # Verify Istio installation - kubectl get pods -n istio-system - ``` -- You should see several pods, including the Istio control plane components like - - `istiod` - - `istio-ingressgateway` - - `istio-egressgateway` + +```bash +# Verify Istio installation +kubectl get pods -n istio-system +``` + +- You should see several pods, including the Istio control plane components like: + - `istiod` + - `istio-ingressgateway` + - `istio-egressgateway` -
- **`kubectl get pods -n istio-system`** - ```plaintext - NAME READY STATUS RESTARTS AGE - istio-egressgateway-684f5dc857-bzww6 1/1 Running 0 21m - istio-ingressgateway-6b5bd79c5c-9n8tg 1/1 Running 0 21m - istiod-68885d595-vv2ft 1/1 Running 0 22m - ``` +```bash +kubectl get pods -n istio-system +``` + + + ```plaintext + NAME READY STATUS RESTARTS AGE + istio-egressgateway-684f5dc857-bzww6 1/1 Running 0 21m + istio-ingressgateway-6b5bd79c5c-9n8tg 1/1 Running 0 21m + istiod-68885d595-vv2ft 1/1 Running 0 22m + ``` ### Step 03: Install Kiali -- We will install kiali with Helm +- We will install Kiali using Helm: ```bash # Add the Kiali Helm chart repository @@ -183,40 +166,42 @@ ### Step 04: Verify Kiali installation -- Once installed, check the status of Kiali: - ```bash - kubectl get pods -n istio-system - ``` +- Once installed, check the status of `Kiali`: -- You should see the Kiali pod running, along with the Istio components from previous step. - ```plaintext - NAME READY STATUS RESTARTS AGE - istio-egressgateway-684f5dc857-bzww6 1/1 Running 0 28m - istio-ingressgateway-6b5bd79c5c-9n8tg 1/1 Running 0 28m - istiod-68885d595-vv2ft 1/1 Running 0 28m - kiali-68ccc848b6-j4q28 1/1 Running 0 27m - ``` +```bash +kubectl get pods -n istio-system +``` + +- You should see the `Kiali` pod running, along with the `Istio` components from previous step. + +```plaintext +NAME READY STATUS RESTARTS AGE +istio-egressgateway-684f5dc857-bzww6 1/1 Running 0 28m +istio-ingressgateway-6b5bd79c5c-9n8tg 1/1 Running 0 28m +istiod-68885d595-vv2ft 1/1 Running 0 28m +kiali-68ccc848b6-j4q28 1/1 Running 0 27m +``` --- ## Part 02 - Viewing the Network with Istio -- Istio uses a sidecar proxy model, where an Envoy proxy is deployed alongside each microservice pod. +- `Istio` uses a sidecar proxy model, where an envoy proxy is deployed alongside each microservice pod. - This proxy intercepts and manages traffic between the services. ### Step 01: Enable Istio Injection - You need to enable **Istio sidecar injection** for your Kubernetes namespace. -- This will ensure that new pods in the `default` namespace will automatically have the Envoy proxy sidecar injected. +- This will ensure that new pods in the `default` namespace will automatically have the envoy proxy sidecar injected. - For example, to enable injection in the `default` namespace: ```bash kubectl label namespace default istio-injection=enabled ``` -### Step 2: Deploy Sample Application +### Step 02: Deploy Sample Application -- To see Istio in action, deploy a sample application, such as **Bookinfo** , which is available in Istio's demo repository. +- To see `Istio` in action, deploy a sample application, such as **Bookinfo**, which is available in `Istio`'s demo repository. ```bash # Deploy the sample application supplied by istio @@ -233,7 +218,7 @@ ### Step 04: Expose the Application -- To expose the application via Istio's ingress gateway, create an Istio **Gateway** and **VirtualService** . +- To expose the application via `Istio's` ingress gateway, create an `Istio` **Gateway** and **VirtualService** . ```bash # This will expose the 'Bookinfo' application to the external world via Istio ingress gateway. @@ -248,8 +233,8 @@ ### Step 01: Access Kiali Dashboard -- Once Kiali is installed, you can access its dashboard. -- First, define a port-forward the Kiali service: +- Once `Kiali` is installed, you can access it's dashboard. +- First, port-forward to the `Kiali` service: ```bash kubectl port-forward \ @@ -263,22 +248,22 @@ ### Step 02: Explore the Service Mesh Topology -- Once inside the Kiali dashboard, Open the `Mesh` View +- Once inside the `Kiali` dashboard, open the `Mesh` View - You will see a **graph** of your services in the mesh. - The graph shows the interactions between microservices, along with traffic flows, success/error rates, and latency. -- You can use the Kiali interface to: - - **Zoom in/out** of the topology. - - View detailed metrics for each service. - - Understand the traffic flow, including retries, timeouts, and error rates. +- You can use the `Kiali` interface to: + - **Zoom in/out** of the topology. + - View detailed metrics for each service. + - Understand the traffic flow, including retries, timeouts, and error rates. --- ## Part 04: Creating a Demo Istio VirtualService -- In Istio, **VirtualServices** are used to define the routing rules for your services. +- In `Istio`, **VirtualServices** are used to define the routing rules for your services. -### Step 1: Define a VirtualService +### Step 01: Define a VirtualService - Create a `VirtualService` resource to route traffic to the `ratings` service in the **Bookinfo** demo app. ```yaml @@ -298,7 +283,7 @@ ``` ### Step 02: Apply the VirtualService -- Apply the `VirtualService` +- Apply the `VirtualService`. - This will route all traffic for the `ratings` service to version `v2`. ```bash @@ -307,13 +292,13 @@ ### Step 03: Verify the Routing -- You can use Kiali to visualize the traffic flow and verify that the routing is happening as expected. -- The Kiali dashboard should reflect the new route configuration for `ratings`. +- You can use `Kiali` to visualize the traffic flow and verify that routing is happening as expected. +- The `Kiali` dashboard should reflect the new route configuration for `ratings`. --- ## Conclusion -- You have now successfully installed Istio and Kiali, set up a service mesh, and visualized your network's behavior. -- The combination of Istio's powerful traffic management features and Kiali's intuitive visualization interface makes it easier to manage and monitor microservices in a Kubernetes cluster. +- You have now successfully installed `Istio` and `Kiali`, set up a service mesh, and visualized your network's behavior. +- The combination of `Istio's` powerful traffic management features and `Kiali's` intuitive visualization interface makes it easier to manage and monitor microservices in a Kubernetes cluster. diff --git a/Labs/11-CRD-Custom-Resource-Definition/README.md b/Labs/11-CRD-Custom-Resource-Definition/README.md index 55fbdba..5613ba9 100644 --- a/Labs/11-CRD-Custom-Resource-Definition/README.md +++ b/Labs/11-CRD-Custom-Resource-Definition/README.md @@ -7,41 +7,19 @@ # Custom resources definition (CRD) ### Intro -- **Custom resources definition (CRD)** was added to Kubernetes 1.7 -- CRD added the ability to define Custom objects/resources +- `Custom resources definition` (**CRD**) was added to Kubernetes 1.7. +- `CRD` added the ability to define custom objects/resources. --- #### What is a Custom Resource Definition(CRD) -- Custom resources - A resource is an endpoint in the Kubernetes API that stores a collection of API objects of a certain kind; for example, the built-in pods resource contains a collection of Pod objects. - A custom resource is an **extension of the Kubernetes API** that is not necessarily available in a default Kubernetes installation. It represents a customization of a particular Kubernetes installation. However, many core Kubernetes functions are now built using custom resources, making Kubernetes more modular. +- A resource is an endpoint in the Kubernetes API that stores a collection of API objects of a certain kind; for example, the builtin pods resource contains a collection of Pod objects. - Custom resources can appear and disappear in a running cluster through **dynamic registration**, and cluster admins can update custom resources independently of the cluster itself. - - Once a custom resource is installed, users can create and access its objects using `kubectl`, just as they do for built-in resources like Pods. +- A custom resource is an **extension of the Kubernetes API** that is not necessarily available in a default Kubernetes installation. It represents a customization of a particular Kubernetes installation. However, many core Kubernetes functions are now built using custom resources, making Kubernetes more modular. - The custom resource created is also stored in the etcd cluster with proper replication and lifecycle management. +- Custom resources can appear and disappear in a running cluster through **dynamic registration**, and cluster admins can update custom resources independently of the cluster itself. ---- - - - ---- - -
-:arrow_left:  - 10-Istio -  ||   12-Wordpress-MySQL-PVC -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) +- Once a custom resource is installed, users can create and access its objects using `kubectl`, just as they do for built-in resources like Pods. - \ No newline at end of file +- The custom resource created is also stored in the `etcd` cluster with proper replication and lifecycle management. diff --git a/Labs/12-Wordpress-MySQL-PVC/README.md b/Labs/12-Wordpress-MySQL-PVC/README.md index 4c50fa1..bd9ec64 100644 --- a/Labs/12-Wordpress-MySQL-PVC/README.md +++ b/Labs/12-Wordpress-MySQL-PVC/README.md @@ -6,62 +6,46 @@ --- # WordPress, MySQL, PVC -- In This tutorial you will deploy a WordPress site and a MySQL database. +- In this tutorial you will deploy a WordPress site and a MySQL database. - You will use `PersistentVolumes` and `PersistentVolumeClaims` as storage. --- ## Walkthrough - Patch `minikube` so we can use `Service: LoadBalancer` - ```sh - # Sourse: - # https://github.com/knative/serving/blob/b31d96e03bfa1752031d0bc4ae2a3a00744d6cd5/docs/creating-a-kubernetes-cluster.md#loadbalancer-support-in-minikube - sudo ip route add \ - $(cat ~/.minikube/profiles/minikube/config.json | \ - jq -r ".KubernetesConfig.ServiceCIDR") \ - via $(minikube ip) - kubectl run minikube-lb-patch \ - --replicas=1 \ - --image=elsonrodriguez/minikube-lb-patch:0.1 \--namespace=kube-system - ``` -- Create the desired Namespace -- Create the MySQL resources +```sh +# Source: +# https://github.com/knative/serving/blob/b31d96e03bfa1752031d0bc4ae2a3a00744d6cd5/docs/creating-a-kubernetes-cluster.md#loadbalancer-support-in-minikube + +sudo ip route add \ + $(cat ~/.minikube/profiles/minikube/config.json | \ + jq -r ".KubernetesConfig.ServiceCIDR") \ + via $(minikube ip) + +kubectl run minikube-lb-patch \ + --replicas=1 \ + --image=elsonrodriguez/minikube-lb-patch:0.1 \--namespace=kube-system +``` + +- Create the desired `Namespace` +- Create the `MySQL` resources: - Create `Service` - Create `PersistentVolumeClaims` - Create `Deployment` - - Create password file -- Create the WordPress resources + - Create `password file` +- Create the WordPress resources: - Create `Service` - Create `PersistentVolumeClaims` - Create `Deployment` -- Create a `kustomization.yaml` with - - Secret generator - - MySQL resources - - WordPress resources +- Create a `kustomization.yaml` with: + - `Secret generator` + - `MySQL` resources + - `WordPress` resources - Deploy the stack - Port forward from the host to the application - - We use a port forward so we will be able to test and verify if the WordPress is actually running - ```sh - kubectl port-forward service/wordpress 8080:32267 -n wp-demo - ``` - - - ---- - -
-:arrow_left:  - 11-CRD-Custom-Resource-Definition -  ||   13-HelmChart -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) +- We use a port forward so we will be able to test and verify if the WordPress is actually running: - \ No newline at end of file +```sh +kubectl port-forward service/wordpress 8080:32267 -n wp-demo +``` diff --git a/Labs/13-HelmChart/README.md b/Labs/13-HelmChart/README.md index ebfef94..cdece36 100644 --- a/Labs/13-HelmChart/README.md +++ b/Labs/13-HelmChart/README.md @@ -1,106 +1,67 @@ ![](../../resources/k8s-logos.png) - -# Kubernetes Hands-on Lab: Helm Chart +# K8S Hands-on ![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) --- -## Overview +# Helm Chart -- Welcome to the Helm Chart hands-on lab! In this tutorial, you'll learn the essentials of Helm (version 3), the package manager for Kubernetes. -- You'll build, package, install, and manage applications using Helm charts, gaining practical experience with real Kubernetes resources. +- Welcome to the `Helm` Chart hands-on lab! In this tutorial, you'll learn the essentials of `Helm` (version 3), the package manager for Kubernetes. +- You'll build, package, install, and manage applications using `Helm` charts, gaining practical experience with real Kubernetes resources. --- + +## Pre requirements -## Prerequisites - -- [Helm](https://helm.sh/docs/intro/install/) installed -- Access to a Kubernetes cluster (local or remote) +- [`Helm`](https://helm.sh/docs/intro/install/) installed +- K8S cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/) configured to interact with your cluster ---- [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) - ### **CTRL + click to open in new window** - + --- -## What You'll Learn +## What will you learn -- What Helm is and why it's useful -- Helm chart structure and key files -- Common Helm commands for managing releases -- How to create, package, install, upgrade, and rollback a Helm chart +- What `Helm` is and why is it useful +- `Helm` chart structure and key files +- Common `Helm` commands for managing releases +- How to create, pack, install, upgrade, and rollback a `Helm` chart - Troubleshooting and best practices --- -## Table of Contents - -- [Overview](#overview) -- [Prerequisites](#prerequisites) -- [What You'll Learn](#what-youll-learn) -- [Table of Contents](#table-of-contents) -- [Introduction](#introduction) - - [Terminology](#terminology) - - [Chart files and folders](#chart-files-and-folders) - - [Common Helm Commands](#common-helm-commands) -- [Lab](#lab) - - [Step 01. Installing Helm](#step-01-installing-helm) - - [Verify Installation](#verify-installation) - - [Step 02 - Creating our Helm chart](#step-02---creating-our-helm-chart) - - [Create a New Chart](#create-a-new-chart) - - [Navigate to the Chart Directory](#navigate-to-the-chart-directory) - - [Write the chart content](#write-the-chart-content) - - [Step 03 - Pack the chart](#step-03---pack-the-chart) - - [`helm package`](#helm-package) - - [Step 04 - Validate the chart content](#step-04---validate-the-chart-content) - - [`helm template`](#helm-template) - - [Step 05 - Install the chart](#step-05---install-the-chart) - - [`helm install`](#helm-install) - - [Step 06: Verify the installation](#step-06-verify-the-installation) - - [Step 07: Test the service](#step-07-test-the-service) - - [Step 08: Upgrade the release to newer version](#step-08-upgrade-the-release-to-newer-version) - - [Step 09: Check the upgrade](#step-09-check-the-upgrade) - - [Step 10 - History:](#step-10---history) - - [`helm history`](#helm-history) - - [Step 11 - Rollback](#step-11---rollback) - - [`helm rollback`](#helm-rollback) -- [Finalize \& Cleanup](#finalize--cleanup) -- [Troubleshooting](#troubleshooting) -- [Next Steps](#next-steps) - ---- - ## Introduction - `Helm` is the **package manager** for Kubernetes. - It simplifies the deployment, management, and upgrade of applications on your Kubernetes cluster. -- Helm helps you manage Kubernetes applications by providing a way to define, install, and upgrade complex Kubernetes applications. -- When packaging applications as Helm charts, you gain a standardized and reusable approach for deploying and managing your services. +- `Helm` helps you manage Kubernetes applications by providing a way to define, install, and upgrade complex Kubernetes applications. +- When packing applications as `Helm` charts, you gain a standardized and reusable approach for deploying and managing your services. -- A Helm chart consists of a few files that define the Kubernetes resources that will be **created** when the chart is installed. +- A `Helm` chart consists of a few files that define the Kubernetes resources that will be **created** when the chart is installed. - These files include the: - - `Chart.yaml` file, which contains metadata about the chart, such as its name and version, and the + - `Chart.yaml` file, which contains metadata about the chart, such as its name and version, and the chart's dependencies and maintainers. - `values.yaml` file, which contains the configuration values for the chart. - - The `templates` directory contains the Kubernetes resource templates that will be used to create the actual resources in the cluster. + - The `templates` directory which contains the Kubernetes resource templates to be used to create the actual resources in the cluster. ### Terminology * `Chart` - - A Helm package is called a **chart**. - - Charts are versioned, shareable packages that contain all the Kubernetes resources needed to run an application. + - A `Helm` package is called a **chart**. + - Charts are versioned, shareable packages that contain all the Kubernetes resources needed to run an application. * `Release` - - A specific instance of a chart is called a **release**. - - Each release is a deployed *version of a chart*, with its own configuration, resources, and revision history. + - A specific instance of a chart is called a **release**. + - Each release is a deployed *version of a chart*, with its own configuration, resources, and revision history. * `Repository` - - A collection of charts is stored in a Helm repository. - - Helm charts can be hosted in public or private repositories for easy sharing and distribution. + - A collection of charts is stored in a `Helm` repository. + - `Helm` charts can be hosted in public or private repositories for easy sharing and distribution. ### Chart files and folders @@ -112,8 +73,9 @@ | `charts/` | Directory containing dependencies of the chart. | | `README.md` | Documentation for the chart, explaining how to use and configure it. | +##### codewizard-helm-demo Helm Chart tructure + ```sh -## codewizard-helm-demo - Chart.yaml # Defines chart metadata and values schema - values.yaml # Default configuration values - templates/ # Deployment templates using Go templating language @@ -122,233 +84,243 @@ - README.md # Documentation for your chart ``` -### Common Helm Commands +### Common `Helm` Commands + +Here are some of the most common `Helm` commands you’ll use when working with `Helm` charts: -Here are some of the most common Helm commands you’ll use when working with Helm charts: | Command | Description | | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | -| helm **create** `chart-name` | Create a new Helm chart with the specified name. | -| helm **install** `release-name` `chart-path` | Install a Helm chart to your Kubernetes cluster. | -| helm **upgrade** `release-name` `chart-path` | Upgrade an installed release with a new version of a chart. | -| | | -| helm **uninstall** `release-name` | Uninstall a release from the Kubernetes cluster. | -| helm **list** | List all installed Helm releases in the cluster. | -| helm **status** `release-name` | Show the status of a deployed Helm release. | -| helm **rollback** `release-name` `revision` | Rollback a release to a previous revision. | -| helm **get all** `release-name` | Retrieve all information about a deployed release (e.g., templates, values). | -| helm **show values** `chart-name` | Show the default values of a Helm chart. | -| helm **template** `chart-name` | Generate the output of the Helm chart. | -| helm **lint** `chart-path` | This command takes a path to a chart and runs a series of tests to verify that the chart is well-formed. | -| helm **history** `chart-name` | This command takes a path to a chart and runs a series of tests to verify that the chart is well-formed. | +| `helm` **create** `chart-name` | Create a new `Helm` chart with the specified name. | +| `helm` **install** `release-name` `chart-path` | Install a `Helm` chart to your Kubernetes cluster. | +| `helm` **upgrade** `release-name` `chart-path` | Upgrade an installed release with a new version of a chart. | +| `helm` **uninstall** `release-name` | Uninstall a release from the Kubernetes cluster. | +| `helm` **list** | List all installed `Helm` releases in the cluster. | +| `helm` **status** `release-name` | Show the status of a deployed `Helm` release. | +| `helm` **rollback** `release-name` `revision` | Rollback a release to a previous revision. | +| `helm` **get all** `release-name` | Retrieve all information about a deployed release (e.g., templates, values). | +| `helm` **show values** `chart-name` | Show the default values of a `Helm` chart. | +| `helm` **template** `chart-name` | Generate the output of the `Helm` chart. | +| `helm` **lint** `chart-path` | This command takes a path to a chart and runs a series of tests to verify that the chart is well-formed. | +| `helm` **history** `chart-name` | This command takes a path to a chart and runs a series of tests to verify that the chart is well-formed. | --- -## Lab +# Lab + +### Step 01 - Installing `Helm` -### Step 01. Installing Helm +- Before you can use the `codewizard-helm-demo` chart, you'll need to **install** `Helm` on your local machine. -- Before you can use the `codewizard-helm-demo` chart, you'll need to **install** Helm on your local machine. +- `Helm` install methods by OS: -- Steps to Install Helm - | OS | Command | - | ------------------------ | ---------------------------------------------------------------------------------- | - | Linux | `curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 \| bash` | - | MacOS | `brew install helm` | - | Windows (via Chocolatey) | `choco install kubernetes-helm` | + | OS | Command | + | ------------------------ | ---------------------------------------------------------------------------------- | + | Linux | `curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 \| bash` | + | MacOS | ``brew install helm`` | + | Windows (via Chocolatey) | ``choco install kubernetes-helm`` | #### Verify Installation - - To confirm that Helm is installed correctly, run: - ```bash - $ helm version - version.BuildInfo{Version:"xx", GitCommit:"xx", GitTreeState:"clean", GoVersion:"xx"} - ``` + - To confirm that `Helm` is installed correctly, run: + + ```bash + $ helm version + + ## Expected output + version.BuildInfo{Version:"xx", GitCommit:"xx", GitTreeState:"clean", GoVersion:"xx"} + ``` + +--- -### Step 02 - Creating our Helm chart +### Step 02 - Creating our `Helm` chart -- Creating our custom `codewizard-helm-demo` Helm chart -- The custom `codewizard-helm-demo` Helm chart is build upon the following K8S resources: +- Creating our custom `codewizard-helm-demo` `Helm` chart +- The custom `codewizard-helm-demo` `Helm` chart is build upon the following K8S resources: - - ConfigMap - - Deployment - - Service + - ConfigMap + - Deployment + - Service -- As mentioned above we will also have the following Helm resources: - - Chart.yaml - - values.yaml - - templates/\_helpers.tpl +- As mentioned above, we will also have the following `Helm` resources: + - Chart.yaml + - values.yaml + - templates/\_helpers.tpl #### Create a New Chart -- First, you need to create a Helm chart using the `helm create` command. +- First, we need to create a `Helm` chart using the ``helm create`` command. - This command will generate the necessary file structure for your new chart. ```bash helm create codewizard-helm-demo ``` -- ?? Question: What is result of this command? Examine the chart structure +??? Question "What is the result of this command?" + Examine the chart structure! #### Navigate to the Chart Directory - - Move into the Chart Directory - - ```bash - cd codewizard-helm-demo - ``` + ```bash + cd codewizard-helm-demo + ``` #### Write the chart content - - Copy the content of the chart folder (in this lab) to the chart directory (overwrite the files) + - Copy the content of the chart folder (in this lab) to the chart directory (overwriting the files). ### Step 03 - Pack the chart -- After you’ve create or customized your chart, you need to pack it as `.tgz` file, which can then be shared or installed. +- After we have created or customized our chart, we need to pack it as `.tgz` file, which can then be shared or installed. - #### `helm package` +#### helm package -> [!NOTE] -> `helm package` packages a chart into a **versioned chart archive file**. -> If a path is given, this will look at that path for a chart which must contain a `Chart.yaml` file and then package that directory. +!!! warning "Helm Package" + `helm package` packages a chart into a **versioned chart archive file**. + If a path is given, this will "look" at that path for a chart which must contain a `Chart.yaml` file and then pack that directory. - ```sh - helm package codewizard-helm-demo - ``` +```sh +helm package codewizard-helm-demo +``` -- This command will create a file called `codewizard-helm-demo-.tgz` in your current directory. +- This command will create a file called `codewizard-helm-demo-.tgz` inside your current directory. ### Step 04 - Validate the chart content -#### `helm template` +#### ``helm template`` -- Helm allows you to **generate** the Kubernetes manifests based on the templates and values files without actually installing the chart. -- This is useful to preview what the generated resources will look like. +- `Helm` allows you to **generate** the Kubernetes manifests based on the templates and values files without actually installing the chart. +- This is useful to preview what the generated resources will look like: - ```sh - helm template codewizard-helm-demo - ``` -- This will output the rendered Kubernetes manifests to your terminal. +```sh +helm template codewizard-helm-demo + +## This will output the rendered Kubernetes manifests to your terminal +``` ### Step 05 - Install the chart - Install the `codewizard-helm-demo` chart into Kubernetes cluster -#### `helm install` + +#### The ``helm install`` command - This command installs a chart archive. -- The install argument must be a chart reference, a path to a packaged chart, a path to an unpacked chart directory or a URL. -- To override values in a chart: - - `--values` - pass in a file - - `--set` - pass configuration from the command line - - ``` - # Install the packed helm - helm install codewizard-helm-demo codewizard-helm-demo-0.1.0.tgz - ``` +- The install argument must be a chart reference, a path to a packed chart, a path to an unpacked chart directory or a URL. +- To override values in a chart, use: + - `--values` - pass in a file + - `--set` - pass configuration from the command line -### Step 06: Verify the installation -- Examine newly created Helm chart release, and all cluster created resources +```sh +# Install the packed helm chart +helm install codewizard-helm-demo codewizard-helm-demo-0.1.0.tgz +``` - ``` - # List the installed helms - helm ls +### Step 06 - Verify the installation - # Check the resources - kubectl get all -n codewizard - ``` +- Examine newly created `Helm` chart release, and all cluster created resources: -### Step 07: Test the service +```sh +# List the installed helms +helm ls -- Perform an HTTP GET request, send it to the newly created cluster service -- Confirm that the response contains the `CodeWizard Helm Demo` message passed from the `values.yaml` file +# Check the resources +kubectl get all -n codewizard +``` - ```sh - kubectl run busybox \ - --image=busybox \ - --rm \ - -it \ - --restart=Never \ - -- /bin/sh -c "wget -qO- http://codewizard-helm-demo.codewizard.svc.cluster.local" +### Step 07 - Test the service - ### Output: - `CodeWizard Helm Demo` - ``` +- Perform an `HTTP GET` request, send it to the newly created cluster service. +- Confirm that the response contains the `CodeWizard Helm Demo` message passed from the `values.yaml` file. -### Step 08: Upgrade the release to newer version +```sh +kubectl run busybox \ + --image=busybox \ + --rm \ + -it \ + --restart=Never \ + -- /bin/sh -c "wget -qO- http://codewizard-helm-demo.codewizard.svc.cluster.local" + +### Output: +CodeWizard Helm Demo +``` -- Perform a Helm upgrade on the `codewizard-helm-demo` release +### Step 08 - Upgrade the release to newer version - ``` - # upgrade and pass different message than the one from the default values - # Use the --set to pass the desired value - helm upgrade \ - codewizard-helm-demo \ - codewizard-helm-demo-0.1.0.tgz \ - --set nginx.conf.message="Helm Rocks" - ``` +- Perform a Helm upgrade on the `codewizard-helm-demo` release: + +```sh +# upgrade and pass a different message than the one from the default values +# Use the --set to pass the desired value +helm upgrade \ + codewizard-helm-demo \ + codewizard-helm-demo-0.1.0.tgz \ + --set nginx.conf.message="Helm Rocks" +``` -### Step 09: Check the upgrade +### Step 09 - Check the upgrade -- Perform another HTTP GET request. -- Confirm that the response now has the updated message `Helm Rocks` +- Perform another `HTTP GET` request. +- Confirm that the response now has the updated message `Helm Rocks`: ```sh - kubectl run busybox \ - --image=busybox \ - --rm \ - -it \ - --restart=Never \ - -- /bin/sh -c "wget -qO- http://codewizard-helm-demo.codewizard.svc.cluster.local" - - ### Output: - `Helm Rocks` - ``` +kubectl run busybox \ + --image=busybox \ + --rm \ + -it \ + --restart=Never \ + -- /bin/sh -c "wget -qO- http://codewizard-helm-demo.codewizard.svc.cluster.local" + +### Output: +Helm Rocks +``` -### Step 10 - History: +### Step 10 - History - Examine the `codewizard-helm-demo` release history - #### `helm history` +#### `helm history` - `helm history` prints historical revisions for a given release. - - A default maximum of 256 revisions will be returned + - A default maximum of 256 revisions will be returned. - ``` - helm history codewizard-helm-demo +```sh +$ helm history codewizard-helm-demo - ### Sample output - REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION - 1 ... superseded codewizard-helm-demo-0.1.0 1.19.7 Install complete - 2 ... deployed codewizard-helm-demo-0.1.0 1.19.7 Upgrade complete - ``` +### Sample output +REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION +1 ... superseded codewizard-helm-demo-0.1.0 1.19.7 Install complete +2 ... deployed codewizard-helm-demo-0.1.0 1.19.7 Upgrade complete +``` ### Step 11 - Rollback - #### `helm rollback` +#### `helm rollback` - - Rollback the `codewizard-helm-demo` release to previous version + - Rollback the `codewizard-helm-demo` release to previous version: - ```sh - helm rollback codewizard-helm-demo +```sh +$ helm rollback codewizard-helm-demo + +### Output: +Rollback was a success! Happy Helming! +``` - ### Output: - Rollback was a success! Happy Helming! +- Check again to verify that you get the original message! - ``` - - Check again to verify that you get the original message +--- ## Finalize & Cleanup -To remove all resources created by this lab, uninstall the `codewizard-helm-demo` release: +- To remove all resources created by this lab, uninstall the `codewizard-helm-demo` release: ```sh helm uninstall codewizard-helm-demo ``` -(Optional) If you created a dedicated namespace for this lab, you can delete it: +- (Optional) If you have created a dedicated namespace for this lab, you can delete it by runniung: ```sh kubectl delete namespace codewizard @@ -360,57 +332,52 @@ kubectl delete namespace codewizard - **Helm not found:** - Make sure Helm is installed and available in your `PATH`. Run `helm version` to verify. +Make sure `Helm` is installed and available in your `PATH`. +Run the following to verify: + +```sh +helm version +``` + +
- **Pods not starting:** - Check pod status and logs: +Check pod status and logs by running the following commands: - ```sh - kubectl get pods -n codewizard - kubectl describe pod -n codewizard - kubectl logs -n codewizard - ``` +```sh +kubectl get pods -n codewizard +kubectl describe pod -n codewizard +kubectl logs -n codewizard +``` + +
- **Service not reachable:** - Ensure the service and pods are running: +Ensure the service and pods are running by running the following commands: - ```sh - kubectl get svc -n codewizard - kubectl get pods -n codewizard - ``` +```sh +kubectl get svc -n codewizard +kubectl get pods -n codewizard +``` + +
- **Values not updated after upgrade:** - Double-check your `--set` or `--values` flags and confirm the upgrade with: +Double-check your `--set` or `--values` flags and confirm the upgrade by running: - ```sh - helm get values codewizard-helm-demo - ``` +```sh +helm get values codewizard-helm-demo +``` --- ## Next Steps -- Try creating your own Helm chart for a different application. -- Explore Helm chart repositories like [Artifact Hub](https://artifacthub.io/). -- Learn about advanced Helm features: dependencies, hooks, and chart testing. -- Integrate Helm with CI/CD pipelines for automated deployments. +- Try creating your own `Helm` chart for a different application. +- Explore `Helm` chart repositories like [Artifact Hub](https://artifacthub.io/). +- Learn about advanced `Helm` features, such as: dependencies, hooks, and chart testing. +- Integrate `Helm` with CI/CD pipelines for automated deployments. - Read more in the [official Helm documentation](https://helm.sh/docs/). - ---- - -12-Wordpress-MySQL-PVC -  ||   14-Logging -   :arrow_right: - ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - diff --git a/Labs/14-Logging/README.md b/Labs/14-Logging/README.md new file mode 100644 index 0000000..0ddde84 --- /dev/null +++ b/Labs/14-Logging/README.md @@ -0,0 +1,185 @@ +![](../../resources/k8s-logos.png) + +# K8S Hands-on + +![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) + +--- + +## Logging + +- Welcome to the `Logging` hands-on lab! In this tutorial, we will learn the essentials of `Logging` in Kubernetes clusters. +- We will deploy a sample application, configure log collection, and explore logs using popular tools like `Fluentd`, `Elasticsearch`, and `Kibana` (EFK stack). + +--- + +## Pre requirements + +- Kubernetes cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/) configured to interact with your cluster +- [Helm](https://helm.sh/docs/intro/install/) installed for easier deployment + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) + +### **CTRL + click to open in new window** + +--- + +## What will we learn? + +- Why `Logging` is important in Kubernetes +- How to deploy a sample app that generates logs +- How to collect logs using Fluentd +- How to store and search logs with `Elasticsearch` +- How to visualize logs with `Kibana` +- Troubleshooting and best practices + +--- + +## Introduction + +- `Logging` is critical for monitoring, debugging, and auditing applications in Kubernetes. +- Kubernetes does not provide a builtin, centralized `Logging` solution, but it allows us to integrate with many `Logging` stacks. +- We will set up the EFK stack (`Elasticsearch`, `Fluentd`, `Kibana`) to collect, store, and visualize logs from our cluster. + +--- + +## Lab + +### Step 01 - Deploy a Sample Application + +- Deploy a simple `Nginx` application that generates access logs. + +```sh +kubectl create deployment nginx --image=nginx +kubectl expose deployment nginx --port=80 --type=NodePort +``` + +- Check that the pod is running: + +```sh +kubectl get pods +``` + +### Step 02 - Deploy `Elasticsearch` + +- Deploy `Elasticsearch` using `Helm`: + +```sh +helm repo add elastic https://helm.elastic.co +helm repo update +helm install elasticsearch elastic/elasticsearch --set replicas=1 --set minimumMasterNodes=1 +``` + +- Wait for the pod to be ready and check its status: + +```sh +kubectl get pods +``` + + +### Step 03 - Deploy `Kibana` + +- Deploy `Kibana` using `Helm`: + +```sh +helm install kibana elastic/kibana +``` + +- Forward the `Kibana` port: + +```sh +kubectl port-forward svc/kibana-kibana 5601:5601 & +``` + +!!! warning "If you are running this lab in Google Cloud Shell:" + 1. After running the port-forward command above, click the **Web Preview** button in the Cloud Shell toolbar (usually at the top right). + 2. Enter port `5601` when prompted. + 3. This will open `Kibana` in a new browser tab at a URL like `https://.shell.cloud.google.com/?port=5601`. + 4. If you see a warning about an untrusted connection, you can safely proceed. + +- Access `Kibana` at [http://localhost:5601](http://localhost:5601) (if running locally) or via the Cloud Shell Web Preview, as explained above. + +### Step 04 - Deploy `Fluentd` + +- Deploy `Fluentd` as a `DaemonSet` to collect logs from all nodes and forward them to `Elasticsearch`. + +```sh +kubectl apply -f https://raw.githubusercontent.com/fluent/fluentd-kubernetes-daemonset/master/fluentd-daemonset-elasticsearch-rbac.yaml +``` + +- Check that `Fluentd` pods are running: + +```sh +kubectl get pods -l app=fluentd +``` + +### Step 05 - Generate and View Logs + +- Access the `Nginx` service to generate logs: + +```sh +minikube service nginx +``` + + +In `Kibana`, configure an index pattern to view logs: + +1. Open Kibana in your browser (using the Cloud Shell Web Preview as described above). +2. In the left menu, click **Stack Management** > **Kibana** > **Index Patterns**. +3. Click **Create index pattern**. +4. In the "Index pattern" field, enter `fluentd-*` (or `logstash-*` if your logs use that prefix). +5. Click **Next step**. +6. For the time field, select `@timestamp` and click **Create index pattern**. +7. Go to **Discover** in the left menu to view and search your logs. + +Explore the logs, search, and visualize traffic. + +--- + +## Troubleshooting + +##### **Pods not starting:** + - Check pod status and logs: + +```sh +kubectl get pods +kubectl describe pod +kubectl logs +``` + +
+ +##### **Kibana not reachable:** + + - Ensure port-forward is running and no firewall is blocking port 5601. + +
+ +##### **No logs in Kibana:** + + - Check Fluentd and Elasticsearch pod logs for errors. + - Ensure index pattern is set up correctly in Kibana. + +--- + +## Cleanup + +- To remove all resources created by this lab: + +```sh +helm uninstall elasticsearch +helm uninstall kibana +kubectl delete deployment nginx +kubectl delete service nginx +kubectl delete -f https://raw.githubusercontent.com/fluent/fluentd-kubernetes-daemonset/master/fluentd-daemonset-elasticsearch-rbac.yaml +``` + +--- + +## Next Steps + +- Try deploying other logging stacks like `Loki` + `Grafana`. +- Explore log aggregation, alerting, and retention policies. +- Integrate logging with monitoring and alerting tools. +- Read more in the [Kubernetes logging documentation](https://kubernetes.io/docs/concepts/cluster-administration/logging/). diff --git a/Labs/15-Prometheus-Grafana/README.md b/Labs/15-Prometheus-Grafana/README.md index 7138c32..88acdba 100644 --- a/Labs/15-Prometheus-Grafana/README.md +++ b/Labs/15-Prometheus-Grafana/README.md @@ -7,77 +7,62 @@ ![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) --- +# Prometheus and Grafana Monitoring Lab +- In this lab, we will learn how to set up and configure *`Prometheus` and `Grafana` for monitoring a Kubernetes cluster. +- You will install `Prometheus` to collect metrics from the cluster and `Grafana` to visualize those metrics. +- By the end of this lab, you will have a functional monitoring stack that provides insights into the health and performance of your Kubernetes environment. + +--- -## PreRequirements +## Pre requirements -- [Helm](https://helm.sh/docs/intro/install/) -- K8S cluster -- **kubectl** configured to interact with your cluster. +- [`Helm`](https://helm.sh/docs/intro/install/) installed +- K8S cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/) configured to interact with your cluster ---- [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) - ### **CTRL + click to open in new window** - + --- -# Prometheus and Grafana: Setup and Configuration Guide +## Prometheus and Grafana Setup and Configuration Guide -- This guide walks you through the steps to set up **Prometheus** and **Grafana** on your Kubernetes cluster. -- It includes hands-on steps for installing Prometheus using Helm, configuring Prometheus to collect metrics, setting up Grafana to visualize key metrics, and automating the setup using a bash script. +- This guide serves as a comprehensive walkthrough of the steps to set up `Prometheus` and `Grafana` on your Kubernetes cluster. +- It includes hands-on steps for installing `Prometheus` using `Helm`, configuring `Prometheus` to collect metrics, setting up `Grafana` to visualize key metrics, and automating the setup using a bash script. --- - -## Table of Contents - -- [Introduction to Prometheus and Grafana](#introduction-to-prometheus-and-grafana) - - [`Prometheus`](#prometheus) - - [`Grafana`](#grafana) -- [Part 01 - Installing Prometheus and Grafana](#part-01---installing-prometheus-and-grafana) - - [Step 01: Add Prometheus and Grafana Helm Repositories](#step-01-add-prometheus-and-grafana-helm-repositories) - - [Step 02: Install Prometheus Stack](#step-02-install-prometheus-stack) - - [Step 03: Install Grafana](#step-03-install-grafana) - - [Step 04: Access Grafana](#step-04-access-grafana) -- [Part 02 - Configuring Prometheus](#part-02---configuring-prometheus) - - [Step 01: Verify Prometheus Metrics Collection](#step-01-verify-prometheus-metrics-collection) -- [Part 03 - Configuring Grafana](#part-03---configuring-grafana) - - [Step 01: Add Prometheus as a Data Source in Grafana](#step-01-add-prometheus-as-a-data-source-in-grafana) - - [Step 02: Create a Dashboard to Display Metrics](#step-02-create-a-dashboard-to-display-metrics) - - [Step 03: Get Number of Pods in the Cluster](#step-03-get-number-of-pods-in-the-cluster) - - [Step 04: Customize the Panel](#step-04-customize-the-panel) - ---- - ## Introduction to Prometheus and Grafana ### `Prometheus` -- **Prometheus** is an open-source systems monitoring and alerting toolkit designed for reliability and scalability. +- `Prometheus` is an open-source systems monitoring and alerting toolkit designed for reliability and scalability. - It collects and stores metrics as time-series data, providing powerful querying capabilities. - It is commonly used in Kubernetes environments for monitoring cluster health, application performance, and infrastructure. ### `Grafana` -- **Grafana** is a popular open-source data visualization tool that works well with Prometheus. +- `Grafana` is a popular open-source data visualization tool that works well with `Prometheus`. - It allows you to create dashboards and visualize metrics in real-time, providing insights into system performance and application health. -- Grafana supports a wide range of visualization options, including `graphs`, `heatmaps`, `tables`, and more. -- Together, **Prometheus** and **Grafana** provide a powerful stack for monitoring and alerting in Kubernetes. +- `Grafana` supports a wide range of visualization options, including `graphs`, `heatmaps`, `tables`, and more. +- Together, `Prometheus` and `Grafana` provide a powerful stack for monitoring and alerting in Kubernetes. --- ## Part 01 - Installing Prometheus and Grafana -- To begin, we'll use **Helm** , the package manager for Kubernetes, to deploy Prometheus and Grafana. +!!! warning "Helm Charts" + We will use [Helm](../13-HelmChart/README.md), to deploy Prometheus and Grafana. -### Step 01: Add Prometheus and Grafana Helm Repositories -First, add the official Helm charts for Prometheus and Grafana: +### Step 01 - Add Prometheus and Grafana Helm Repositories + +- Let's add the official `Helm` charts for `Prometheus` and `Grafana`: ```bash @@ -89,9 +74,9 @@ helm repo add grafana https://grafana.github.io/helm-charts helm repo update ``` -### Step 02: Install Prometheus Stack +### Step 02 - Install Prometheus Stack -- `Prometheus` is installed using the `prometheus-stack` Helm chart. +- `Prometheus` is installed using the `prometheus-stack` `Helm` chart. ```bash # Install Prometheus @@ -107,21 +92,22 @@ helm repo update helm status prometheus -n monitoring ``` -### Step 03: Install Grafana +### Step 03 - Install Grafana - - Now, let's install **Grafana** . - - Grafana will be deployed in the same `monitoring` namespace. + - Now, let's install `Grafana`. + - `Grafana` will be deployed in the same `monitoring` namespace. ```bash helm install grafana grafana/grafana --namespace monitoring + # Verify the status of the release using the following: - helm status prometheus -n monitoring + helm status grafana -n monitoring ``` -### Step 04: Access Grafana +### Step 04 - Access Grafana -- Grafana will expose a service in your Kubernetes cluster. -- To access it, you need password & use port forwarding +- `Grafana` will expose a service in your Kubernetes cluster. +- To access it, you need a password and port forwarding. ```bash # In order to get the Grafana admin password, run the following command: @@ -132,7 +118,7 @@ helm repo update # Set the port forwarding so you can access the service using your browsers kubectl port-forward \ --namespace monitoring \ - service/grafana 3000:80 & + service/grafana 3000:80 ``` - Verify that you can access `**Grafana` @@ -141,67 +127,102 @@ helm repo update - **Username** : `admin` - **Password** : (the password you retrieved earlier) + +!!! warning "Accessing Grafana on Google Cloud Shell" + + If you are running your cluster in Google Cloud Shell, you cannot use `localhost` for port forwarding. Instead, use the Cloud Shell Web Preview: + + 1. Run the port-forward command as usual: + ```bash + kubectl port-forward --namespace monitoring service/grafana 3000:80 + ``` + 2. In Google Cloud Shell, click the "Web Preview" button (top right) and select "Preview on port 3000". + 3. Grafana will open in a new browser tab. + - **Username**: `admin` + - **Password**: (the password you retrieved earlier) + + **Note:** You can use any available port (e.g., 3000, 3001) in the port-forward command, just match it in the Web Preview. + + --- ## Part 02 - Configuring Prometheus - `Prometheus` can collect various metrics from your Kubernetes cluster **automatically** if the right **exporters** are enabled. -- The **kube-prometheus-stack** chart that you installed earlier automatically configures Prometheus to scrape a number of Kubernetes components (like `kubelet`, `node-exporter`, and `kube-state-metrics`) for various metrics. +- The **kube-prometheus-stack** chart that you installed earlier automatically configures `Prometheus` to scrape a number of Kubernetes components (like `kubelet`, `node-exporter`, and `kube-state-metrics`) for various metrics. -### Step 01: Verify Prometheus Metrics Collection +### Step 01 - Verify Prometheus Metrics Collection -- You can check if Prometheus is correctly scraping metrics by navigating to Prometheus' web UI. +- You can check if `Prometheus` is correctly scraping metrics by navigating to `Prometheus`' web UI. + + +```bash +# Port-forward the Prometheus service: +kubectl port-forward \ + --namespace monitoring \ + svc/prometheus-operated 9090:9090 +``` - ```bash - # Port-forward the Prometheus service: - kubectl port-forward \ - --namespace monitoring \ - svc/prometheus-operated 9090:9090 - ``` - Verify that you can access `Prometheus` - Open [http://localhost:9090](http://localhost:9090) - - In the expression filed paste the following: - ```bash - # This query will show the current status of the `kube-state-metrics` job. - up{job="kube-state-metrics"} - ``` + - In the expression field paste the following: + + ```bash + # This query will show the current status of the `kube-state-metrics` job + up{job="kube-state-metrics"} + ``` + +!!! warning "Accessing Prometheus on Google Cloud Shell" + + If you are running your cluster in Google Cloud Shell, you cannot use `localhost` for port forwarding. Instead, use the Cloud Shell Web Preview: + + 1. Run the port-forward command as usual: + ```bash + kubectl port-forward --namespace monitoring svc/prometheus-operated 9090:9090 + ``` + 2. In Google Cloud Shell, click the "Web Preview" button (top right) and select "Preview on port 9090". + 3. Prometheus will open in a new browser tab. + + **Note:** You can use any available port (e.g., 9090, 9091) in the port-forward command, just match it in the Web Preview. --- ## Part 03 - Configuring Grafana -- In this part we will set grafana to display Cluster CPU, Memory, and Requests. -- Grafana dashboards can be configured to display **real-time metrics** for CPU, memory, and requests. +- In this part we will set `grafana` to display the Cluster's CPUs, Memory, and Requests. +- `Grafana` dashboards can be configured to display **real-time metrics** for CPU, memory, and requests. - `Prometheus` stores these metrics and `Grafana` will query `Prometheus` to display them. -### Step 01: Add Prometheus as a Data Source in Grafana - - 1. Log into Grafana. [http://localhost:3000](http://localhost:3000) +### Step 01 - Add Prometheus as a Data Source in Grafana + + 1. Log into `Grafana` at: [http://localhost:3000](http://localhost:3000), or use the **Cloud Shell Web Preview**. 2. Click on the hamburger icon on the left sidebar to open the **Configuration** menu. - 3. Click on **Data Sources** . - 4. Click **Add data source** and choose **Prometheus** . - 5. In the **URL** field, enter the Prometheus server URL: `http://prometheus-operated:9090`. - 6. Click **Save & Test** to confirm that the connection is working. + 3. Click on **Data Sources**. + 4. Click **Add data source** and choose **Prometheus**. + 5. In the **URL** field, enter the Prometheus server URL: `http://prometheus-operated:9090`. + 6. Click **Save & Test** to confirm that the connection is working. -### Step 02: Create a Dashboard to Display Metrics +### Step 02 - Create a Dashboard to Display Metrics - - Next step is to create a dashboard and panels to display the desired metrics - - To create a dashboard in Grafana for CPU, memory, and requests do the following: + - Next step is to create a dashboard and panels to display the desired metrics. + - To create a dashboard in `Grafana` for CPU, memory, and requests do the following: - 1. In Grafana, open the left sidebar menu and select **Dashboard**. + 1. In `Grafana`, open the left sidebar menu and select **Dashboard**. 2. Click **Add visualization**. - 3. Choose `Data Source` (we defined it previously) - 4. In the panel editor, click on the `Code` option (right side of the query builder) + 3. Choose `Data Source` (as we defined it previously). + 4. In the panel editor, click on the `Code` option (right side of the query builder). 5. Enter the below queries to visualize metric(s): Note: To add new query click on the `+ Add query` - 6. Save the dashboard + 6. Save the dashboard. +
+ - **CPU Usage** - ```plaintext - sum(rate(container_cpu_usage_seconds_total{namespace="default", container!="", container!="POD"}[5m])) by (pod, namespace) - ``` + ```plaintext + sum(rate(container_cpu_usage_seconds_total{namespace="default", container!="", container!="POD"}[5m])) by (pod, namespace) + ``` - **Memory Usage** : @@ -215,22 +236,27 @@ helm repo update sum(rate(http_requests_total{job="kubelet", cluster="", namespace="default"}[5m])) by (pod, namespace) ``` ---- -### Step 03: Get Number of Pods in the Cluster + +### Step 03 - Get Number of Pods in the Cluster - To track the number of pods running in the cluster, add new panel with the following query: -- This query counts the number of pods running in all the namespace. - ```plaintext - count(kube_pod_info{}) by (namespace) - ``` -- Add another query which will count the number of pods under the namespace `monitoring` - - Tip: We already defined query based upon namespaces before.... - - ```plaintext - count(kube_pod_info{}) by (namespace) - ``` +```sh +# This query counts the number of pods running in all the namespaces +count(kube_pod_info{}) by (namespace) +``` + +- Add another query which will count the number of pods under the namespace `monitoring`: + + + ```sh + count(kube_pod_info{namespace="monitoring"}) by (namespace) + ``` +!!! note "Tip" + We have already defined query based upon namespaces before.... + You can use the same approach to filter by other labels as well. + ### Step 04: Customize the Panel - Change the visualization by changing the **Graph Style** diff --git a/Labs/16-Affinity-Taint-Tolleration/README.md b/Labs/16-Affinity-Taint-Tolleration/README.md index 871c1d6..5606d9b 100644 --- a/Labs/16-Affinity-Taint-Tolleration/README.md +++ b/Labs/16-Affinity-Taint-Tolleration/README.md @@ -1,6 +1,442 @@ ![](../../resources/k8s-logos.png) + # K8S Hands-on + ![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) ---- \ No newline at end of file +--- + +# Node Affinity, Taints, and Tolerations Lab + +- In this lab, we will explore Kubernetes mechanisms for controlling Pod placement on Nodes. +- We will learn how to use `Node Affinity`, `Taints`, and `Tolerations` to schedule Pods on specific Nodes based on labels, constraints, and preferences. +- By the end of this lab, you will understand how to control where Pods run in your cluster and how to reserve Nodes for specific workloads. + +--- + + +## Pre requirements + +- K8S cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/) configured to interact with your cluster + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) + +### **CTRL + click to open in new window** + + +--- + +## Introduction to Pod Scheduling + +Kubernetes provides several mechanisms to control which Nodes your Pods run on: + +### Node Affinity + +- `Node Affinity` allows us to constrain which Nodes our Pods can be scheduled on based on Node labels. +- It's a more expressive and flexible version of `nodeSelector`. +- There are two types: + - **`requiredDuringSchedulingIgnoredDuringExecution`**: Hard requirement - Pod will not be scheduled unless the rule is met. + - **`preferredDuringSchedulingIgnoredDuringExecution`**: Soft preference - Scheduler will try to enforce but will still schedule the Pod if it can't. + +### Taints and Tolerations + +- `Taints` are applied to Nodes and allow a Node to repel a set of Pods. +- `Tolerations` are applied to Pods and allow (but do not require) Pods to schedule onto Nodes with matching `Taints`. +- `Taints` and `Tolerations` work together to ensure that Pods are not scheduled onto inappropriate Nodes. +- Use cases include: + - Dedicating Nodes to specific workloads + - Reserving Nodes with special hardware (GPUs, SSDs) + - Isolating problematic Pods + +--- + +## Part 01 - Node Affinity + +- In this section, we will learn how to use Node `Affinity` to schedule Pods on specific Nodes. + +### Step 01 - Label Your Nodes + +- First, let's label some Nodes to use with Node `Affinity`: + +```bash +# Get list of nodes +kubectl get nodes + +# Label a node with environment=production +kubectl label nodes environment=production + +# Label another node with environment=development +kubectl label nodes environment=development + +# Verify the labels +kubectl get nodes --show-labels +``` + +### Step 02 - Create a Pod with Required Node Affinity + +- Create a Pod that **must** run on a Node with `environment=production`: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: affinity-required-pod +spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: environment + operator: In + values: + - production + containers: + - name: nginx + image: nginx:latest +``` + +- Apply the Pod: + +```bash +# Create the Pod +kubectl apply -f affinity-required-pod.yaml + +# Check which Node the Pod is running on +kubectl get pod affinity-required-pod -o wide + +# Verify it's running on the production Node +kubectl describe pod affinity-required-pod | grep Node: +``` + +### Step 03 - Create a Pod with Preferred Node Affinity + +- Create a Pod that **prefers** to run on a Node with `environment=development`: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: affinity-preferred-pod +spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + preference: + matchExpressions: + - key: environment + operator: In + values: + - development + containers: + - name: nginx + image: nginx:latest +``` + +- Apply the Pod: + +```bash +# Create the Pod +kubectl apply -f affinity-preferred-pod.yaml + +# Check which Node the Pod is running on +kubectl get pod affinity-preferred-pod -o wide + +# This Pod will prefer the development Node but can run elsewhere if needed +``` + +### Step 04 - Experiment with Node Affinity Operators + +- `Node Affinity` supports several operators: + + - **`In`**: Label value is in the list of values + - **`NotIn`**: Label value is not in the list of values + - **`Exists`**: Label key exists (value does not matter) + - **`DoesNotExist`**: Label key does not exist + - **`Gt`**: Label value is greater than the specified value (numeric comparison) + - **`Lt`**: Label value is less than the specified value (numeric comparison) + +- Example with multiple conditions: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: affinity-multiple-conditions +spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: environment + operator: In + values: + - production + - staging + - key: disk-type + operator: Exists + containers: + - name: nginx + image: nginx:latest +``` + +--- + +## Part 02 - Taints and Tolerations + +- In this section, we will learn how to use `Taints` and `Tolerations` to control Pod scheduling. + +### Step 01 - Understanding Taint Effects + +- `Taints` have three effects: + + - **`NoSchedule`**: Pods without matching `tolerations` will not be scheduled on the Node. + - **`PreferNoSchedule`**: Scheduler will try to avoid placing Pods without `tolerations`, but it's not guaranteed. + - **`NoExecute`**: Existing Pods without `tolerations` will be evicted, and new ones won't be scheduled. + +### Step 02 - Apply a Taint to a Node + +- Let's `taint` a Node to dedicate it for special workloads: + +```bash +# Apply a taint to a Node +kubectl taint nodes dedicated=special-workload:NoSchedule + +# Verify the taint +kubectl describe node | grep Taints +``` + +### Step 03 - Create a Pod Without Toleration + +- Let's try to create a Pod without a `toleration`: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: pod-without-toleration +spec: + containers: + - name: nginx + image: nginx:latest +``` + +```bash +# Create the Pod +kubectl apply -f pod-without-toleration.yaml + +# Check the Pod status - it should not be scheduled on the tainted Node +kubectl get pod pod-without-toleration -o wide + +# If all your Nodes are tainted, the Pod will remain Pending +kubectl describe pod pod-without-toleration +``` + +### Step 04 - Create a Pod With Toleration + +- Now let's create a Pod that tolerates the `taint`: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: pod-with-toleration +spec: + tolerations: + - key: "dedicated" + operator: "Equal" + value: "special-workload" + effect: "NoSchedule" + containers: + - name: nginx + image: nginx:latest +``` + +```bash +# Create the Pod +kubectl apply -f pod-with-toleration.yaml + +# This Pod can now be scheduled on the tainted Node +kubectl get pod pod-with-toleration -o wide +``` + +### Step 05 - Understanding Toleration Operators + +- Tolerations support two operators: + + - **`Equal`**: Requires exact match of key, value, and effect + - **`Exists`**: Only checks for key existence (value is ignored) + +- Example with `Exists` operator: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: pod-with-exists-toleration +spec: + tolerations: + - key: "dedicated" + operator: "Exists" + effect: "NoSchedule" + containers: + - name: nginx + image: nginx:latest +``` + +### Step 06 - NoExecute Effect + +- The `NoExecute` effect is special - it evicts running Pods: + +```bash +# Apply a NoExecute taint +kubectl taint nodes maintenance=true:NoExecute + +# Any Pods on this Node without matching toleration will be evicted +kubectl get pods -o wide --watch +``` + +- Let's add a toleration with `tolerationSeconds` to delay eviction: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: pod-with-delayed-eviction +spec: + tolerations: + - key: "maintenance" + operator: "Equal" + value: "true" + effect: "NoExecute" + tolerationSeconds: 300 # Pod will be evicted after 5 minutes + containers: + - name: nginx + image: nginx:latest +``` + +--- + +## Part 03 - Combining Affinity, Taints, and Tolerations + +- We can combine `Node Affinity` with `Taints` and `Tolerations` for fine-grained control. + +### Step 01 - Create a Dedicated Node Pool + +- Let's simulate a dedicated Node pool for GPU workloads: + +```bash +# Label a Node for GPU workload +kubectl label nodes hardware=gpu + +# Taint the Node to prevent non-GPU Pods +kubectl taint nodes nvidia.com/gpu=true:NoSchedule +``` + +### Step 02 - Deploy a GPU Workload + +- Let's create a Pod that requires GPU Nodes: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: gpu-workload +spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: hardware + operator: In + values: + - gpu + tolerations: + - key: "nvidia.com/gpu" + operator: "Equal" + value: "true" + effect: "NoSchedule" + containers: + - name: gpu-app + image: nvidia/cuda:11.0-base + command: ["nvidia-smi"] +``` + +```bash +# Apply the Pod +kubectl apply -f gpu-workload.yaml + +# Verify it's scheduled on the GPU Node +kubectl get pod gpu-workload -o wide +``` + +--- + +## Part 04 - Cleanup and Best Practices + +### Step 01 - Remove Taints + +```bash +# Remove a taint from a Node (add a minus sign at the end) +kubectl taint nodes dedicated=special-workload:NoSchedule- + +# Remove all taints with a specific key +kubectl taint nodes nvidia.com/gpu- +``` + +### Step 02 - Remove Labels + +```bash +# Remove a label from a Node (add a minus sign at the end) +kubectl label nodes environment- +kubectl label nodes hardware- +``` + +### Step 03 - Delete Test Pods + +```bash +# Delete all test Pods +kubectl delete pod affinity-required-pod affinity-preferred-pod +kubectl delete pod pod-with-toleration pod-without-toleration +kubectl delete pod gpu-workload +``` + +--- + +### Best Practices + +1. **Use Node Affinity for preferences**, `Taints/Tolerations` for hard requirements. +2. **Label Nodes consistently** across your cluster (e.g., `node-role`, `hardware-type`, `environment`). +3. **Document your taints** - team members need to know why Nodes are tainted. +4. **Use `PreferNoSchedule`** for soft isolation instead of `NoSchedule` when appropriate. +5. **Combine with Pod Priority** for more sophisticated scheduling strategies. +6. **Test eviction behavior** before using `NoExecute` in production. +7. **Use tolerationSeconds** to gracefully handle Node maintenance. +8. **Monitor unschedulable Pods** - they indicate scheduling constraint conflicts. + +--- + +## Summary + +In this lab, you learned: + +- How to use **Node Affinity** to schedule Pods on specific Nodes based on labels. +- The difference between `required` and `preferred` affinity rules. +- How to use **Taints** to repel Pods from Nodes. +- How to use **Tolerations** to allow Pods on tainted Nodes. +- The three taint effects: `NoSchedule`, `PreferNoSchedule`, and `NoExecute`. +- How to combine affinity, taints, and tolerations for complex scheduling scenarios. +- Best practices for Pod placement in production clusters. + +--- + +## Additional Resources + +- [Kubernetes Node Affinity Documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) +- [Kubernetes Taints and Tolerations Documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) +- [Pod Priority and Preemption](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/) \ No newline at end of file diff --git a/Labs/17-PodDisruptionBudgets-PDB/README.md b/Labs/17-PodDisruptionBudgets-PDB/README.md index 86b636c..90bf067 100644 --- a/Labs/17-PodDisruptionBudgets-PDB/README.md +++ b/Labs/17-PodDisruptionBudgets-PDB/README.md @@ -5,19 +5,29 @@ --- -# PodDisruptionBudgets(PDB) +# Pod Disruption Budgets (PDB) +- In this lab, we will learn about `Pod Disruption Budgets (PDB)` in Kubernetes. +- We will explore how to define and implement PDBs to ensure application availability during voluntary disruptions, such as node maintenance or cluster upgrades. +- By the end of this lab, you will understand how to create and manage Pod Disruption Budgets to maintain the desired level of service availability in your Kubernetes cluster. + +--- + + +## Pre requirements -### Pre-Requirements - K8S cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/) configured to interact with your cluster -[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) -**CTRL + click to open in new window** +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) +### **CTRL + click to open in new window** + + --- ## `PodDisruptionBudgets`: Budgeting the Number of Faults to Tolerate -- A pod disruption budget is an **indicator of the number of disruptions that can be tolerated at a given time for a class of pods** (a budget of faults). +- A `pod disruption budget` is an **indicator of the number of disruptions that can be tolerated at a given time for a class of pods** (a budget of faults). - Disruptions may be caused by **deliberate** or **accidental** Pod deletion. - Whenever a disruption to the pods in a service is calculated to cause the service to **drop below the budget**, the operation is paused until it can maintain the budget. This means that the `drain event` could be temporarily halted while it waits for more pods to become available such that the budget isn’t crossed by evicting the pods. @@ -29,15 +39,20 @@ - `ReplicaSet` - `StatefulSet` -- For this tutorial you should get familier with Kubernetes Eviction Policies since it demonstrates how Pod Disruption Budgets handle evictions. +- For this tutorial you should get familier with [**Kubernetes Eviction Policies**](https://kubernetes.io/docs/concepts/scheduling-eviction/), as it demonstrates how `Pod Disruption Budgets` handle evictions. -- As in the Kubernetes Eviction Policies tutorial we start with eviction-hard="memory.available<480M +- As in the `Kubernetes Eviction Policies` tutorial, we start with +```sh +eviction-hard="memory.available<480M" +``` +--- ### Sample -- In the below sample we will configure a `PodDisruptionBudget` which insure that we will always have **at least** 1 Nginx instance. +- In the below sample we will configure a `Pod Disruption Budget` which insure that we will always have **at least** 1 Nginx instance. + +- First we need an [Nginx Deployment](./resources/Deployment.yaml): -- First we need an [Nginx Deployment](./resources/Deployment.yaml) ```yaml apiVersion: apps/v1 kind: Deployment @@ -48,6 +63,9 @@ metadata: app: nginx # <- We will use this name below ... ``` + +- Now we can create the `Pod Disruption Budget`: + ```yaml apiVersion: policy/v1beta1 kind: PodDisruptionBudget @@ -62,18 +80,15 @@ spec: --- ## Walkthrough + [01. start minikube with Feature Gates](#01-start-minikube-with-feature-gates) + [02. Check Node Pressure(s)](#02-check-node-pressure) --- -### 01. start minikube with Feature Gates -- For more details about Feature Gates read: - https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/#feature-stages - -- For more details about eviction-signals - https://kubernetes.io/docs/tasks/administer-cluster/out-of-resource/#eviction-signals - +### Step 01 - Start minikube with Feature Gates +- Run thwe following command to start minikube with the required `Feature Gates` and `Eviction Signals`: ```sh minikube start \ @@ -82,8 +97,14 @@ minikube start \ --extra-config=kubelet.feature-gates="ExperimentalCriticalPodAnnotation=true" ``` -### 02. Check Node Pressure(s) -- Check to see the Node conditions, if we have any kind of "Presure" +- For more details about `Feature Gates`, read [here](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/#feature-stages). + +- For more details about `eviction-signals`, read [here](https://kubernetes.io/docs/tasks/administer-cluster/out-of-resource/#eviction-signals). + +### Step 02 - Check Node Pressure(s) + +- Check to see the Node conditions, if we have any kind of "Pressure", by running the following: + ```sh kubectl describe node minikube | grep MemoryPressure @@ -105,7 +126,10 @@ Allocated resources: ephemeral-storage 0 (0%) 0 (0%) ``` -### 03. Create 3 Pods using 50 MB each. +### Step 03 - Create 3 Pods using 50 MB each + +- Create a file named `50MB-ram.yaml` with the following content: + ```yaml # ./resources/50MB-ram.yaml ... @@ -123,34 +147,21 @@ resources: memory: "128Mi" cpu: "500m" ``` -- Create the pods + +- Create the pods with the following command: + ```sh kubectl apply -f resources/50MB-ram.yaml ``` -### 04. Check MemoryPressure +### Step 04 - Check Memory Pressure + +- Now let's check the Node conditions again to see if we have `MemoryPressure`: + ```sh $ kubectl describe node minikube | grep MemoryPressure # Output should be similar to MemoryPressure False ... KubeletHasSufficientMemory kubelet has sufficient memory available ``` - - ---- - -
-:arrow_left:  - 16-Affinity-Taint-Tolleration -  ||   18-ArgoCD -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - \ No newline at end of file +- As we can see, we still have `sufficient memory available`. \ No newline at end of file diff --git a/Labs/18-ArgoCD/README.md b/Labs/18-ArgoCD/README.md new file mode 100644 index 0000000..a1d13a4 --- /dev/null +++ b/Labs/18-ArgoCD/README.md @@ -0,0 +1,877 @@ +![](../../resources/k8s-logos.png) + +# K8S Hands-on + +![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) + +--- + +# ArgoCD + +- In this tutorial, we will learn the essentials of `ArgoCD`, a declarative GitOps continuous delivery tool for Kubernetes. +- We will install `ArgoCD`, deploy applications, sync resources from Git repositories, and gain practical experience with GitOps workflows. + +--- + +## Pre Requirements + +- K8S cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/) configured to interact with your cluster +- A `Git repository` (GitHub, GitLab, or Bitbucket) for storing application manifests +- Basic understanding of Kubernetes resources (Deployments, Services, etc.) + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) + +### **CTRL + click to open in new window** + +--- + +## What will we learn? + +- What `ArgoCD` is and why it's useful for GitOps. +- How to install and configure `ArgoCD` on Kubernetes. +- `ArgoCD` core concepts: Applications, Projects, and Sync. +- How to deploy applications from `Git repositories`. +- Application health and sync status monitoring. +- Rollback and sync strategies. +- Best practices for GitOps workflows. + +--- + +### What is ArgoCD? + +- `ArgoCD` is a declarative, GitOps continuous delivery tool for Kubernetes. +- It follows the `GitOps` pattern where Git repositories are the source of truth for defining the desired application state. +- `ArgoCD` automates the deployment of the desired application states in the specified target environments. + +### Why ArgoCD? + +- **GitOps Workflow**: Uses Git as the single source of truth. +- **Automated Deployment**: Automatically syncs your Kubernetes cluster with Git repositories. +- **Application Health Monitoring**: Continuous monitoring of deployed applications. +- **Rollback Capabilities**: Easy rollback to previous versions. +- **Multi-Cluster Support**: Manage applications across multiple clusters. +- **SSO Integration**: Supports various SSO providers for authentication. +- **RBAC**: Fine-grained access control. +- **Audit Trail**: Full audit trail of all operations. + +### Terminology + +* **Application** + - An `ArgoCD` **Application** is a Kubernetes resource object representing a deployed application instance in an environment. + - It defines the source repository, target cluster, and sync policies. + +* **Project** + - An `ArgoCD` **Project** provides a logical grouping of applications. + - Projects are useful for organizing applications and implementing RBAC. + +* **Sync** + - **Sync** is the process of making a live cluster state match the desired state defined in Git. + - Sync can run manually or automatically. + +* **Sync Status** + - Indicates whether the live state matches the Git state. + - Status can be: **Synced**, **OutOfSync**, or **Unknown**. + +* **Health Status** + - Indicates the health of the application resources. + - Status can be: **Healthy**, **Progressing**, **Degraded**, **Suspended**, or **Missing**. + +### ArgoCD Architecture + +| Component | Description | +| ------------------------ | ---------------------------------------------------------------------------------------------- | +| **API Server** | Exposes the API consumed by Web UI, CLI, and CI/CD systems | +| **Repository Server** | Maintains a local cache of Git repositories holding application manifests | +| **Application Controller** | Monitors running applications and compares the current live state against the desired state | +| **Dex** | Identity service for integrating with external identity providers | +| **Redis** | Used for caching | + +### Common ArgoCD CLI Commands + +| Command | Description | +| ----------------------------------------------------- | -------------------------------------------------------------------- | +| `argocd login ` | Login to `ArgoCD` server | +| `argocd app create ` | Create a new application | +| `argocd app list` | List all applications | +| `argocd app get ` | Get application details | +| `argocd app sync ` | Sync (deploy) an application | +| `argocd app delete ` | Delete an application | +| `argocd app set ` | Update application parameters | +| `argocd app diff ` | Show differences between Git and live state | +| `argocd app history ` | Show application deployment history | +| `argocd app rollback ` | Rollback to a previous revision | + +--- + +# Lab + +## Part 01 - Installing ArgoCD + +### Step 01 - Create an ArgoCD Namespace + +- Create a dedicated namespace for `ArgoCD`: + +```bash +kubectl create namespace argocd +``` + +### Step 02 - Install ArgoCD + +- Install `ArgoCD` using the official installation manifest: + +```bash +kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml +``` + +### Step 03 - Verify Installation + +- Check that all `ArgoCD` pods are running: + +```bash +kubectl get pods -n argocd +``` + +- Expected output should show all pods in **Running** status: + +```plaintext +NAME READY STATUS RESTARTS AGE +argocd-application-controller-0 1/1 Running 0 2m +argocd-dex-server-xxx 1/1 Running 0 2m +argocd-redis-xxx 1/1 Running 0 2m +argocd-repo-server-xxx 1/1 Running 0 2m +argocd-server-xxx 1/1 Running 0 2m +``` + +### Step 04 - Expose ArgoCD Server + +- By default, the `ArgoCD` API server is not exposed externally. +- We' will use port-forwarding to access it, by running: + +```bash +kubectl port-forward svc/argocd-server -n argocd 8080:443 +``` + +- Alternatively, you can change the service type to **LoadBalancer** or create an **Ingress**: + +```bash +# Change to LoadBalancer (for cloud environments) +kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}' + +# Or use NodePort (for local/development) +kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort"}}' +``` + +### Step 05 - Get Initial Admin Password + +- The initial password for the `admin` user is auto-generated and stored as a secret by running the following command: + +```bash +# Get the initial admin password +kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo +``` + +!!! note + Save this password - you'll need it to login to the ArgoCD UI later. + +### Step 06 - Access ArgoCD UI + +- Open your browser and navigate to: + - **Port-forward**: `https://localhost:8080` + - **LoadBalancer**: Use the external IP + - **NodePort**: Use `http://:` + +- Login with: + - **Username**: `admin` + - **Password**: (from Step 05) + +--- + +## Part 02 - Installing ArgoCD CLI + +### Step 01 - Download and Install ArgoCD CLI + +- Install the `ArgoCD CLI` based on your operating system: + +**Linux:** +```bash +curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 +sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd +rm argocd-linux-amd64 +``` + +**macOS:** +```bash +brew install argocd +``` + +**Windows (via Chocolatey):** +```bash +choco install argocd-cli +``` + +### Step 02 - Verify CLI Installation + +```bash +argocd version --short +``` + +### Step 03 - Login via CLI + +```bash +# If using port-forward +argocd login localhost:8080 --insecure + +# You'll be prompted for username and password +``` + +### Step 04 - Change Admin Password (optional, but highly recommended) + +```bash +argocd account update-password +``` + +--- + +## Part 03 - Deploying Your First Application + +### Step 01 - Prepare a Git Repository + +- For this lab, we will use a sample Git repository with Kubernetes manifests. +- You can use the `ArgoCD` example repository or your own: + +```bash +# Sample repository URL +https://github.com/argoproj/argocd-example-apps.git +``` + +### Step 02 - Create an Application via CLI + +- Create an `ArgoCD` application that deploys the guestbook app: + +```bash +argocd app create guestbook \ + --repo https://github.com/argoproj/argocd-example-apps.git \ + --path guestbook \ + --dest-server https://kubernetes.default.svc \ + --dest-namespace default +``` + +??? info "Command Explanation" + - `--repo`: The Git repository URL + - `--path`: Path within the repository where manifests are located + - `--dest-server`: Target Kubernetes cluster (default is the cluster where ArgoCD is installed) + - `--dest-namespace`: Target namespace for deployment + +### Step 03 - View Application Status + +```bash +# List all applications +argocd app list + +# Get detailed information about the application +argocd app get guestbook +``` + +### Step 04 - Sync the Application + +- Initially, the application status will be **OutOfSync**. +- Sync it to deploy by running: + +```bash +argocd app sync guestbook +``` + +### Step 05 - Verify Deployment + +```bash +# Check the deployed resources +kubectl get all -n default + +# You should see the guestbook deployment, service, and pods +``` + +### Step 06 - Access the Application + +```bash +# Port-forward to access the guestbook service +kubectl port-forward svc/guestbook-ui -n default 8081:80 + +# Open browser to http://localhost:8081 +``` + +--- + +## Part 04 - Creating Application via UI + +### Step 01 - Access ArgoCD UI + +- Navigate to the `ArgoCD` UI at `https://localhost:8080` + +### Step 02 - Create a New Application + +1. Click on **"+ NEW APP"** button. +2. Fill in the following details: + - **Application Name**: `helm-guestbook` + - **Project**: `default` + - **Sync Policy**: `Manual` (or `Automatic` for auto-sync) + - **Repository URL**: `https://github.com/argoproj/argocd-example-apps.git` + - **Revision**: `HEAD` + - **Path**: `helm-guestbook` + - **Cluster URL**: `https://kubernetes.default.svc` + - **Namespace**: `default` + +3. Click **"CREATE"**. + +### Step 03 - View Application in UI + +- You should now be able to see the `helm-guestbook` application tile in the UI. +- Click on it to see the application topology. + +### Step 04 - Sync via UI + +- Click the **"SYNC"** button. +- Select the resources you want to sync (or select all). +- Click **"SYNCHRONIZE"**. + +### Step 05 - Monitor Sync Progress + +- Watch the sync progress in real-time. +- The UI will show each resource being created/updated. +- Once completed, all resources should show as **Healthy** and **Synced**. + +--- + +## Part 05 - Application Management + +### Step 01 - View Application Details + +```bash +# Get full application details +argocd app get guestbook + +# View application parameters +argocd app get guestbook --show-params +``` + +### Step 02 - View Sync History + +```bash +# View deployment history +argocd app history guestbook +``` + +### Step 03 - View Differences + +```bash +# Show differences between Git and live state +argocd app diff guestbook +``` + +### Step 04 - Manual Sync with Options + +```bash +# Sync with prune (removes resources not in Git) +argocd app sync guestbook --prune + +# Sync specific resources +argocd app sync guestbook --resource Deployment:guestbook-ui + +# Dry-run sync +argocd app sync guestbook --dry-run +``` + +--- + +## Part 06 - Auto-Sync and Self-Healing + +### Step 01 - Enable Auto-Sync + +- Enable automatic synchronization so `ArgoCD` automatically deploys changes from Git: + +```bash +argocd app set guestbook --sync-policy automated +``` + +### Step 02 - Enable Self-Healing + +- Enable self-healing to automatically fix out-of-sync resources: + +```bash +argocd app set guestbook --self-heal +``` + +### Step 03 - Enable Auto-Prune + +- Enable auto-prune to automatically delete resources removed from Git: + +```bash +argocd app set guestbook --auto-prune +``` + +### Step 04 - Test Auto-Sync + +1. Make a change to your Git repository (e.g., change replica count). +2. Commit and push the change. +3. Watch as `ArgoCD` automatically detects and syncs the change: + +```bash +# Watch the application sync status +watch argocd app get guestbook +``` + +### Step 05 - Test Self-Healing + +1. Manually modify a deployed resource by running: + +```bash +# Manually scale the deployment +kubectl scale deployment guestbook-ui --replicas=5 +``` + +2. Watch as `ArgoCD` detects the drift and automatically restores the desired state: + +```bash +# ArgoCD will revert the replica count to what's in Git +argocd app get guestbook +``` + +--- + +## Part 07 - Rollback + +### Step 01 - View Application History + +```bash +# View all deployment revisions +argocd app history guestbook +``` + +Example output: +```plaintext +ID DATE REVISION +0 2025-11-10 10:15:30 abc123 (HEAD) +1 2025-11-10 10:20:45 def456 +2 2025-11-10 10:25:30 ghi789 +``` + +### Step 02 - Rollback to Previous Revision + +```bash +# Rollback to revision ID 1 +argocd app rollback guestbook 1 +``` + +### Step 03 - Verify Rollback + +```bash +# Verify the application state +argocd app get guestbook + +# Check deployed resources +kubectl get all -n default +``` + +--- + +## Part 08 - Working with Helm Charts + +### Step 01 - Create Helm-Based Application + +```bash +argocd app create nginx-helm \ + --repo https://charts.bitnami.com/bitnami \ + --helm-chart nginx \ + --revision 15.1.0 \ + --dest-server https://kubernetes.default.svc \ + --dest-namespace default +``` + +### Step 02 - Set Helm Values + +```bash +# Set Helm values +argocd app set nginx-helm \ + --helm-set service.type=NodePort \ + --helm-set replicaCount=3 +``` + +### Step 03 - Sync Helm Application + +```bash +argocd app sync nginx-helm +``` + +### Step 04 - View Helm Parameters + +```bash +# View all Helm parameters +argocd app get nginx-helm --show-params +``` + +--- + +## Part 09 - Working with Kustomize + +### Step 01 - Create Kustomize-Based Application + +```bash +argocd app create kustomize-guestbook \ + --repo https://github.com/argoproj/argocd-example-apps.git \ + --path kustomize-guestbook \ + --dest-server https://kubernetes.default.svc \ + --dest-namespace default +``` + +### Step 02 - Sync Kustomize Application + +```bash +argocd app sync kustomize-guestbook +``` + +### Step 03 - Verify Deployment + +```bash +kubectl get all -n default -l app=kustomize-guestbook +``` + +--- + +## Part 10 - Projects and RBAC + +### Step 01 - Create a New Project + +```bash +argocd proj create my-project \ + --description "My demo project" \ + --src https://github.com/argoproj/argocd-example-apps.git \ + --dest https://kubernetes.default.svc,default \ + --dest https://kubernetes.default.svc,my-namespace +``` + +### Step 02 - List Projects + +```bash +argocd proj list +``` + +### Step 03 - View Project Details + +```bash +argocd proj get my-project +``` + +### Step 04 - Create Application in Project + +```bash +argocd app create my-app \ + --project my-project \ + --repo https://github.com/argoproj/argocd-example-apps.git \ + --path guestbook \ + --dest-server https://kubernetes.default.svc \ + --dest-namespace default +``` + +--- + +## Part 11 - Multi-Source Applications + +### Step 01 - Create Multi-Source Application + +- `ArgoCD` supports applications with multiple source repositories: + +```yaml +# Save as multi-source-app.yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: multi-source-app + namespace: argocd +spec: + project: default + sources: + - repoURL: https://github.com/argoproj/argocd-example-apps.git + path: guestbook + targetRevision: HEAD + - repoURL: https://github.com/another-repo/configs.git + path: overlays/prod + targetRevision: HEAD + destination: + server: https://kubernetes.default.svc + namespace: default + syncPolicy: + automated: + prune: true + selfHeal: true +``` + +### Step 02 - Apply Multi-Source Application + +```bash +kubectl apply -f multi-source-app.yaml +``` + +--- + +## Part 12 - Sync Windows and Waves + +### Step 01 - Configure Sync Windows + +- Sync windows allow you to define time periods when syncs are allowed or denied: + +```bash +# Add a sync window to allow syncs only during business hours +argocd proj windows add my-project \ + --kind allow \ + --schedule "0 9 * * 1-5" \ + --duration 8h \ + --applications "*" +``` + +### Step 02 - Configure Sync Waves + +- Use annotations to control the order of resource synchronization: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: database + annotations: + argocd.argoproj.io/sync-wave: "0" # Deploy first +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + annotations: + argocd.argoproj.io/sync-wave: "1" # Deploy after database +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + annotations: + argocd.argoproj.io/sync-wave: "2" # Deploy last +``` + +--- + +## Part 13 - Health Checks and Hooks + +### Step 01 - Custom Health Checks + +- Define custom health checks for your resources: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-cm + namespace: argocd +data: + resource.customizations: | + cert-manager.io/Certificate: + health.lua: | + hs = {} + if obj.status ~= nil then + if obj.status.conditions ~= nil then + for i, condition in ipairs(obj.status.conditions) do + if condition.type == "Ready" and condition.status == "False" then + hs.status = "Degraded" + hs.message = condition.message + return hs + end + if condition.type == "Ready" and condition.status == "True" then + hs.status = "Healthy" + hs.message = condition.message + return hs + end + end + end + end + hs.status = "Progressing" + hs.message = "Waiting for certificate" + return hs +``` + +### Step 02 - Sync Hooks + +- Use hooks to execute actions during sync: + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: database-migration + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: HookSucceeded +spec: + template: + spec: + containers: + - name: migration + image: myapp/migration:latest + command: ["./run-migrations.sh"] + restartPolicy: Never +``` + +--- + +## Finalize & Cleanup + +### Clean Up Applications + +```bash +# Delete all applications +argocd app delete guestbook --cascade +argocd app delete helm-guestbook --cascade +argocd app delete nginx-helm --cascade +argocd app delete kustomize-guestbook --cascade + +# Or delete via kubectl +kubectl delete applications -n argocd --all +``` + +### Uninstall ArgoCD + +```bash +# Delete ArgoCD installation +kubectl delete -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml + +# Delete the namespace +kubectl delete namespace argocd +``` + +--- + +## Troubleshooting + +### ArgoCD Server Not Accessible + +- Check if the `ArgoCD` server pod is running: + +```bash +kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server +``` + +- Check the service: + +```bash +kubectl get svc -n argocd argocd-server +``` + +### Application Stuck in Progressing State + +- Check application details: + +```bash +argocd app get +kubectl describe application -n argocd +``` + +- Check pod logs: + +```bash +kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller +``` + +### Sync Fails with Permission Errors + +- Verify RBAC settings: + +```bash +argocd proj get +``` + +- Check if the destination namespace exists: + +```bash +kubectl get namespace +``` + +### Out of Sync Status + +- View the differences: + +```bash +argocd app diff +``` + +- Force sync: + +```bash +argocd app sync --force +``` + +### Repository Connection Issues + +- Test repository connectivity: + +```bash +argocd repo add --username --password +argocd repo list +``` + +--- + +## Best Practices + +### GitOps Workflow + +1. **Single Source of Truth**: Keep all Kubernetes manifests in Git. +2. **Branch Strategy**: Use branches for different environments (dev, staging, prod). +3. **Pull Requests**: Use PRs for all changes with proper reviews. +4. **Automated Testing**: Validate manifests before merging. +5. **Rollback Strategy**: Use Git revert for rollbacks. + +### Application Organization + +1. **Use Projects**: Organize applications by team or environment. +2. **Naming Conventions**: Use clear, consistent naming. +3. **Sync Policies**: Choose appropriate sync policies per environment. +4. **Resource Limits**: Set proper resource limits in manifests. +5. **Health Checks**: Define custom health checks for complex resources. + +### Security + +1. **RBAC**: Implement fine-grained access control. +2. **SSO Integration**: Use SSO for authentication. +3. **Secret Management**: Use sealed-secrets or external secret managers. +4. **Network Policies**: Restrict `ArgoCD` network access. +5. **Audit Logging**: Enable and monitor audit logs. + +### Monitoring + +1. **Notifications**: Configure notifications for sync failures. +2. **Metrics**: Monitor `ArgoCD` metrics with `Prometheus`. +3. **Dashboards**: Create Grafana dashboards for visibility. +4. **Alerts**: Set up alerts for critical failures. +5. **Regular Reviews**: Periodically review application health. + +--- + +## Next Steps + +- Explore **ApplicationSets** for managing multiple applications. +- Integrate `ArgoCD` with **CI/CD pipelines**. +- Set up **notifications** using Slack, email, or webhooks. +- Implement **Progressive Delivery** with Argo Rollouts. +- Configure **SSO** integration with your identity provider. +- Set up **multi-cluster** management. +- Explore **ArgoCD Image Updater** for automated image updates. +- Read the [official ArgoCD documentation](https://argo-cd.readthedocs.io/) +- Join the [ArgoCD community](https://github.com/argoproj/argo-cd) + +--- + +## Additional Resources + +- [ArgoCD Official Documentation](https://argo-cd.readthedocs.io/) +- [ArgoCD GitHub Repository](https://github.com/argoproj/argo-cd) +- [ArgoCD Best Practices](https://argo-cd.readthedocs.io/en/stable/user-guide/best_practices/) +- [GitOps Principles](https://www.gitops.tech/) +- [Argo Rollouts (Progressive Delivery)](https://argoproj.github.io/argo-rollouts/) +- [ArgoCD ApplicationSet](https://argo-cd.readthedocs.io/en/stable/user-guide/application-set/) + diff --git a/Labs/19-CustomScheduler/README.md b/Labs/19-CustomScheduler/README.md index 298313c..1a476c4 100644 --- a/Labs/19-CustomScheduler/README.md +++ b/Labs/19-CustomScheduler/README.md @@ -7,37 +7,55 @@ # Writing custom Scheduler -### Pre-Requirements +- `Scheduling` is the process of selecting a node for a pod to run on. +- In this lab we will write our own pods `scheduler`. +- It is probably not something that you will ever need to do, but still it's a good practice to understand how scheduling works in K8S and how you can extend it. + + +--- + + +## Pre Requirements + - K8S cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/) configured to interact with your cluster +- A `Git repository` (GitHub, GitLab, or Bitbucket) for storing application manifests +- Basic understanding of Kubernetes resources (Deployments, Services, etc.) + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) -[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) -**CTRL + click to open in new window** +### **CTRL + click to open in new window** + --- -# Custom Scheduler -- Official docs: [Scheduler Configuration](https://kubernetes.io/docs/reference/scheduling/config) -- In this lab we will write our own pods scheduler. -- It not something that you will even need to do, but still its a good practice +## Custom Scheduler + +- See further information in the official documentation: [Scheduler Configuration](https://kubernetes.io/docs/reference/scheduling/config) - To schedule a given pod using a specific scheduler, specify the name of the scheduler in that specification `.spec.schedulerName`. + ## A bit about scheduler -- Scheduling happens in a series of **stages* that are exposed through extension points. + +- Scheduling happens in a series of **stages** that are exposed through extension points. - We can define several scheduling Profile. A scheduling Profile allows you to configure the different stages of scheduling in the `kube-scheduler` +--- + ## Sample `KubeSchedulerConfiguration` + ```yaml ### # Sample KubeSchedulerConfiguration ### # -# You can configure `kube-scheduler` to run more than one profile. -# Each profile has an associated scheduler name and can have a different -# set of plugins configured in its extension points. +# You can configure `kube-scheduler` to run more than one profile. +# Each profile has an associated scheduler name and can have a different +# set of plugins configured in its extension points. # With the following sample configuration, # the scheduler will run with two profiles: # - default plugins -# - all scoring plugins disabled. +# - all scoring plugins disabled apiVersion: kubescheduler.config.k8s.io/v1beta1 kind: KubeSchedulerConfiguration @@ -52,7 +70,9 @@ profiles: disabled: - name: '*' ``` -- Once you have your scheduler code you can use it in your pod scheduler + +- Once you have your scheduler code, you can use it in your pod scheduler: + ```yaml # In this sample we use deployment but it will apply to any pod ... @@ -69,8 +89,10 @@ spec: ``` -### Sample bash scheduler. +### Sample bash scheduler + - The "trick" is loop over all the waiting pods and search for the custom scheduler match in `spec.schedulerName` + ```sh ... @@ -95,23 +117,3 @@ spec: fi ... ``` - - - ---- - -
-:arrow_left:  - 18-ArgoCD -  ||   20-CronJob -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - \ No newline at end of file diff --git a/Labs/20-CronJob/README.md b/Labs/20-CronJob/README.md index a4ed853..2fc1498 100644 --- a/Labs/20-CronJob/README.md +++ b/Labs/20-CronJob/README.md @@ -5,14 +5,121 @@ --- -# Writing custom Scheduler + +# CronJobs + +- In this lab, we will learn how to create and manage `CronJobs` in Kubernetes. +- A `CronJob` creates `Jobs` on a time-based schedule. It is useful for running periodic and recurring tasks, such as backups or report generation. + +--- ### Pre-Requirements - K8S cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/) configured to interact with your cluster [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) **CTRL + click to open in new window** --- -# CronJobs +### What is a CronJob? +- A `CronJob` in Kubernetes runs Jobs on a time-based schedule, similar to Linux cron. +- Useful for periodic tasks like backups, reports, or cleanup. + +--- + +### Step - 01: Create a CronJob YAML +- Create a file named `hello-cronjob.yaml` with the following content: + +```yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello + namespace: default +spec: + schedule: "*/1 * * * *" # Every 1 minute + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: busybox + args: + - /bin/sh + - -c + - date; echo Hello from the Kubernetes CronJob! + restartPolicy: OnFailure +``` + + + +### Step - 02: Apply the CronJob + +```sh +kubectl apply -f hello-cronjob.yaml +``` + +### Step - 03: Verify CronJob Creation + +```sh +kubectl get cronjob hello +``` + + +### Step - 04: Check CronJob and Jobs + +- List CronJobs: + +```sh +kubectl get cronjobs +``` + +- List Jobs created by the CronJob: + +```sh +kubectl get jobs +``` + +- List Pods created by Jobs: + +```sh +kubectl get pods +``` + + + +### Step - 05: View Job Output + +- Get the name of a pod created by the CronJob, then view its logs: + +```sh +kubectl logs +``` + +Example output: + +``` +Mon Nov 10 12:00:00 UTC 2025 +Hello from the Kubernetes CronJob! +``` + + +### Step - 06: Clean Up + +- Delete the CronJob and its Jobs: + +```sh +kubectl delete cronjob hello +kubectl delete jobs --all +``` + +--- + +### Questions: + +- What happens if the job takes longer than the schedule interval? +- How would you change the schedule to run every 5 minutes? +- How can you limit the number of successful or failed jobs to keep? + diff --git a/Labs/21-KubeAPI/README.md b/Labs/21-KubeAPI/README.md index ebfb22c..149f440 100644 --- a/Labs/21-KubeAPI/README.md +++ b/Labs/21-KubeAPI/README.md @@ -4,86 +4,93 @@ ![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) -### Verify pre-requirements +--- +# Kube API Access from Pod -- **`kubectl`** - short for Kubernetes Controller - is the CLI for Kubernetes cluster and is required in order to be able to run the labs. -- In order to install `kubectl` and if required creating a local cluster, please refer to [Kubernetes - Install Tools](https://kubernetes.io/docs/tasks/tools/) - +- In this lab, we will learn how to access the Kubernetes API from within a Pod. +- We will create a simple Pod that runs a script to query the Kubernetes API server and retrieve information about the cluster. --- -## Lab Highlights: - - [01. Build the docker image](#01-Build-the-docker-image) - - [01.01. The script which will be used for query K8S API](#0101-The-script-which-will-be-used-for-query-K8S-API) - - [01.02. Build the docker image](#0102-Build-the-docker-image) - - [02. Deploy the Pod to K8S](#02-Deploy-the-Pod-to-K8S) - - [02.01. Run kustomization to deploy](#0201-Run-kustomization-to-deploy) - - [02.02. Query the K8S API](#0202-Query-the-K8S-API) + +### Pre-Requirements +- K8S cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/) configured to interact with your cluster + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) +**CTRL + click to open in new window** --- - -### 01. Build the docker image +### Part 01 - Build the docker image + +- In order to demonstrate the API query we will build a custom docker image. +- It is optional to use the pre-build image and skip this step. + +### Step 01 - The script which will be used for query K8S API + +- In order to be able to access K8S API from within a pod, we will be using the following script: -- In order to demonstrate the APi query we will build a custom docker image. -- You can use the pre-build image and skip this step -### 01.01. The script which will be used for query K8S API +```sh +# `api_query.sh` + +#!/bin/sh + +################################# +## Access the internal K8S API ## +################################# +# Point to the internal API server hostname +API_SERVER_URL=https://kubernetes.default.svc -- In order to be able to access K8S api from within a pod we will be using the following script: -- `api_query.sh` +# Path to ServiceAccount token +# The service account is mapped by the K8S Api server in the pods +SERVICE_ACCOUNT_FOLDER=/var/run/secrets/kubernetes.io/serviceaccount - ```sh - #!/bin/sh +# Read this Pod's namespace if required +# NAMESPACE=$(cat ${SERVICE_ACCOUNT_FOLDER}/namespace) - ################################# - ## Access the internal K8S API ## - ################################# - # Point to the internal API server hostname - API_SERVER_URL=https://kubernetes.default.svc +# Read the ServiceAccount bearer token +TOKEN=$(cat ${SERVICE_ACCOUNT_FOLDER}/token) + +# Reference the internal certificate authority (CA) +CACERT=${SERVICE_ACCOUNT_FOLDER}/ca.crt + +# Explore the API with TOKEN and the Certificate +curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${API_SERVER_URL}/api +``` - # Path to ServiceAccount token - # The service account is mapped by the K8S Api server in the pods - SERVICE_ACCOUNT_FOLDER=/var/run/secrets/kubernetes.io/serviceaccount +### Step 02 - Build the docker image - # Read this Pod's namespace if required - # NAMESPACE=$(cat ${SERVICE_ACCOUNT_FOLDER}/namespace) +- For the pod image we will use the following Dockerfile: - # Read the ServiceAccount bearer token - TOKEN=$(cat ${SERVICE_ACCOUNT_FOLDER}/token) - # Reference the internal certificate authority (CA) - CACERT=${SERVICE_ACCOUNT_FOLDER}/ca.crt - # Explore the API with TOKEN and the Certificate - curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${API_SERVER_URL}/api - ``` +```Dockerfile -### 01.02. Build the docker image +# `Dockerfile` -- For the pod image we will use the following Dockerfile -- `Dockerfile` +FROM alpine - ```Dockerfile - FROM alpine +# Update and install dependencies +RUN apk add --update nodejs npm curl - # Update and install dependencies - RUN apk add --update nodejs npm curl +# Copy the endpoint script +COPY api_query.sh . - # Copy the endpoint script - COPY api_query.sh . +# Set the execution bit +RUN chmod +x api_query.sh . +``` - # Set the execution bit - RUN chmod +x api_query.sh . - ``` +--- -### 02. Deploy the Pod to K8S +### Part 02 - Deploy the Pod to K8S -- Once the image is ready we can deploy the image as pod to the cluster -- The required resources are under the k8s folder +- Once the image is ready, we can deploy it as a pod to the cluster. +- The required resources are under the k8s folder. -### 02.01. Run kustomization to deploy +### Step 01 - Run kustomization to deploy - Deploy to the cluster @@ -95,9 +102,9 @@ kubectl kustomize k8s | kubectl delete -f - kubectl kustomize k8s | kubectl apply -f - ``` -### 02.02. Query the K8S API +### Step 02 - Query the K8S API -- Run the following script to verify that the connection to the API is working +- Run the following script to verify that the connection to the API is working: ```sh # Get the deployment pod name @@ -106,23 +113,3 @@ POD_NAME=$(kubectl get pod -A -l app=monitor-app -o jsonpath="{.items[0].metadat # Print out the logs to verify that the pods is connected to the API kubectl exec -it -n codewizard $POD_NAME sh ./api_query.sh ``` - - - ---- - -
-:arrow_left:  - 21-Auditing -  ||   22-Rancher -   :arrow_right:
- ---- - -
- ©CodeWizard LTD -
- -![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) - - \ No newline at end of file diff --git a/Labs/24-HelmOperator/README.md b/Labs/24-HelmOperator/README.md index 7ed2569..df08e3c 100644 --- a/Labs/24-HelmOperator/README.md +++ b/Labs/24-HelmOperator/README.md @@ -1,17 +1,35 @@ -## Helm Operator Tutorial +## ![](../../resources/k8s-logos.png) + +# K8S Hands-on + +![Visitor Badge](https://visitor-badge.laobi.icu/badge?page_id=nirgeier) + +--- + + + +## Helm Operator - An in-depth Helm-based operator tutorial. +- The `Helm Operator` is a Kubernetes operator, allowing one to declaratively manage Helm chart releases. + +--- + - > The Helm Operator is a Kubernetes operator, allowing one to declaratively manage Helm chart releases. +### Pre-Requirements +- K8S cluster - Setting up minikube cluster instruction +- [**kubectl**](https://kubernetes.io/docs/tasks/tools/) configured to interact with your cluster -### Prerequisites +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/nirgeier/KubernetesLabs) +**CTRL + click to open in new window** - Docker - kubectl -- operator-sdk [brew install operator-sdk] -- `cluster-admin` permissions. +- operator-sdk installed and configured +- `cluster-admin` permissions -### Prerequisites - Install `operator-sdk` + +### Install `operator-sdk` ```sh # Grab the ARCH and OS @@ -28,7 +46,9 @@ curl -LO ${OPERATOR_SDK_DL_URL}/operator-sdk_${OS}_${ARCH} chmod +x operator-sdk_${OS}_${ARCH} && sudo mv operator-sdk_${OS}_${ARCH} /usr/local/bin/operator-sdk ``` -### Step 01: Create a new project +--- + +### Step 01 - Create a new project - Use the CLI to create a new Helm-based nginx-operator project: @@ -124,14 +144,17 @@ operator-sdk \ ### Step 02 - Customize the operator logic - For this example the nginx-operator will execute the following reconciliation logic for each Nginx Custom Resource (CR): - - Create an nginx Deployment if it doesn’t exist - - Create an nginx Service if it doesn’t exist - - Create an nginx Ingress if it is enabled and doesn’t exist - - Ensure that the Deployment, Service, and optional Ingress match the desired configuration (e.g. replica count, image, service type, etc) as specified by the Nginx CR + - Create an nginx Deployment, if it doesn’t exist. + - Create an nginx Service, if it doesn’t exist. + - Create an nginx Ingress, if it is enabled and doesn’t exist. + - Update the Deployment, Service, and Ingress, if they already exist but don’t match the desired configuration as specified by the Nginx CR. + - Ensure that the Deployment, Service, and optional Ingress all match the desired configuration (e.g. replica count, image, service type, etc) as specified by the Nginx CR. + +
#### Watch the Nginx CR -- By default, the nginx-operator watches Nginx resource events as shown in `watches.yaml` and executes Helm releases using the specified chart: +- By default, the Nginx-operator watches Nginx resource events as shown in `watches.yaml` and executes Helm releases using the specified chart: ```yaml # Use the 'create api' subcommand to add watches to this file. @@ -141,22 +164,26 @@ operator-sdk \ chart: helm-charts/nginx ``` +
+ ### Reviewing the Nginx Helm Chart - When a Helm operator project is created, the SDK creates an example Helm chart that contains a set of templates for a simple Nginx release. -- For this example, we have templates for deployment, service, and ingress resources, along with a NOTES.txt template, which Helm chart developers use to convey helpful information about a release. +- For this example, we have templates for deployment, service, and ingress resources, along with a `NOTES.txt` template, which Helm chart developers use to convey helpful information about a release. + +
### Understanding the Nginx CR spec -- Helm uses a concept called values to provide customizations to a Helm chart’s defaults, which are defined in the Helm chart’s values.yaml file. +- Helm uses a concept called `values` to provide customizations to a Helm chart’s defaults, which are defined in the Helm chart’s `values.yaml` file. - Overriding these defaults is as simple as setting the desired values in the CR spec. -- Let’s use the number of replicas as an example. +- Let’s use the number of replicas value as an example. -- First, inspecting `helm-charts/nginx/values.yaml`, we see that the chart has a value called `replicaCount` and it is set to `1` by default. +- First, inspecting `helm-charts/nginx/values.yaml`, we can see that the chart has a value called `replicaCount` and it is set to `1` by default. -- Lets update the value to 3 `replicaCount: 3`. +- Let’s update the value to 3 - `replicaCount: 3`. ```yaml # Update `config/samples/demo_v1alpha1_nginx.yaml` to look like the following: @@ -169,19 +196,20 @@ operator-sdk \ replicaCount: 3 # <------- Adding our replicas count ``` -- Similarly, we see that the default service port is set to `80`, but we would like to use `8888`, so we’ll again update config/samples/demo_v1alpha1_nginx.yaml by adding the service port override. +- Similarly, we see that the default service port is set to `80`, but we would like to use `8888`, so we will again update config/samples/demo_v1alpha1_nginx.yaml by adding the service port override. - ```yaml - # Update `config/samples/demo_v1alpha1_nginx.yaml` to look like the following: - apiVersion: demo.codewizard.co.il/v1alpha1 - kind: Nginx - metadata: - name: nginx-sample - spec: - #... (Around line 36) - service: - port: 8888 # <------- Updating our service port - ``` + +```yaml +# Update `config/samples/demo_v1alpha1_nginx.yaml` to look like the following: +apiVersion: demo.codewizard.co.il/v1alpha1 +kind: Nginx +metadata: + name: nginx-sample +spec: + #... (Around line 36) + service: + port: 8888 # <------- Updating our service port +``` ### Step 03 - Build the operator’s image @@ -198,10 +226,11 @@ IMG ?= controller:latest IMG ?= nirgeier/helm_operator:latest ``` -- Now lets build and push the image - ```sh - make docker-build docker-push - ``` +- Now let's build and push the image: + +```sh +make docker-build docker-push +``` ### Step 04 - Deploy the operator to the cluster @@ -212,7 +241,6 @@ make deploy kubectl get deployment -n nginx-operator-system ``` ---- ### Step 05 - Create the custom Nginx @@ -247,14 +275,14 @@ replicaCount: 5 38 # type: ClusterIP ``` -- Apply the changes +- Apply the changes: ```sh # Apply the changes kubectl apply -f config/samples/demo_v1alpha1_nginx.yaml ``` -- Check to see that the operator is working as expected +- Check to see that the operator is working as expected: ```sh # Ensure that the nginx-operator still running @@ -272,14 +300,14 @@ kubectl get svc | grep nginx-sample ### Step07 - Logging / Debugging -- We can view the operator logs using the following command: +- We can view the operator's logs using the following command: ```sh # View the operator logs kubectl logs deployment.apps/nginx-operator-controller-manager -n nginx-operator-system -c manager ``` -- Review the CR status and events +- Review the CR status and events: ```sh kubectl describe nginxes.demo.codewizard.co.il diff --git a/Labs/index.md b/Labs/index.md new file mode 100644 index 0000000..8987674 --- /dev/null +++ b/Labs/index.md @@ -0,0 +1,120 @@ +# Kubernetes Labs + +## 📋 Lab Overview + +Welcome to the hands-on Kubernetes labs! This comprehensive series of labs will guide you through essential Kubernetes concepts and advanced topics. + +## 🗂️ Available Labs + +### Foundation Labs + +| Lab | Topic | Description | +|-----|-------|-------------| +| [00](00-VerifyCluster/README.md) | Verify Cluster | Ensure your Kubernetes cluster is properly configured | +| [01](01-Namespace/README.md) | Namespace | Learn to organize resources with namespaces | +| [02](02-Deployments-Imperative/README.md) | Deployments (Imperative) | Create deployments using kubectl commands | +| [03](03-Deployments-Declarative/README.md) | Deployments (Declarative) | Create deployments using YAML manifests | +| [04](04-Rollout/README.md) | Rollout | Manage deployment updates and rollbacks | +| [05](05-Services/README.md) | Services | Expose applications with Kubernetes services | + +### Storage & StatefulSets + +| Lab | Topic | Description | +|-----|-------|-------------| +| [06](06-DataStore/README.md) | DataStore | Work with persistent storage in Kubernetes | +| [09](09-StatefulSet/README.md) | StatefulSet | Deploy stateful applications | +| [12](12-Wordpress-MySQL-PVC/README.md) | WordPress MySQL PVC | Complete stateful application with persistent storage | + +### Networking & Ingress + +| Lab | Topic | Description | +|-----|-------|-------------| +| [07](07-nginx-Ingress/README.md) | Nginx Ingress | Configure ingress controllers for external access | +| [10](10-Istio/README.md) | Istio | Implement service mesh for microservices | + +### Configuration Management + +| Lab | Topic | Description | +|-----|-------|-------------| +| [08](08-Kustomization/README.md) | Kustomization | Manage configurations with Kustomize | +| [13](13-HelmChart/README.md) | Helm Chart | Package and deploy applications with Helm | + +### GitOps & CI/CD + +| Lab | Topic | Description | +|-----|-------|-------------| +| [18](18-ArgoCD/README.md) | ArgoCD | Implement GitOps with ArgoCD | + +### Observability + +| Lab | Topic | Description | +|-----|-------|-------------| +| [14](14-Logging/README.md) | Logging | Centralized logging with Fluentd | +| [15](15-Prometheus-Grafana/README.md) | Prometheus & Grafana | Monitoring and visualization | +| [23](23-MetricServer/README.md) | Metric Server | Resource metrics collection | + +### Advanced Topics + +| Lab | Topic | Description | +|-----|-------|-------------| +| [11](11-CRD-Custom-Resource-Definition/README.md) | Custom Resource Definition | Extend Kubernetes API with CRDs | +| [16](16-Affinity-Taint-Tolleration/README.md) | Affinity, Taint & Toleration | Control pod scheduling | +| [17](17-PodDisruptionBudgets-PDB/README.md) | Pod Disruption Budgets | Ensure availability during disruptions | +| [19](19-CustomScheduler/README.md) | Custom Scheduler | Build custom scheduling logic | +| [20](20-CronJob/README.md) | CronJob | Schedule recurring tasks | +| [21](21-Auditing/README.md) | Auditing | Track cluster activities | +| [21](21-KubeAPI/README.md) | Kube API | Work with Kubernetes API | +| [24](24-HelmOperator/README.md) | Helm Operator | Manage Helm releases with operators | +| [25](25-kubebuilder/README.md) | Kubebuilder | Build Kubernetes operators | + +### Tools & Utilities + +| Lab | Topic | Description | +|-----|-------|-------------| +| [22](22-Rancher/README.md) | Rancher | Multi-cluster management platform | +| [26](26-k9s/README.md) | k9s | Terminal-based Kubernetes UI | +| [27](27-krew/README.md) | Krew | kubectl plugin manager | +| [28](28-kubeapps/README.md) | Kubeapps | Application dashboard for Kubernetes | +| [29](29-kubeadm/README.md) | Kubeadm | Bootstrap Kubernetes clusters | +| [30](30-k9s/README.md) | k9s (Advanced) | Advanced k9s usage | + +## 🎯 Learning Path + +### Beginner Track +Start here if you're new to Kubernetes:
+1. Lab 00: Verify Cluster
+2. Lab 01: Namespace
+3. Lab 02: Deployments (Imperative)
+4. Lab 03: Deployments (Declarative)
+5. Lab 05: Services
+ +### Intermediate Track +For those with basic Kubernetes knowledge:
+1. Lab 04: Rollout
+2. Lab 06: DataStore
+3. Lab 07: Nginx Ingress
+4. Lab 08: Kustomization
+5. Lab 13: Helm Chart
+ +### Advanced Track +For experienced Kubernetes users:
+1. Lab 10: Istio
+2. Lab 11: Custom Resource Definition
+3. Lab 18: ArgoCD
+4. Lab 19: Custom Scheduler
+5. Lab 25: Kubebuilder
+ +## 💡 Tips for Success + +- **Take your time**: Don't rush through the labs +- **Practice regularly**: Repetition builds muscle memory +- **Experiment**: Try variations of the examples +- **Read the docs**: Kubernetes documentation is excellent +- **Join the community**: Engage with other learners + +## 🚀 Get Started + +Ready to begin? Click on any lab on the left menu, or start with [Lab 00: Verify Cluster](00-VerifyCluster/README.md)! + + + diff --git a/Labs/welcome.md b/Labs/welcome.md new file mode 100644 index 0000000..7cc6809 --- /dev/null +++ b/Labs/welcome.md @@ -0,0 +1,76 @@ +# Welcome to Kubernetes Labs + +![Kubernetes Logo](/Users/orni/MKdocs/KubernetesLabs/KubernetesLabs/resources/Kubernetes-Logo.wine.png) +![Kubernetes Logo](mkdocs/overrides/assets/images/k8s-logos.png) + +Welcome to the **Kubernetes Labs** repository! This is a comprehensive collection of hands-on labs designed to help you learn and master Kubernetes concepts, from basic deployments to advanced topics like Istio, ArgoCD and custom schedulers. + +## 📚 What You'll Learn + +This lab series covers a wide range of `Kubernetes` topics: + +- **Basics**: Namespaces, Deployments, Services and Rollouts +- **Storage**: DataStores, Persistent Volume Claims and StatefulSets +- **Networking**: Ingress Controllers and Service Mesh (Istio) +- **Configuration Management**: Kustomization and Helm Charts +- **GitOps**: ArgoCD for continuous deployment +- **Observability**: Logging, Prometheus and Grafana +- **Advanced Topics**: Custom Resource Definitions (CRDs), Custom Schedulers and Pod Disruption Budgets +- **Tools**: k9s, Krew, Kubeapps, Kubeadm and Rancher + +## 🎯 Who Is This For? + +These labs are designed for: + +- **Beginners** wanting to get started with Kubernetes +- **Intermediate users** looking to deepen their knowledge +- **Advanced practitioners** seeking to master complex Kubernetes patterns +- **DevOps Engineers** preparing for Kubernetes certifications (CKA, CKAD, CKS) + +## 🛠️ Prerequisites + +Before starting these labs, you should have: + +- Basic understanding of containerization (Docker) +- Command-line (CLI) familiarity +- A Kubernetes cluster (Minikube, Kind, or cloud-based cluster) +- kubectl installed and configured + +## 📖 How to Use This Repository + +1. **Start with the Basics**: Begin with Lab 00 (Verify Cluster) to ensure your environment is set up correctly +2. **Progress Sequentially**: The labs are numbered in a logical progression +3. **Hands-On Practice**: Each lab includes practical exercises and examples +4. **Experiment**: Don't be afraid to modify the examples and see what happens + +## 🎓 Lab Structure + +Each lab includes: + +- **Objectives**: What you'll learn in the lab +- **Step-by-step instructions**: Detailed walkthrough of concepts +- **Example YAML files**: Ready-to-use Kubernetes manifests +- **Verification steps**: How to confirm everything is working +- **Challenges**: Optional exercises to test your understanding + +## 🚦 Getting Started + +Ready to begin? Head over to the [Labs](index.md) section and start with: + +1. [00 Verify Cluster](00-VerifyCluster/README.md) - Verify your Kubernetes cluster is ready +2. [01 Namespace](01-Namespace/README.md) - Learn about Kubernetes namespaces +3. [02 Deployments Imperative](02-Deployments-Imperative/README.md) - Create deployments using kubectl + +## 🤝 Contributing + +Contributions are welcome! If you find issues or have suggestions for improvements, please feel free to open an issue or submit a pull request. + +## 📬 Contact + +For questions or feedback, please reach out through the social links located above the header of this page. + +--- + +**Happy Learning! 🎉** + +Let's dive into the world of Kubernetes together! diff --git a/codewizard-helm-demo/.helmignore b/codewizard-helm-demo/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/codewizard-helm-demo/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/codewizard-helm-demo/Chart.yaml b/codewizard-helm-demo/Chart.yaml new file mode 100644 index 0000000..cae3fe3 --- /dev/null +++ b/codewizard-helm-demo/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: codewizard-helm-demo +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/codewizard-helm-demo/templates/NOTES.txt b/codewizard-helm-demo/templates/NOTES.txt new file mode 100644 index 0000000..1dc190d --- /dev/null +++ b/codewizard-helm-demo/templates/NOTES.txt @@ -0,0 +1,35 @@ +1. Get the application URL by running these commands: +{{- if .Values.httpRoute.enabled }} +{{- if .Values.httpRoute.hostnames }} + export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }} +{{- else }} + export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}") + {{- end }} +{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }} + echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application" + + NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules. + The rules can be set for path, method, header and query parameters. + You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml' +{{- end }} +{{- else if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "codewizard-helm-demo.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "codewizard-helm-demo.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "codewizard-helm-demo.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "codewizard-helm-demo.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/codewizard-helm-demo/templates/_helpers.tpl b/codewizard-helm-demo/templates/_helpers.tpl new file mode 100644 index 0000000..4b4fabb --- /dev/null +++ b/codewizard-helm-demo/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "codewizard-helm-demo.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "codewizard-helm-demo.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "codewizard-helm-demo.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "codewizard-helm-demo.labels" -}} +helm.sh/chart: {{ include "codewizard-helm-demo.chart" . }} +{{ include "codewizard-helm-demo.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "codewizard-helm-demo.selectorLabels" -}} +app.kubernetes.io/name: {{ include "codewizard-helm-demo.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "codewizard-helm-demo.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "codewizard-helm-demo.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/codewizard-helm-demo/templates/deployment.yaml b/codewizard-helm-demo/templates/deployment.yaml new file mode 100644 index 0000000..bb59fbe --- /dev/null +++ b/codewizard-helm-demo/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "codewizard-helm-demo.fullname" . }} + labels: + {{- include "codewizard-helm-demo.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "codewizard-helm-demo.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "codewizard-helm-demo.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "codewizard-helm-demo.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/codewizard-helm-demo/templates/hpa.yaml b/codewizard-helm-demo/templates/hpa.yaml new file mode 100644 index 0000000..fe5e47b --- /dev/null +++ b/codewizard-helm-demo/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "codewizard-helm-demo.fullname" . }} + labels: + {{- include "codewizard-helm-demo.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "codewizard-helm-demo.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/codewizard-helm-demo/templates/httproute.yaml b/codewizard-helm-demo/templates/httproute.yaml new file mode 100644 index 0000000..f304111 --- /dev/null +++ b/codewizard-helm-demo/templates/httproute.yaml @@ -0,0 +1,38 @@ +{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include "codewizard-helm-demo.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include "codewizard-helm-demo.labels" . | nindent 4 }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + weight: 1 + {{- end }} +{{- end }} diff --git a/codewizard-helm-demo/templates/ingress.yaml b/codewizard-helm-demo/templates/ingress.yaml new file mode 100644 index 0000000..93d795e --- /dev/null +++ b/codewizard-helm-demo/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "codewizard-helm-demo.fullname" . }} + labels: + {{- include "codewizard-helm-demo.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "codewizard-helm-demo.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/codewizard-helm-demo/templates/service.yaml b/codewizard-helm-demo/templates/service.yaml new file mode 100644 index 0000000..93e94da --- /dev/null +++ b/codewizard-helm-demo/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "codewizard-helm-demo.fullname" . }} + labels: + {{- include "codewizard-helm-demo.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "codewizard-helm-demo.selectorLabels" . | nindent 4 }} diff --git a/codewizard-helm-demo/templates/serviceaccount.yaml b/codewizard-helm-demo/templates/serviceaccount.yaml new file mode 100644 index 0000000..7b99bcc --- /dev/null +++ b/codewizard-helm-demo/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "codewizard-helm-demo.serviceAccountName" . }} + labels: + {{- include "codewizard-helm-demo.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/codewizard-helm-demo/templates/tests/test-connection.yaml b/codewizard-helm-demo/templates/tests/test-connection.yaml new file mode 100644 index 0000000..d3863ec --- /dev/null +++ b/codewizard-helm-demo/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "codewizard-helm-demo.fullname" . }}-test-connection" + labels: + {{- include "codewizard-helm-demo.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "codewizard-helm-demo.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/codewizard-helm-demo/values.yaml b/codewizard-helm-demo/values.yaml new file mode 100644 index 0000000..bc4eead --- /dev/null +++ b/codewizard-helm-demo/values.yaml @@ -0,0 +1,161 @@ +# Default values for codewizard-helm-demo. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: nginx + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# -- Expose the service via gateway-api HTTPRoute +# Requires Gateway API resources and suitable controller installed within the cluster +# (see: https://gateway-api.sigs.k8s.io/guides/) +httpRoute: + # HTTPRoute enabled. + enabled: false + # HTTPRoute annotations. + annotations: {} + # Which Gateways this Route is attached to. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + # Hostnames matching HTTP header. + hostnames: + - chart-example.local + # List of rules and filters applied. + rules: + - matches: + - path: + type: PathPrefix + value: /headers + # filters: + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: My-Overwrite-Header + # value: this-is-the-only-value + # remove: + # - User-Agent + # - matches: + # - path: + # type: PathPrefix + # value: /echo + # headers: + # - name: version + # value: v2 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/mkdocs-site/00-VerifyCluster/index.html b/mkdocs-site/00-VerifyCluster/index.html new file mode 100644 index 0000000..f6705af --- /dev/null +++ b/mkdocs-site/00-VerifyCluster/index.html @@ -0,0 +1,126 @@ + 00 Verify Cluster - KubernetesLabs

K8S Hands-on

Visitor Badge

Verify pre-requirements

  • kubectl - short for Kubernetes Controller - is the CLI for Kubernetes cluster and is required in order to be able to run the labs.
  • In order to install kubectl and if required creating a local cluster, please refer to Kubernetes - Install Tools

01. Installing minikube

  • If you don’t have an existing cluster you can use google cloud for the labs hands-on
  • Click on the button below to be able to run the labs on Google Shell
    [Use: CTRL + click to open in new window]
    Open in Cloud Shell

  • Run the following script in the opened terminal

# Download minikube
+curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
+
+# Install minikube
+sudo install minikube-linux-amd64 /usr/local/bin/minikube
+

02. Start minikube

minikube start
+
  • You should see an output like this:
* minikube v1.16.0 on Debian 10.7
+  - MINIKUBE_FORCE_SYSTEMD=true
+  - MINIKUBE_HOME=/google/minikube
+  - MINIKUBE_WANTUPDATENOTIFICATION=false
+* Automatically selected the docker driver
+* Starting control plane node minikube in cluster minikube
+* Pulling base image ...
+* Downloading Kubernetes v1.20.0 preload ...
+    > preloaded-images-k8s-v8-v1....: 491.00 MiB / 491.00 MiB  100.00% 86.82 Mi
+* Creating docker container (CPUs=2, Memory=4000MB) ...
+* Preparing Kubernetes v1.20.0 on Docker 20.10.0 ...
+  - Generating certificates and keys ...
+  - Booting up control plane ...
+  - Configuring RBAC rules ...
+* Verifying Kubernetes components...
+* Enabled addons: default-storageclass, storage-provisioner
+* Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
+

03. Check the minikube status

minikube status
+
  • You should see output similar to this one:
minikube
+type: Control Plane
+host: Running
+kubelet: Running
+apiserver: Running
+kubeconfig: Configured
+timeToStop: Nonexistent
+

04. Verify that the cluster is up and running

$ kubectl cluster-info
+
+Kubernetes control plane is running at https://192.168.49.2:8443
+KubeDNS is running at https://192.168.49.2:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
+
  • Verify that kubectl is installed and configured
kubectl config view
+
  • You should get something like the following
    apiVersion: v1
    +clusters:
    +  - cluster:
    +      certificate-authority: /google/minikube/.minikube/ca.crt
    +      server: https://192.168.49.2:8443
    +    name: minikube
    +contexts:
    +  - context:
    +      cluster: minikube
    +      namespace: default
    +      user: minikube
    +    name: minikube
    +current-context: minikube
    +kind: Config
    +preferences: {}
    +users:
    +  - name: minikube
    +    user:
    +      client-certificate: /google/minikube/.minikube/profiles/minikube/client.crt
    +      client-key: /google/minikube/.minikube/profiles/minikube/client.key
    +

05. Verify that you can “talk” to your cluster

# In this sample we have minikube pod running
+$ kubectl get nodes
+NAME       STATUS   ROLES                  AGE    VERSION
+minikube   Ready    control-plane,master   3m9s   v1.20.0
+
\ No newline at end of file diff --git a/mkdocs-site/01-Namespace/index.html b/mkdocs-site/01-Namespace/index.html new file mode 100644 index 0000000..374c955 --- /dev/null +++ b/mkdocs-site/01-Namespace/index.html @@ -0,0 +1,82 @@ + 01 Namespace - KubernetesLabs

K8S Hands-on

Visitor Badge


Namespaces

  • Kubernetes supports multiple virtual clusters backed by the same physical cluster.
  • These virtual clusters are called namespaces.
  • Namespaces are the default way for Kubernetes to separate resources.
  • Using namespaces we can isolate the development, improve security and much more.
  • Kubernetes clusters has a builtin namespace called default and might contain more namespaces, like kube-system, for example.

Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window


01. Create Namespace

# In this sample `codewizard` is the desired namespace
+$ kubectl create namespace codewizard
+namespace/codewizard created
+
+### !!! Try to create the following namespace (with _ & -), and see what happens:
+$ kubectl create namespace my_namespace-
+

02. Setting the default Namespace for kubectl

  • To set the default namespace run:
$ kubectl config set-context $(kubectl config current-context) --namespace=codewizard
+
+Context minikube modified.
+

03. Verify that you’ve updated the namespace

$ kubectl config get-contexts
+CURRENT     NAME                 CLUSTER          AUTHINFO         NAMESPACE
+            docker-desktop       docker-desktop   docker-desktop
+            docker-for-desktop   docker-desktop   docker-desktop
+*           minikube             minikube         minikube         codewizard
+

0.4 Using the -n Flag:

  • When using kubectl you can pass the -n flag in order to execute the kubectl command on a desired namespace.
  • For example:
# get resources of a specific workspace
+$ kubectl get pods -n <namespace>
+
\ No newline at end of file diff --git a/mkdocs-site/02-Deployments-Imperative/index.html b/mkdocs-site/02-Deployments-Imperative/index.html new file mode 100644 index 0000000..a13a6e4 --- /dev/null +++ b/mkdocs-site/02-Deployments-Imperative/index.html @@ -0,0 +1,114 @@ + 02 Deployments Imperative - KubernetesLabs

K8S Hands-on

Visitor Badge


Deployment - Imperative

Creating deployments using kubectl create

  • We start with creating the following deployment praqma/network-multitool
  • This is a multitool for container/network testing and troubleshooting.

Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window


01. Create Namespace

  • As completed in the previous lab, create the desired namespace [codewizard]:
$ kubectl create namespace codewizard
+namespace/codewizard created
+

02. Deploy Multitool Image

# Deploy the first container
+$ kubectl create deployment multitool -n codewizard --image=praqma/network-multitool
+deployment.apps/multitool created
+
  • kubectl create deployment actually creating a replica set for us.
  • We can verify it by running:
$ kubectl get all -n codewizard
+
+## Expected output:
+NAME                                    READY    UP-TO-DATE  AVAILABLE
+deployment.apps/multitool               1/1      1           1
+
+NAME                                    DESIRED  CURRENT     READY
+replicaset.apps/multitool-7885b5f94f    1        1           1
+
+NAME                                    READY    STATUS      RESTARTS
+pod/multitool-7885b5f94f-9s7xh          1/1      Running     0
+

03. Test the Deployment

  • The above deployment contains a container named, multitool.
  • In order for us to be able to access this multitool container, we need to create a resource of type Service which will “open” the server for incoming traffic.

Create a service using kubectl expose

# "Expose" the desired port for incoming traffic
+# This command is equivalent to declare a `kind: Service` im YAML file
+
+$ kubectl expose deployment -n codewizard multitool --port 80 --type NodePort
+service/multitool exposed
+
  • Verify that the service have been created by running:

$ kubectl get service -n codewizard
+
+# The output should be something like
+NAME                TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
+service/multitool   NodePort   10.102.73.248   <none>        80:31418/TCP   3s
+

Find the port & the IP which was assigned to our pod by the cluster.

  • Grab the port from the previous output.
  • Port: In the above sample its 31418 [80:31418/TCP]
  • IP: we will need to grab the cluster ip using kubectl cluster-info
# get the IP
+$ kubectl cluster-info
+
+# You should get output similar to this one
+Kubernetes control plane is running at https://192.168.49.2:8443
+KubeDNS is running at https://192.168.49.2:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
+
+# Programmatically get the port and the IP
+CLUSTER_IP=$(kubectl get nodes \
+            --selector=node-role.kubernetes.io/control-plane \
+            -o jsonpath='{$.items[*].status.addresses[?(@.type=="InternalIP")].address}')
+
+NODE_PORT=$(kubectl get -o \
+            jsonpath="{.spec.ports[0].nodePort}" \
+            services multitool -n codewizard)
+
  • In this sample the cluster-ip is 192.168.49.2


Test the deployment

  • Test to see if the deployment worked using the ip address and port number we have retrieved above.
  • Execute curl with the following parameters: http://${CLUSTER_IP}:${NODE_PORT}

curl http://${CLUSTER_IP}:${NODE_PORT}
+
+# Or in the above sample
+curl 192.168.49.2:30436
+
+# The output should be similar to this:
+Praqma Network MultiTool (with NGINX) ...
+
- If you get the above output, congratulations! You have successfully created a deployment using imperative commands.

\ No newline at end of file diff --git a/mkdocs-site/03-Deployments-Declarative/index.html b/mkdocs-site/03-Deployments-Declarative/index.html new file mode 100644 index 0000000..634a25c --- /dev/null +++ b/mkdocs-site/03-Deployments-Declarative/index.html @@ -0,0 +1,150 @@ + 03 Deployments Declarative - KubernetesLabs

K8S Hands-on

Visitor Badge


Deployment - Declarative

Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window


01. Create Namespace

  • As completed in the previous lab, create the desired namespace [codewizard]:
$ kubectl create namespace codewizard
+namespace/codewizard created
+

02. Deploy nginx using yaml file (declarative)

  • Let’s create the YAML file for the deployment.
  • If this is your first k8s YAML file, its advisable that you type it in order to get the feeling of the structure.
  • Save the file with the following name: nginx.yaml
apiVersion: apps/v1
+kind: Deployment # We use a deployment and not pod !!!!
+metadata:
+  name: nginx # Deployment name
+  namespace: codewizard
+  labels:
+    app: nginx # Deployment label
+spec:
+  replicas: 2
+  selector:
+    matchLabels: # Labels for the replica selector
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx # Labels for the replica selector
+        version: "1.17" # Specify specific verion if required
+    spec:
+      containers:
+        - name: nginx # The name of the pod
+          image: nginx:1.17 # The image which we will deploy
+          ports:
+            - containerPort: 80
+
  • Create the deployment using the -f flag & --record=true
$ kubectl apply -n codewizard -f nginx.yaml --record=true
+deployment.extensions/nginx created
+

03. Verify that the deployment has been created:

$ kubectl get deployments -n codewizard
+NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE
+multitool   1         1         1            1
+nginx       1         1         1            1
+

04. Check if the pods are running:

$ kubectl get pods -n codewizard
+NAME                         READY   STATUS    RESTARTS
+multitool-7885b5f94f-9s7xh   1/1     Running   0
+nginx-647fb5956d-v8d2w       1/1     Running   0
+

05. Playing with K8S replicas

  • Let’s play with the replica and see K8S in action.
  • Open a second terminal and execute:
$ kubectl get pods -n codewizard --watch
+

05. Update the nginx.yaml file with replica’s value of 5:

spec:
+  replicas: 5
+

06. Update the deployment using kubectl apply

$ kubectl apply -n codewizard -f nginx.yaml --record=true
+deployment.apps/nginx configured
+

  • Switch to the second terminal and you should see something like the following:
$ kubectl get pods --watch -n codewizard
+NAME                         READY   STATUS    RESTARTS   AGE
+multitool-74477484b8-dj7th   1/1     Running   0          20m
+nginx-dc8bb9b45-hqdv9        1/1     Running   0          111s
+nginx-dc8bb9b45-vdmp5        0/1     Pending   0          0s
+nginx-dc8bb9b45-28wwq        0/1     Pending   0          0s
+nginx-dc8bb9b45-wkc68        0/1     Pending   0          0s
+nginx-dc8bb9b45-vdmp5        0/1     Pending   0          0s
+nginx-dc8bb9b45-28wwq        0/1     Pending   0          0s
+nginx-dc8bb9b45-x7j4g        0/1     Pending   0          0s
+nginx-dc8bb9b45-wkc68        0/1     Pending   0          0s
+nginx-dc8bb9b45-x7j4g        0/1     Pending   0          0s
+nginx-dc8bb9b45-vdmp5        0/1     ContainerCreating   0          0s
+nginx-dc8bb9b45-28wwq        0/1     ContainerCreating   0          0s
+nginx-dc8bb9b45-wkc68        0/1     ContainerCreating   0          0s
+nginx-dc8bb9b45-x7j4g        0/1     ContainerCreating   0          0s
+nginx-dc8bb9b45-vdmp5        1/1     Running             0          2s
+nginx-dc8bb9b45-28wwq        1/1     Running             0          3s
+nginx-dc8bb9b45-x7j4g        1/1     Running             0          3s
+nginx-dc8bb9b45-wkc68        1/1     Running             0          3s
+


- Can you explain what do you see?

Why are there more containers than requested?


07. Scaling down with kubectl scale

  • Scaling down using kubectl, and not by editing the YAML file:
kubectl scale -n codewizard --replicas=1 deployment/nginx
+
  • Switch to the second terminal. The current output should show something like this:
NAME                         READY   STATUS    RESTARTS   AGE
+multitool-74477484b8-dj7th   1/1     Running   0          29m
+nginx-dc8bb9b45-28wwq        1/1     Running   0          4m41s
+nginx-dc8bb9b45-hqdv9        1/1     Running   0          10m
+nginx-dc8bb9b45-vdmp5        1/1     Running   0          4m41s
+nginx-dc8bb9b45-wkc68        1/1     Running   0          4m41s
+nginx-dc8bb9b45-x7j4g        1/1     Running   0          4m41s
+nginx-dc8bb9b45-x7j4g        1/1     Terminating   0          6m21s
+nginx-dc8bb9b45-vdmp5        1/1     Terminating   0          6m21s
+nginx-dc8bb9b45-28wwq        1/1     Terminating   0          6m21s
+nginx-dc8bb9b45-wkc68        1/1     Terminating   0          6m21s
+nginx-dc8bb9b45-x7j4g        0/1     Terminating   0          6m22s
+nginx-dc8bb9b45-vdmp5        0/1     Terminating   0          6m22s
+nginx-dc8bb9b45-wkc68        0/1     Terminating   0          6m22s
+nginx-dc8bb9b45-28wwq        0/1     Terminating   0          6m22s
+nginx-dc8bb9b45-28wwq        0/1     Terminating   0          6m26s
+nginx-dc8bb9b45-28wwq        0/1     Terminating   0          6m26s
+nginx-dc8bb9b45-vdmp5        0/1     Terminating   0          6m26s
+nginx-dc8bb9b45-vdmp5        0/1     Terminating   0          6m26s
+nginx-dc8bb9b45-wkc68        0/1     Terminating   0          6m27s
+nginx-dc8bb9b45-wkc68        0/1     Terminating   0          6m27s
+nginx-dc8bb9b45-x7j4g        0/1     Terminating   0          6m27s
+nginx-dc8bb9b45-x7j4g        0/1     Terminating   0          6m27s
+
\ No newline at end of file diff --git a/mkdocs-site/03-Deployments-Declarative/nginx.yaml b/mkdocs-site/03-Deployments-Declarative/nginx.yaml new file mode 100644 index 0000000..360e728 --- /dev/null +++ b/mkdocs-site/03-Deployments-Declarative/nginx.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment # We use a deployment and not pod !!!! +metadata: + name: nginx # Deployment name + namespace: codewizard + labels: + app: nginx # Deployment label +spec: + replicas: 1 + selector: + matchLabels: # Labels for the replica selector + app: nginx + template: + metadata: + labels: + app: nginx # Labels for the replica selector + version: "1.17" # Specify specific verion if required + spec: + containers: + - name: nginx # The name of the pod + image: nginx:1.17 # The image which we will deploy + ports: + - containerPort: 80 diff --git a/mkdocs-site/04-Rollout/index.html b/mkdocs-site/04-Rollout/index.html new file mode 100644 index 0000000..7020ee1 --- /dev/null +++ b/mkdocs-site/04-Rollout/index.html @@ -0,0 +1,168 @@ + 04 Rollout - KubernetesLabs

K8S Hands-on

Visitor Badge


Rollout (Rolling Update)

  • In this step we will deploy the same application with several different versions and we will “switch” between them.
  • For learning purposes we will play a little with the CLI.

Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window


01. Create namespace

  • As completed in the previous lab, create the desired namespace [codewizard]:
$ kubectl create namespace codewizard
+namespace/codewizard created
+

02. Create the desired deployment

  • We will use the save-config flag

    save-config
    If true, the configuration of current object will be saved in its annotation.
    Otherwise, the annotation will be unchanged.
    This flag is useful when you want to perform kubectl apply on this object in the future.

  • Let’s run the following:

    $ kubectl create deployment -n codewizard nginx --image=nginx:1.17 --save-config
    +
    Note that in case we already have this deployed, we will get an error message.


03. Expose nginx as a service

$ kubectl expose deployment -n codewizard nginx --port 80 --type NodePort
+service/nginx exposed
+
Again, note that in case we already have this service we will get an error message as well.


04. Verify that the pods and the service are running

$ kubectl get all -n codewizard
+
+# The output should be similar to this
+NAME                        READY      STATUS    RESTARTS   AGE
+pod/nginx-db749865c-lmgtv   1/1        Running   0          66s
+
+NAME                        TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
+service/nginx               NodePort   10.102.79.9   <none>        80:31204/TCP   30s
+
+NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
+deployment.apps/nginx   1/1     1            1           66s
+
+NAME                              DESIRED   CURRENT   READY   AGE
+replicaset.apps/nginx-db749865c   1         1         1       66s
+

05. Change the number of replicas to 3

$ kubectl scale deployment -n codewizard nginx --replicas=3
+deployment.apps/nginx scaled
+

06. Verify that now we have 3 replicas

$ kubectl get pods -n codewizard
+NAME                    READY   STATUS    RESTARTS   AGE
+nginx-db749865c-f5mkt   1/1     Running   0          86s
+nginx-db749865c-jgcvb   1/1     Running   0          86s
+nginx-db749865c-lmgtv   1/1     Running   0          4m44s
+

07. Test the deployment

# !!! Get the Ip & port for this service
+$ kubectl get services -n codewizard -o wide 
+
+# Write down the port number
+NAME    TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE    SELECTOR
+nginx   NodePort   10.102.79.9   <none>        80:31204/TCP   7m7s   app=nginx
+
+# Get the cluster IP and port
+$ kubectl cluster-info  
+Kubernetes control plane is running at https://192.168.49.2:8443
+
+# Using the above <host>:<port> test the nginx
+# -I is for getting the headers
+$ curl -sI <host>:<port>
+
+# The response should display the nginx version
+example: curl -sI 192.168.49.2:31204
+
+HTTP/1.1 200 OK
+Server: nginx/1.17.10 <------------ This is the pod version
+Date: Fri, 15 Jan 2021 20:13:48 GMT
+Content-Type: text/html
+Content-Length: 612
+Last-Modified: Tue, 14 Apr 2020 14:19:26 GMT
+Connection: keep-alive
+ETag: "5e95c66e-264"
+Accept-Ranges: bytes
+...
+

08. Deploy another version of nginx

# Deploy another version of nginx (1.16)
+$ kubectl set image deployment -n codewizard nginx nginx=nginx:1.16 --record
+deployment.apps/nginx image updated
+
+# Check to verify that the new version deployed - same as in previous step
+$ curl -sI <host>:<port>
+
+# The response should display the new version
+HTTP/1.1 200 OK
+Server: nginx/1.16.1 <------------ This is the pod version (new version)
+Date: Fri, 15 Jan 2021 20:16:11 GMT
+Content-Type: text/html
+Content-Length: 612
+Last-Modified: Tue, 13 Aug 2019 10:05:00 GMT
+Connection: keep-alive
+ETag: "5d528b4c-264"
+Accept-Ranges: bytes
+

09. Investigate rollout history:

  • The rollout history command print out all the saved records:
$ kubectl rollout history deployment nginx -n codewizard
+deployment.apps/nginx
+REVISION  CHANGE-CAUSE
+1         <none>
+2         kubectl set image deployment nginx nginx=nginx:1.16 --record=true
+3         kubectl set image deployment nginx nginx=nginx:1.15 --record=true
+

10. Let’s see what was changed during the previous updates:

  • Print out the rollout changes:
# replace the X with 1 or 2 or any number revision id
+$ kubectl rollout history deployment nginx -n codewizard --revision=<X>  # replace here
+deployment.apps/nginx with revision #1
+Pod Template:
+  Labels:       app=nginx
+        pod-template-hash=db749865c
+  Containers:
+   nginx:
+    Image:      nginx:1.17
+    Port:       <none>
+    Host Port:  <none>
+    Environment:        <none>
+    Mounts:     <none>
+  Volumes:      <none>
+

11. Undo the version upgrade by rolling back and restoring previous version

# Check the current nginx version
+$ curl -sI <host>:<port>
+
+# Undo the last deployment
+$ kubectl rollout undo deployment nginx
+deployment.apps/nginx rolled back
+
+# Verify that we have the previous version
+$ curl -sI <host>:<port>
+

12. Rolling Restart

  • If we deploy using imagePullPolicy: always set in the YAML file, we can use rollout restart to force K8S to grab the latest image.
  • This is the fastest restart method these days
# Force pods restart
+kubectl rollout restart deployment [deployment_name]
+
\ No newline at end of file diff --git a/mkdocs-site/05-Services/index.html b/mkdocs-site/05-Services/index.html new file mode 100644 index 0000000..217006f --- /dev/null +++ b/mkdocs-site/05-Services/index.html @@ -0,0 +1,199 @@ + 05 Services - KubernetesLabs

K8S Hands-on

Visitor Badge


Service Discovery

  • In the following lab we will learn what is a Service and go over the different Service types.

Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window


01. Some general notes on what is a Service

  • Service is a unit of application behavior bound to a unique name in a service registry.
  • Service consist of multiple network endpoints implemented by workload instances running on pods, containers, VMs etc.
  • Service allow us to gain access to any given pod or container (e.g., a web service).
  • A service is (normally) created on top of an existing deployment and exposing it to the “world”, using IP(s) & port(s).
  • K8S define 3 main ways (+FQDN internally) to define a service, which means that we have 4 different ways to access Pods.
  • There are several proxy mode which inplements diffrent behaviour, for example in user proxy mode for each Service kube-proxy opens a port (randomly chosen) on the local node. Any connections to this “proxy port” are proxied to one of the Service’s backend Pods (as reported via Endpoints).
  • All the service types are assigned with a Cluster-IP.
  • Every service also creates Endoint(s), which point to the actual pods. Endpoints are usually referred to as back-ends of a particular service.

01. Create namespace and clear previous data if there is any

# If the namespace already exists and contains data form previous steps, let's clean it
+kubectl delete namespace codewizard
+
+# Create the desired namespace [codewizard]
+$ kubectl create namespace codewizard
+namespace/codewizard created
+

02. Create the required resources for this hand-on

# Network tools pod
+$ kubectl create deployment -n codewizard multitool --image=praqma/network-multitool
+deployment.apps/multitool created
+
+# nginx pod
+$ kubectl create deployment -n codewizard nginx --image=nginx
+deployment.apps/nginx created
+
+# Verify that the pods running
+$ kubectl get all -n codewizard
+
+NAME                             READY   STATUS    RESTARTS   AGE
+pod/multitool-74477484b8-bdrwr   1/1     Running   0          29s
+pod/nginx-6799fc88d8-p2fjn       1/1     Running   0          7s
+NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
+deployment.apps/multitool   1/1     1            1           30s
+deployment.apps/nginx       1/1     1            1           8s
+NAME                                   DESIRED   CURRENT   READY   AGE
+replicaset.apps/multitool-74477484b8   1         1         1       30s
+replicaset.apps/nginx-6799fc88d8       1         1         1       8s
+


Service types

  • As previously mentioned, there are several services type. Let’s practice them:

Service type: ClusterIP

  • If not specified, the default service type is ClusterIP.
  • In order to expose the deployment as a service, use: --type=ClusterIP
  • ClusterIP will expose the pods within the cluster. Since we don’t have an external IP, it will not be reachable from outside the cluster.
  • When the service is created K8S attaches a DNS record to the service in the following format: <service name>.<namespace>.svc.cluster.local

03. Expose the nginx with ClusterIP

# Expose the service on port 80
+$ kubectl expose deployment nginx -n codewizard --port 80 --type ClusterIP
+service/nginx exposed
+
+# Check the services and see it's type
+# Grab the ClusterIP - we will use it in the next steps
+$ kubectl get services -n codewizard
+
+NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)
+nginx        ClusterIP   10.109.78.182   <none>        80/TCP
+

04. Test the nginx with ClusterIP

  • Since the service is a ClusterIP, we will test if we can access the service using the multitool pod.
# Get the name of the multitool pod to be used
+$ kubectl get pods -n codewizard
+NAME
+multitool-XXXXXX-XXXXX
+
+# Run an interactive shell inside the network-multitool-container (same concept as with Docker)
+$ kubectl exec -it <pod name> -n codewizard -- sh
+
  • Connect to the service in any of the following ways:

Test the nginx with ClusterIP

1. using the IP from the services output. grab the server response:
bash-5.0# curl -s <ClusterIP>
+
# Expected output:
+<!DOCTYPE html>
+<html>
+<head>
+<title>Welcome to nginx!</title>
+<style>
+html { color-scheme: light dark; }
+body { width: 35em; margin: 0 auto;
+font-family: Tahoma, Verdana, Arial, sans-serif; }
+</style>
+</head>
+<body>
+<h1>Welcome to nginx!</h1>
+<p>If you see this page, the nginx web server is successfully installed and
+working. Further configuration is required.</p>
+
+<p>For online documentation and support please refer to
+<a href="http://nginx.org/">nginx.org</a>.<br/>
+Commercial support is available at
+<a href="http://nginx.com/">nginx.com</a>.</p>
+
+<p><em>Thank you for using nginx.</em></p>
+</body>
+</html>
+


2. Test the nginx using the deployment name - using the service name since its the DNS name behind the scenes
bash-5.0# curl -s nginx
+
# Expected output:
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Welcome to nginx!</title>
+    <style>
+      body {
+        width: 35em;
+        margin: 0 auto;
+        font-family: Tahoma, Verdana, Arial, sans-serif;
+      }
+    </style>
+  </head>
+  <body>
+    <h1>Welcome to nginx!</h1>
+    <p>
+      If you see this page, the nginx web server is successfully installed and
+      working. Further configuration is required.
+    </p>
+    <p>
+      For online documentation and support please refer to
+      <a href="http://nginx.org/">nginx.org</a>.<br />
+      Commercial support is available at
+      <a href="http://nginx.com/">nginx.com</a>.
+    </p>
+    <p><em>Thank you for using nginx.</em></p>
+  </body>
+</html>
+


3. using the full DNS name - for every service we have a full FQDN (Fully qualified domain name) so we can use it as well

# bash-5.0# curl -s <service name>.<namespace>.svc.cluster.local
+bash-5.0# curl -s nginx.codewizard.svc.cluster.local
+


Service type: NodePort

  • NodePort: Exposes the Service on each Node’s IP at a static port (the NodePort).
  • A ClusterIP Service, to which the NodePort Service routes, is automatically created.
  • NodePort service is reachable from outside the cluster, by requesting <Node IP>:<Node Port>.

05. Create NodePort

1. Delete previous service
# Delete the existing service from previous steps
+$ kubectl delete svc nginx -n codewizard
+service "nginx" deleted from codewizard namespace
+


2. Create NodePort service

# As before but this time the type is a NodePort
+$ kubectl expose deployment -n codewizard nginx --port 80 --type NodePort
+service/nginx exposed
+
+# Verify that the type is set to NodePort.
+# This time you should see ClusterIP and port as well
+$ kubectl get svc -n codewizard
+NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)
+nginx        NodePort    100.65.29.172  <none>        80:32593/TCP
+

3. Test the NodePort service
  • If we have the host IP and the node port number, we can connect directly to the pod.

  • If you followed the previous labs, you should be able to do it yourself by now......

# Tiny clue....
+$ kubectl cluster-info
+$ kubectl get services
+
+# Executing curl <cluster host ip>:<port> you should see the flowing Output
+Welcome to nginx!
+...
+Thank you for using nginx.
+

Service type: LoadBalancer

Note

We cannot test a LoadBalancer service locally on a localhost, but only on a cluster which can provide an external-IP

06. Create LoadBalancer (only if you are on real cloud)


1. Delete previous service

# Delete the existing service from previous steps
+$ kubectl delete svc nginx -n codewizard
+service "nginx" deleted
+

2. Create LoadBalancer Service

# As before this time the type is a LoadBalancer
+$ kubectl expose deployment nginx -n codewizard --port 80 --type LoadBalancer
+service/nginx exposed
+
+# In real cloud we should se an EXTERNAL-IP and we can access the service
+# via the internet
+$ kubectl get svc
+NAME         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)
+nginx        LoadBalancer   100.69.15.89   35.205.60.29  80:31354/TCP
+

3. Test the LoadBalancer Service
# Testing load balancer only require us to use the EXTERNAL-IP
+$ curl -s <EXTERNAL-IP>
+
\ No newline at end of file diff --git a/mkdocs-site/06-DataStore/index.html b/mkdocs-site/06-DataStore/index.html new file mode 100644 index 0000000..11ba1dc --- /dev/null +++ b/mkdocs-site/06-DataStore/index.html @@ -0,0 +1,303 @@ + 06 DataStore - KubernetesLabs

K8S Hands-on

Visitor Badge


Data Store

Secrets and ConfigMaps

  • Secrets & ConfigMap are ways to store and inject configurations into your deployments.
  • Secrets usually store passwords, certificates, API keys and more.
  • ConfigMap usually store configuration (data).

Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window


First, Let’s play a bit with Secrets

01. Create namespace and clear previous data (if there is any)

# If the namespace already exist and contains data from previous steps, lets clean it
+kubectl delete namespace codewizard
+
+# Create the desired namespace [codewizard]
+$ kubectl create namespace codewizard
+namespace/codewizard created
+

Note

You can skip section number 02. if you don’t wish to build and push your docker container


02. Build the docker container

1. write the server code
  • For this demo we will use a tiny NodeJS server which will consume the desired configuration values from the secret
  • This is the code of our server server.js:
//
+// server.js
+//
+const 
+  // Get those values in runtime.
+  // The variables will be passed from the Docker file and later on from K8S ConfingMap/ecret
+  language = process.env.LANGUAGE,
+  token = process.env.TOKEN;
+
+require("http")
+  .createServer((request, response) => {
+    response.write(`Language: ${language}`);
+    response.write(`Token   : ${token}\n`);
+    response.end(`\n`);
+  })
+  // Set the default port to 5000
+  .listen(process.env.PORT || 5000 );
+


2. Write the DockerFile
  • First, let’s wrap it up as docker container
  • If you wish, you can skip this and use the existing docker image: nirgeier/k8s-secrets-sample
  • In the Dockerfile we will set the ENV for or variables
# Base Image
+FROM        node
+
+# exposed port - same port is defined in the server.js
+EXPOSE      5000
+
+# The "configuration" which we pass in runtime
+# The server will "read" those variables at run time and will print them out
+ENV         LANGUAGE    Hebrew
+ENV         TOKEN       Hard-To-Guess
+
+# Copy the server to the container
+COPY        server.js .
+
+# start the server
+ENTRYPOINT  node server.js
+


3. Build the docker container
# The container name is prefixed with the Dockerhub account
+# !!! You should replace the prefix to your dockerhub account
+# In the sample the username is `nirgeier`
+$ docker build . -t nirgeier/k8s-secrets-sample
+
+# The output should be similar to this
+Sending build context to Docker daemon   12.8kB
+Step 1/6 : FROM        node
+latest: Pulling from library/node
+2587235a7635: Pull complete
+953fe5c215cb: Pull complete
+d4d3f270c7de: Pull complete
+ed36dafe30e3: Pull complete
+00e912dd434d: Pull complete
+dd25ee3ea38e: Pull complete
+7e835b17ced9: Pull complete
+79ae84aa9e91: Pull complete
+629164f2c016: Pull complete
+Digest: sha256:3a9d0636755ebcc8e24148a148b395c1608a94bb1b4a219829c9a3f54378accb
+Status: Downloaded newer image for node:latest
+ ---> d6740064592f
+Step 2/6 : EXPOSE      5000
+ ---> Running in 060220eaa65b
+Removing intermediate container 060220eaa65b
+ ---> 68262a3e6741
+Step 3/6 : ENV         LANGUAGE    Hebrew
+ ---> Running in c404e7e6fa16
+Removing intermediate container c404e7e6fa16
+ ---> 45fcf1fe03aa
+Step 4/6 : ENV         TOKEN       Hard-To-Guess
+ ---> Running in d3c1491f9de5
+Removing intermediate container d3c1491f9de5
+ ---> 71e8acdbdab2
+Step 5/6 : COPY        server.js .
+ ---> 42233d2b66a8
+Step 6/6 : ENTRYPOINT  node server.js
+ ---> Running in 223629e16589
+Removing intermediate container 223629e16589
+ ---> f5cbb1895d66
+Successfully built f5cbb1895d66
+Successfully tagged nirgeier/k8s-secrets-sample:latest
+


4. Test the container
# Run the docker container which you build earlier,
+# replace the name if you used your own name
+# and check the response from the server.
+# It should print out the variables which were defined inside the DockerFile 
+$ docker run -d -p5000:5000 nirgeier/k8s-secrets-sample --name server
+
+# Get the response from the container 
+# The port is the one which we exposed inside the DockerFile
+curl 127.0.0.1:5000
+
+# Response:
+Language: Hebrew
+Token   : Hard-To-Guess
+


  • Stop the container
# Stop the running container
+# We are using the name which we passed in the `docker run` command --name <container name>
+docker stop server
+
  • Push the container to your docker hub account if you wish

03. Using K8S deployment & Secrets/ConfigMap

1. Writing the deployment & service file
  • Deploy the docker container that you have prepared in the previous step with the following Deployment file.
  • In this sample we will define the values in the YAML file, later on we will use Secrets/ConfigMap variables-from-yaml.yaml

apiVersion: v1
+kind: Namespace
+metadata:
+  name: codewizard
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: secrets-app
+  namespace: codewizard
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      name: secrets-app
+  template:
+    metadata:
+      labels:
+        name: secrets-app
+    spec:
+      containers:
+        - name: secrets-app
+          image: nirgeier/k8s-secrets-sample
+          imagePullPolicy: Always
+          ports:
+            - containerPort: 5000
+          env:
+            - name: LANGUAGE
+              value: Hebrew
+            - name: TOKEN
+              value: Hard-To-Guess2
+          resources:
+            limits:
+              cpu: "500m"
+              memory: "256Mi"
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: codewizard-secrets
+  namespace: codewizard
+spec:
+  selector:
+    app: codewizard-secrets
+  ports:
+    - protocol: TCP
+      port: 5000
+      targetPort: 5000
+

2. Deploy to cluster

$ kubectl apply -n codewizard -f variables-from-yaml.yaml
+deployment.apps/codewizard-secrets configured
+service/codewizard-secrets created
+

3. Test the app
  • We will need a second container for executing the curl request.
  • We will use a busyBox image for this purpose.
# grab the name of the pod
+$ kubectl get pods -n codewizard
+
+# Output
+NAME                                  READY   STATUS    RESTARTS   AGE
+codewizard-secrets-56f556c758-2mknc   1/1     Running   0          6m27s
+
+# Login to the container and test the reponse
+# kubectl exec -it -n codewizard <pod name> -- sh
+# For the above output we will use
+kubectl exec -it -n codewizard codewizard-secrets-56f556c758-2mknc -- sh
+
+# Now get the server response (from inside the container)
+$ curl localhost:5000                    
+
+# Response
+Language: Hebrew
+Token   : Hard-To-Guess2
+

04. Using Secrets & config maps

1. Create the desired secret and config map for this lab
# Create the secret 
+#   Key   = Token
+#   Value = Hard-To-Guess3
+$ kubectl create -n codewizard secret generic token --from-literal=TOKEN=Hard-To-Guess3
+secret/token created
+
+# Create the config map 
+#   Key   = LANGUAGE
+#   Value = English
+$ kubectl create -n codewizard configmap language --from-literal=LANGUAGE=English
+configmap/language created
+
+# Verify that the resources have been created:
+$ kubectl get secrets,cm -n codewizard
+NAME                         TYPE                                  DATA   AGE
+secret/default-token-8hzhn   kubernetes.io/service-account-token   3      14m
+secret/token                 Opaque                                1      80s
+NAME                         DATA   AGE
+configmap/kube-root-ca.crt   1      14m
+configmap/language           1      44s
+
+# Like other resources we can use describe to view the resource
+$ kubectl describe secret token -n codewizard
+Name:         token
+Namespace:    codewizard
+Labels:       <none>
+Annotations:  <none>
+
+Type:  Opaque <----- The content is stored as BASE64
+
+Data
+====
+TOKEN:  14 bytes
+
+# Same way for the ConfigMap
+$ kubectl describe cm language -n codewizard
+Name:         language
+Namespace:    codewizard
+Labels:       <none>
+Annotations:  <none>
+Data
+====
+LANGUAGE:
+----
+English
+Events:  <none>
+


2. Update the deployment to read the values from Secrets & ConfigMap
  • Change the env section to the following:
          env:
+            - name: LANGUAGE
+              valueFrom:
+                configMapKeyRef:    # This value will be read from the config map
+                  name:   language  # The name of the ConfigMap
+                  key:    LANGUAGE  # The key in the config map
+            - name: TOKEN
+              valueFrom:
+                  secretKeyRef:         # This value will be read from the secret
+                      name:   token     # The name of the secret
+                      key:    TOKEN     # The key in the secret
+


3. Update the deployment to read values from K8S resources

$ kubectl apply -n codewizard -f variables-from-secrets.yaml
+deployment.apps/codewizard-secrets configured
+service/codewizard-secrets unchanged
+

4. Test the changes
  • Refer to step 3.3 for testing your server
# Login to the server
+# In this sample, the pod name is: codewizard-secrets-76d99bdc54-s66vl
+kubectl exec -it codewizard-secrets-76d99bdc54-s66vl -n codewizard -- sh
+
+# Test the changes to verify that they are set from the Secret/ConfigMap
+curl localhost:5000
+
+# Out put should be
+Language: English
+Token   : Hard-To-Guess3
+


Note

Pods are not recreated or updated automatically when Secrets or ConfigMaps change, so you will have to restart your pods manually

  • To update existing secrets or ConfigMap:
$ kubectl create secret generic token -n codewizard --from-literal=Token=Token3 -o yaml --dry-run=client | kubectl replace -f -
+secret/token replaced
+
  • Test your server and verify that you see the old values.
  • Delete the old pods so they can come back to life with the new values.
  • Test your server again, now you should be able to see the changes.
\ No newline at end of file diff --git a/mkdocs-site/06-DataStore/resources/Dockerfile b/mkdocs-site/06-DataStore/resources/Dockerfile new file mode 100644 index 0000000..7fe420f --- /dev/null +++ b/mkdocs-site/06-DataStore/resources/Dockerfile @@ -0,0 +1,15 @@ +# Base Image +FROM node + +# exposed port - same port is defined in the server.js +EXPOSE 5000 + +# The "configuration" which we pass in runtime +ENV LANGUAGE Hebrew +ENV TOKEN Hard-To-Guess + +# Copy the server to the container +COPY server.js . + +# start the server +ENTRYPOINT node server.js \ No newline at end of file diff --git a/mkdocs-site/06-DataStore/resources/secret.yaml b/mkdocs-site/06-DataStore/resources/secret.yaml new file mode 100644 index 0000000..541fab5 --- /dev/null +++ b/mkdocs-site/06-DataStore/resources/secret.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +# This resource is of type Secret +kind: Secret +metadata: + # The name of the secret + name: token +# The content of the secret is defined inside the data object +data: + # The key is :TOKEN + # The value is: `SGFyZC1Uby1HdWVzczM=` which is encoded in base64 + # The plaint text value is : "Hard-To-Guess3" + + TOKEN: SGFyZC1Uby1HdWVzczM= +type: Opaque diff --git a/mkdocs-site/06-DataStore/resources/server.js b/mkdocs-site/06-DataStore/resources/server.js new file mode 100644 index 0000000..8319429 --- /dev/null +++ b/mkdocs-site/06-DataStore/resources/server.js @@ -0,0 +1,12 @@ +const // Get those values in runtime + language = process.env.LANGUAGE, + token = process.env.TOKEN; + +require("http") + .createServer((request, response) => { + response.write(`Language: ${language}\n`); + response.write(`Token : ${token}\n`); + response.end(`\n`); + }) + // Set the default port to 5000 + .listen(process.env.PORT || 5000); diff --git a/mkdocs-site/06-DataStore/resources/variables-from-secrets.yaml b/mkdocs-site/06-DataStore/resources/variables-from-secrets.yaml new file mode 100644 index 0000000..5ca58b1 --- /dev/null +++ b/mkdocs-site/06-DataStore/resources/variables-from-secrets.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: codewizard-secrets + namespace: codewizard +spec: + replicas: 1 + selector: + matchLabels: + name: codewizard-secrets + template: + metadata: + labels: + name: codewizard-secrets + spec: + containers: + # This contaner will use plain ENV parametrs + - name: secrets + image: nirgeier/k8s-secrets-sample + imagePullPolicy: Always + ports: + - containerPort: 5000 + env: + - name: LANGUAGE + valueFrom: + configMapKeyRef: # This value will be read from the config map + name: language # The name of the ConfigMap + key: LANGUAGE # The key in the config map + - name: TOKEN + valueFrom: + secretKeyRef: # This value will be read from the secret + name: token # The name of the secret + key: TOKEN # The key in the secret + resources: + limits: + cpu: "500m" + memory: "256Mi" +--- +apiVersion: v1 +kind: Service +metadata: + name: codewizard-secrets + namespace: codewizard +spec: + selector: + app: codewizard-secrets + ports: + - protocol: TCP + port: 5000 + diff --git a/mkdocs-site/06-DataStore/resources/variables-from-yaml.yaml b/mkdocs-site/06-DataStore/resources/variables-from-yaml.yaml new file mode 100644 index 0000000..490de27 --- /dev/null +++ b/mkdocs-site/06-DataStore/resources/variables-from-yaml.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: codewizard-secrets + namespace: codewizard +spec: + replicas: 1 + selector: + matchLabels: + name: codewizard-secrets + template: + metadata: + labels: + name: codewizard-secrets + spec: + containers: + # This contaner will use plain ENV parametrs + - name: secrets + image: nirgeier/k8s-secrets-sample + imagePullPolicy: Always + ports: + - containerPort: 5000 + env: + - name: LANGUAGE + value: Hebrew + - name: TOKEN + value: Hard-To-Guess2 + resources: + limits: + cpu: "500m" + memory: "256Mi" +--- +apiVersion: v1 +kind: Service +metadata: + name: codewizard-secrets + namespace: codewizard +spec: + selector: + app: codewizard-secrets + ports: + - protocol: TCP + port: 5000 + diff --git a/mkdocs-site/07-nginx-Ingress/index.html b/mkdocs-site/07-nginx-Ingress/index.html new file mode 100644 index 0000000..a1c7800 --- /dev/null +++ b/mkdocs-site/07-nginx-Ingress/index.html @@ -0,0 +1,158 @@ + 07 Nginx Ingress - KubernetesLabs

K8S Hands-on

Visitor Badge


Nginx-Ingress

Important

We cannot see it in action on a localhost (meaning that it will not get an external IP) unless we use the explicit http://host:port format.

  • Kubernetes ingress object is a DNS
  • To enable an ingress object, we need an ingress controller
  • In this demo we will use Nginx-Ingress

Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window


01. Deploy sample app

  • To get started with Nginx-Ingress, we will deploy out previous app:
# Create 3 containers
+$ kubectl create deployment ingress-pods --image=nirgeier/k8s-secrets-sample --replicas=3
+
+# Expose the service
+$ kubectl expose deployment ingress-pods --port=5000
+

02. Deploy default backend

  • Now lets deploy the Nginx-Ingress (grabbed from the official site):
apiVersion: apps/v1
+kind: Deployment
+metadata:
+    name: default-http-backend
+spec:
+    replicas: 1
+    selector:
+        matchLabels:
+        app: default-http-backend
+    template:
+        metadata:
+        labels:
+            app: default-http-backend
+        spec:
+            terminationGracePeriodSeconds: 60
+            containers:
+            - name: default-http-backend
+                # Any image is permissable as long as:
+                # 1. It serves a 404 page at /
+                # 2. It serves 200 on a /healthz endpoint
+                image: gcr.io/google_containers/defaultbackend:1.0
+                livenessProbe:
+                httpGet:
+                    path: /healthz
+                    port: 8080
+                    scheme: HTTP
+                initialDelaySeconds: 30
+                timeoutSeconds: 5
+                ports:
+                - containerPort: 8080
+                resources:
+                limits:
+                    cpu: 10m
+                    memory: 20Mi
+                requests:
+                    cpu: 10m
+                    memory: 20Mi
+

03. Create service

  • Next, let’s create the service:
apiVersion: v1
+kind: Service
+metadata:
+    name: default-http-backend
+spec:
+    selector:
+    app: default-http-backend
+    ports:
+    - protocol: TCP
+    port: 80
+    targetPort: 8080
+    type: NodePort
+

04. Import ssl certificate

  • In this demo we will use certificate.
  • The certificate is in the same folder as this file
  • The certificate is for the hostname: ingress.local
# If you wish to create the certificate use this script
+### ---> The common Name fiels is your host for later on
+###      Common Name (e.g. server FQDN or YOUR name) []:
+$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout certificate.key -out certificate.crt
+
+# Create a pem file
+# The purpose of the DH parameters is to exchange secrets
+$ openssl dhparam -out certificate.pem 2048
+
  • Store the certificate in secret:
# Store the certificate
+$ kubectl create secret tls tls-certificate --key certificate.key --cert certificate.crt
+secret/tls-certificate created
+
+# Store the DH parameters
+$ kubectl create secret generic tls-dhparam --from-file=certificate.pem
+secret/tls-dhparam created
+

05. Deploy the ingress

  • Now that we have the certificate, we can deploy the Ingress:
# Ingress.yaml
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+    name: my-first-ingress
+annotations:
+    kubernetes.io/ingress.class: "nginx"
+    nginx.org/ssl-services: "my-service"
+spec:
+    tls:
+        - hosts:
+        - myapp.local
+        secretName: tls-certificate
+    rules:
+    - host: myapp.local
+        http:
+        paths:
+        - path: /
+            backend:
+            serviceName: ingress-pods
+            servicePort: 5000
+

06. Enable the ingress addon

  • The Ingress is not enabled by default, so we have to “turn it on”:
$ minikube addons enable ingress
+  ingress was successfully enabled
+
\ No newline at end of file diff --git a/mkdocs-site/07-nginx-Ingress/resources/certificate.crt b/mkdocs-site/07-nginx-Ingress/resources/certificate.crt new file mode 100644 index 0000000..10f654b --- /dev/null +++ b/mkdocs-site/07-nginx-Ingress/resources/certificate.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDmzCCAoOgAwIBAgIUWlCuTkTKFJ5Mx80Cg1NdgcrMeZowDQYJKoZIhvcNAQEL +BQAwXTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAwwNaW5ncmVzcy5sb2Nh +bDAeFw0xOTEyMjExOTQzMDlaFw0yMDEyMjAxOTQzMDlaMF0xCzAJBgNVBAYTAkFV +MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz +IFB0eSBMdGQxFjAUBgNVBAMMDWluZ3Jlc3MubG9jYWwwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQC8FakvkG4zKIa9xuRrNLAPk2FspF8h5oLC3JuHff00 +SkRYJoWGynEdLW31RTVN7pvFg2Z3Y+RpXxpnesDQmkjqKEvjfYX1gVwgRCrpESg1 +yRNm3jc7e0RRuwxJglH3gktYDV7+i3v44Tda4K1YTvLvgqOvNSULUAC+m22gzZr9 +gHFPmTyTiTCOVJTVmdVqBgmYtZ0q5PE/Pa9+wtYv4tA8bS4dO5BOYYfuN7vXgvkW +kVXR5lZJFz9aSQ3xzj9veERSeHc8m0bNlAU2AicwH0mOpOtuN4KulWgcvqrCQXT5 +7Fxwx7AeiZkrimyBDThPzLzWMikDgojiEJ+HCHI0chgrAgMBAAGjUzBRMB0GA1Ud +DgQWBBT1Lwz4mFcGWiGfQZQR2Z8sgDrFTTAfBgNVHSMEGDAWgBT1Lwz4mFcGWiGf +QZQR2Z8sgDrFTTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAL +gWcvCxe4QwZnrkyJ8eWKgaHn63sub7hR4mT+EgPeja70not0BORzKjIDr3yZSKJU +Kbuj6Wbkq8wsfeD1HahLNuq0dQE808llFLkHv0NbFY7rRG4XV72Rr03L2rtfzZBX +2N+QG88j6pZQMqYSRlcN4scuJqs9Ug2pmzGOBweZCsg72HJCUjD/FXLWBlY9eNKk +/5tHihqQll42xyUFRrVnuu+1HVaFCRdt6Bct8k2D3I1GzJrz0Se4iHJsq/Izvf7v +InboDQkr6vq1olE8RJBraQ7Fngay7vo12rXCKNt56PIiqbQ8WvQQZfAY7yMOhqAM +ooH8YynHIXY13xyom9EH +-----END CERTIFICATE----- diff --git a/mkdocs-site/07-nginx-Ingress/resources/certificate.key b/mkdocs-site/07-nginx-Ingress/resources/certificate.key new file mode 100644 index 0000000..a392d38 --- /dev/null +++ b/mkdocs-site/07-nginx-Ingress/resources/certificate.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8FakvkG4zKIa9 +xuRrNLAPk2FspF8h5oLC3JuHff00SkRYJoWGynEdLW31RTVN7pvFg2Z3Y+RpXxpn +esDQmkjqKEvjfYX1gVwgRCrpESg1yRNm3jc7e0RRuwxJglH3gktYDV7+i3v44Tda +4K1YTvLvgqOvNSULUAC+m22gzZr9gHFPmTyTiTCOVJTVmdVqBgmYtZ0q5PE/Pa9+ +wtYv4tA8bS4dO5BOYYfuN7vXgvkWkVXR5lZJFz9aSQ3xzj9veERSeHc8m0bNlAU2 +AicwH0mOpOtuN4KulWgcvqrCQXT57Fxwx7AeiZkrimyBDThPzLzWMikDgojiEJ+H +CHI0chgrAgMBAAECggEARwY2+UslEhR/rTJqF0GyKm+RHqGDex28yzDbWnLtJs3U +uSTyz0+rH0WEfFZCJsev8woHq5YBLvlG00S7gwp/9kx5O9Kuv2K2E0kqmxBrisP/ +m5zWZpPJ3MMxhKC9qyV8pieGc8Dgc784VAz76JkHjAJdJVCASKFRZqjy4QJDQO6V +QwTOgSyLBr1osoJmksDFcUXgoAjFEoKIeYnvXbm939ZgaeM3NHaMt2om62tr8uJY +bU2C2Ehvx79bKG0Fmw8u47y2oZrkYQ4hO6/lRs6wJUSuLODCxMlwCaUh4IoXl04m +/Qjp2ARSHkx5ISkpaYryyc9Y/R5dmNmKoZrs+NVfwQKBgQDzGyFh9LaH6lHTyvtc +jocFndZ9r0LWRGGQlN/eKlJvVB+CgMj7RWuC1dq8Bg9N1SeEHWaJVv/+y+GtJEZZ +56DnKW1UDTqkmynoTab5slve0pXNCrHw0Pc+C7OIze0dIcCE2MuZrAvWs8QaEwbw +ieADyO9lYkfE4YgpaW+ShSxcMwKBgQDGD3Sm9hjVC7J9BLYg608ZTzSBcbA0Gm9E +phYEz44wkAsiJ1qyQCPP58HFC+S3ukCILoAMHCMrCcyZCVw5B1t19NRplbqGhCZQ +R3hr3CUT4nLNRgfpR4qAbhaj8aPo8Huig+r4G7N8g3UlZQWiEkL9mxRPZZILDf1Q +zWRsHvdcKQKBgF1Ri8XzTuHrc4+uOkD0QSZJyV0jmq9vPlhmnWzFqDEuBI5u6zdx +FWz6tGU6mkNRUELpmkOcDtZ64t04sHywalZx05LRJTKskTCoJjFxYsys323+7gE3 +5cB+c2NPUPa+zwzvv2/01/KJvPwZU6+f7UrmpeawDEaqID9tRrPixP17AoGAHc3Z +mf5Sgky+UT3SQmXmg0J9/jSjdVO9BrGPgq3REdG7Oyp85XHtca3IZOSDSHqIl3WX +4zqguCtDVIwqCpLm2ns7M6BKb0+XjGEU5/Y6xiE/cVBmhF41o1ntokIMjlMR58S+ +KRPSEJyflj77eAYTeqJJjiEUtwEl63Dc+cA3LPECgYEAzF6BByLBLbyQgto3sDXn +k9Osh021CBk9i4+BB+YOKwPou9Vpaf1bp8oPbF2pfJSBOSKIRgavxClkZfXeV2Px +6KfS3+8N8FQhBxv5pnngqfuYnV08nb2CCi1Eb+2p0+fOF4TrU/z/9+07B7C2cxwh +Y7yHWsVYHzmZ+InbYh8m2Nc= +-----END PRIVATE KEY----- diff --git a/mkdocs-site/07-nginx-Ingress/resources/certificate.pem b/mkdocs-site/07-nginx-Ingress/resources/certificate.pem new file mode 100644 index 0000000..e98c282 --- /dev/null +++ b/mkdocs-site/07-nginx-Ingress/resources/certificate.pem @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEAzrS+fSz9I9gw2+I5mKenJYXhzV0AJLRdCOVTpYjJKIHMxvvbsfsl +ZH686WkxD1v62yDLbYMmWk8UPQMEx4dA980XnImt2gti7df0XHA0R2aZarvDyg30 +k8JMQNBDn8sZJ9QIHgQ/KGvH5Fn5nIjMJ/YtnWxssZAXLq+6azCjzKabKkGYvUmA +TE0g0xwH32Cn1cJlV5pPq2YriGTSLu/HKIV+gyP528F8eXz5IYiOFuV4/hk/UxjX +IlhGiD69dxGaCCxh3lqcDasOxLCwFfWU2XudH6q8Iztifwmgtcuw48kkDL/hTrUQ ++LgrAraFtm20a+Tu6/ZFeASuMX2XRDiVkwIBAg== +-----END DH PARAMETERS----- diff --git a/mkdocs-site/07-nginx-Ingress/resources/ingress-nginx-deployment.yaml b/mkdocs-site/07-nginx-Ingress/resources/ingress-nginx-deployment.yaml new file mode 100644 index 0000000..90b2ca1 --- /dev/null +++ b/mkdocs-site/07-nginx-Ingress/resources/ingress-nginx-deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: default-http-backend +spec: + replicas: 1 + selector: + matchLabels: + app: default-http-backend + template: + metadata: + labels: + app: default-http-backend + spec: + terminationGracePeriodSeconds: 60 + containers: + - name: default-http-backend + # Any image is permissable as long as: + # 1. It serves a 404 page at / + # 2. It serves 200 on a /healthz endpoint + image: gcr.io/google_containers/defaultbackend:1.0 + livenessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + initialDelaySeconds: 30 + timeoutSeconds: 5 + ports: + - containerPort: 8080 + resources: + limits: + cpu: 10m + memory: 20Mi + requests: + cpu: 10m + memory: 20Mi \ No newline at end of file diff --git a/mkdocs-site/07-nginx-Ingress/resources/ingress-nginx-service.yaml b/mkdocs-site/07-nginx-Ingress/resources/ingress-nginx-service.yaml new file mode 100644 index 0000000..e5e0983 --- /dev/null +++ b/mkdocs-site/07-nginx-Ingress/resources/ingress-nginx-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: default-http-backend +spec: + selector: + app: default-http-backend + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: NodePort \ No newline at end of file diff --git a/mkdocs-site/07-nginx-Ingress/resources/ingress.yaml b/mkdocs-site/07-nginx-Ingress/resources/ingress.yaml new file mode 100644 index 0000000..924437b --- /dev/null +++ b/mkdocs-site/07-nginx-Ingress/resources/ingress.yaml @@ -0,0 +1,20 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: my-first-ingress + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.org/ssl-services: "my-service" +spec: + tls: + - hosts: + - myapp.local + secretName: tls-certificate + rules: + - host: myapp.local + http: + paths: + - path: / + backend: + serviceName: ingress-pods + servicePort: 5000 diff --git a/mkdocs-site/08-Kustomization/index.html b/mkdocs-site/08-Kustomization/index.html new file mode 100644 index 0000000..8372ec7 --- /dev/null +++ b/mkdocs-site/08-Kustomization/index.html @@ -0,0 +1,499 @@ + 08 Kustomization - KubernetesLabs

K8S Hands-on

Visitor Badge


Kustomization - kubectl kustomize

Declarative Configuration in Kubernetes

  • Kustomize is a very powerful too for customizing and building Kubernetes resources.
  • Kustomize started at 2017, and added to kubectl since version 1.14.
  • Kustomize has many useful features for managing and deploying resource.
  • When you execute a Kustomization beside using the builtin features, it will also re-order the resources in a logical way for the K8S to be deployed.

01. Re-order the resources

  • Kustomization re-orders the Kind for optimization. For this demo, we will need an existing namespace before using it.

  • The order of the resources is defined in the source code

// An attempt to order things to help k8s, e.g.
+// - Namespace should be first.
+// - Service should come before things that refer to it.
+// In some cases order just specified to provide determinism.
+var orderFirst = []string{
+    "Namespace",
+    "ResourceQuota",
+    "StorageClass",
+    "CustomResourceDefinition",
+    "ServiceAccount",
+    "PodSecurityPolicy",
+    "Role",
+    "ClusterRole",
+    "RoleBinding",
+    "ClusterRoleBinding",
+    "ConfigMap",
+    "Secret",
+    "Endpoints",
+    "Service",
+    "LimitRange",
+    "PriorityClass",
+    "PersistentVolume",
+    "PersistentVolumeClaim",
+    "Deployment",
+    "StatefulSet",
+    "CronJob",
+    "PodDisruptionBudget",
+}
+
+var orderLast = []string{
+    "MutatingWebhookConfiguration",
+    "ValidatingWebhookConfiguration",
+}
+

02. Base resource for our demo

  • In the following samples we will refer to the following base.yaml file:
# base.yaml
+# This is the base file for all the demos in this folder
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: myapp
+spec:
+  selector:
+    matchLabels:
+      app: myapp
+  template:
+    metadata:
+      labels:
+        app: myapp
+    spec:
+      containers:
+        - name: myapp
+          image: __image__
+

03. Common Features


commonAnnotation

kubectl kustomize samples/01-commonAnnotation
+
### FileName: kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+# This will add annotation under every metadata entry
+# ex: main metadata, spec.metadata etc
+commonAnnotations:
+  author: nirgeier@gmail.com
+
  • Output:
### commonAnnotation output
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  annotations:
+    ### Annotation added here
+    author: nirgeier@gmail.com
+    name: myapp
+spec:
+  selector:
+    matchLabels:
+      app: myapp
+  template:
+    metadata:
+      ### Annotation added here
+      annotations:
+        author: nirgeier@gmail.com
+      labels:
+        app: myapp
+    spec:
+      containers:
+        - image: __image__
+          name: myapp
+

commonLabels

kubectl kustomize samples/02-commonLabels
+
apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+# This will add annotation under every metadata entry
+# ex: main metadata, spec.metadata etc
+commonLabels:
+  author: nirgeier@gmail.com
+  env: codeWizard-cluster
+
+bases:
+  - ../_base
+
  • Output:
apiVersion: apps/v1
+kind: Deployment
+metadata:
+    # Labels added ....
+    labels:
+    author: nirgeier@gmail.com
+    env: codeWizard-cluster
+  name: myapp
+spec:
+  selector:
+    matchLabels:
+      app: myapp
+      # Labels added ....
+      author: nirgeier@gmail.com
+      env: codeWizard-cluster
+  template:
+    metadata:
+      labels:
+        app: myapp
+        # Labels added ....
+        author: nirgeier@gmail.com
+        env: codeWizard-cluster
+    spec:
+      containers:
+      - image: __image__
+        name: myapp
+

Generators

  • Kustomization also support generate ConfigMap / Secret in several ways.
  • The default behavior is adding the output hash value as suffix to the name, e.g.: secretMapFromFile-495dtcb64g
apiVersion: v1
+data:
+  APP_ENV: ZGV2ZWxvcG1lbnQ=
+  LOG_DEBUG: dHJ1ZQ==
+  NODE_ENV: ZGV2
+  REGION: d2V1
+kind: Secret
+metadata:
+  name: secretMapFromFile-495dtcb64g # <--------------------------
+type: Opaque
+
  • We can disable the suffix with the following addition to the kustomization.yaml
generatorOptions:
+  disableNameSuffixHash: true
+

configMapGenerator

  • From Env

    • .env
      key1=value1
      +env=qa
      +
    • kustomization.yaml

      # Generate config file from env file
      +configMapGenerator:
      +  - name: configMapFromEnv
      +    env: .env
      +

    • The output of configMapFromEnv:

      apiVersion: v1
      +data:
      +  env: qa
      +  key1: value1
      +kind: ConfigMap
      +metadata:
      +  name: configMapFromEnv-c9655hf97k
      +

  • From File

    • .env
      key1=value1
      +env=qa
      +
    • kustomization.yaml

      # Generate config file from env file
      +configMapGenerator:
      +  - name: configMapFromEnv
      +    files: 
      +    - .env
      +

    • The output of configMapFromEnv:

      apiVersion: v1
      +data:
      +  .env: "key1=value1\r\nenv=qa" # <--------------------------
      +kind: ConfigMap
      +metadata:
      +  name: configFromFile-dfhmctd84d
      +

  • From Literal

    • .env
      key1=value1
      +env=qa
      +
    • kustomization.yaml

      configMapGenerator:
      +  - name: configFromLiterals
      +    literals:
      +      - Key1=value1
      +      - Key2=value2
      +

    • The output of configMapFromEnv:

      apiVersion: v1
      +data:
      +  Key1: value1
      +  Key2: value2
      +kind: ConfigMap
      +metadata:
      +  name: configFromLiterals-h777b4gdf5
      +


Secret Generator

# Similar to configMap but with an additional type field
+secretGenerator:
+  # Generate secret from env file
+  - name: secretMapFromFile
+    env: .env
+    type: Opaque
+generatorOptions:
+  disableNameSuffixHash: true
+

images

  • Modify the name, tags and/or digest for images.
kubectl kustomize samples/04-images
+
# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+  - ./base.yaml
+
+images:
+  # The image as its defined in the Deployment file
+  - name: __image__
+    # The new name to set
+    newName: my-registry/my-image
+    # optional: image tag
+    newTag: v1
+
  • Output:
apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: myapp
+spec:
+  selector:
+    matchLabels:
+      app: myapp
+  template:
+    metadata:
+      labels:
+        app: myapp
+    spec:
+      containers:
+        # --- This image was updated
+        - image: my-registry/my-image:v1
+          name: myapp
+

Namespaces

kubectl kustomize samples/05-Namespace
+
# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+# Add the desired namespace to all resources
+namespace: kustomize-namespace
+
+bases:
+  - ../_base
+
  • Output:
apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: myapp
+  # Namespace added here
+  namespace: kustomize-namespace
+

Prefix-suffix

kubectl kustomize samples/06-Prefix-Suffix
+
# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+# Add the desired Prefix to all resources
+namePrefix: prefix-codeWizard-
+nameSuffix: -suffix-codeWizard
+
+bases:
+  - ../_base
+
  • Output:
apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: prefix-codeWizard-myapp-suffix-codeWizard
+

Replicas

  • deployment
# deployment.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: deployment
+spec:
+  replicas: 5
+  selector:
+    name: deployment
+  template:
+    containers:
+      - name: container
+        image: registry/conatiner:latest
+
  • kustomization
# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+replicas:
+  - name: deployment
+    count: 10
+
+resources:
+  - deployment.yaml
+
  • Output:

Note

There is a bug with the replicas entries which return error for some reason.

$ kubectl kustomize .
+
+# For some reason we get this error:
+Error: json: unknown field "replicas"
+
+# Workaround for this error for now is:
+$ kustomize build .
+
apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: deployment
+spec:
+  replicas: 10
+  selector:
+    name: deployment
+  template:
+    containers:
+      - image: registry/conatiner:latest
+        name: container
+

Patches

  • There are several types of patches like [replace, delete, patchesStrategicMerge]
  • For this demo we will demonstrate patchesStrategicMerge

Patch Add/Update

kubectl kustomize samples/08-Patches/patch-add-update
+
# File: patch-memory.yaml
+# -----------------------
+# Patch limits.memory
+apiVersion: apps/v1
+kind: Deployment
+# Set the desired deployment to patch
+metadata:
+  name: myapp
+spec:
+  # patch the memory limit
+  template:
+    spec:
+      containers:
+        - name: patch-name
+          resources:
+            limits:
+              memory: 512Mi
+
# File: patch-replicas.yaml
+# -------------------------
+apiVersion: apps/v1
+kind: Deployment
+# Set the desired deployment to patch
+metadata:
+  name: myapp
+spec:
+  # This is the patch for this demo
+  replicas: 3
+
# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+bases:
+  - ../_base
+
+patchesStrategicMerge:
+- patch-memory.yaml
+- patch-replicas.yaml
+
  • Output:
apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: myapp
+spec:
+  # This is the first patch
+  replicas: 3
+  selector:
+    matchLabels:
+      app: myapp
+  template:
+    metadata:
+      labels:
+        app: myapp
+    spec:
+      # This is the second patch
+      containers:
+      - name: patch-name
+        resources:
+          limits:
+            memory: 512Mi
+      - image: __image__
+        name: myapp
+

Patch-Delete

kubectl kustomize samples/08-Patches/patch-delete
+
# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+bases:
+  - ../../_base
+
+patchesStrategicMerge:
+- patch-delete.yaml
+
# patch-delete.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: myapp
+spec:
+  template:
+    spec:
+      containers:
+        # Remove this section, in this demo it will remove the 
+        # image with the `name: myapp` 
+        - $patch: delete
+          name: myapp
+          image: __image__
+
  • Output:
apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: myapp
+spec:
+  selector:
+    matchLabels:
+      app: myapp
+  template:
+    metadata:
+      labels:
+        app: myapp
+    spec:
+      containers:
+      - image: nginx
+        name: nginx
+

Patch Replace

kubectl kustomize samples/08-Patches/patch-replace/
+
# kustomization.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+bases:
+  - ../../_base
+
+patchesStrategicMerge:
+- patch-replace.yaml
+
# patch-replace.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: myapp
+spec:
+  template:
+    spec:
+      containers:
+        # Remove this section, in this demo it will remove the 
+        # image with the `name: myapp` 
+        - $patch: replace
+        - name: myapp
+          image: nginx:latest
+          args:
+          - one
+          - two
+
  • Output:
apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: myapp
+spec:
+  selector:
+    matchLabels:
+      app: myapp
+  template:
+    metadata:
+      labels:
+        app: myapp
+    spec:
+      containers:
+      - args:
+        - one
+        - two
+        image: nginx:latest
+        name: myapp
+
\ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/01-commonAnnotation/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/01-commonAnnotation/kustomization.yaml new file mode 100644 index 0000000..b978e0d --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/01-commonAnnotation/kustomization.yaml @@ -0,0 +1,11 @@ +### FileName: kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# This will add annotation under every metadata entry +# ex: main metadata, spec.metadata etc +commonAnnotations: + author: nirgeier@gmail.com + version: v1 +bases: + - ../_base diff --git a/mkdocs-site/08-Kustomization/samples/01-commonAnnotation/output.yaml b/mkdocs-site/08-Kustomization/samples/01-commonAnnotation/output.yaml new file mode 100644 index 0000000..70fcd0b --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/01-commonAnnotation/output.yaml @@ -0,0 +1,42 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + author: nirgeier@gmail.com + version: v1 + labels: + app: postgres + name: postgres + namespace: codewizard +spec: + ports: + - port: 5432 + selector: + app: postgres + type: NodePort +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + author: nirgeier@gmail.com + version: v1 + name: myapp + namespace: codewizard +spec: + selector: + matchLabels: + app: myapp + template: + metadata: + annotations: + author: nirgeier@gmail.com + version: v1 + labels: + app: myapp + spec: + containers: + - image: __image__ + name: myapp + - image: nginx + name: nginx diff --git a/mkdocs-site/08-Kustomization/samples/02-commonLabels/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/02-commonLabels/kustomization.yaml new file mode 100644 index 0000000..141ab98 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/02-commonLabels/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# This will add annotation under every metadata entry +# ex: main metadata, spec.metadata etc +commonLabels: + author: nirgeier@gmail.com + env: qa + version: v1 +bases: + - ../_base diff --git a/mkdocs-site/08-Kustomization/samples/02-commonLabels/output.yaml b/mkdocs-site/08-Kustomization/samples/02-commonLabels/output.yaml new file mode 100644 index 0000000..93d2925 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/02-commonLabels/output.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: postgres + author: nirgeier@gmail.com + env: codeWizard-cluster + version: v1 + name: postgres + namespace: codewizard +spec: + ports: + - port: 5432 + selector: + app: postgres + author: nirgeier@gmail.com + env: codeWizard-cluster + version: v1 + type: NodePort +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + author: nirgeier@gmail.com + env: codeWizard-cluster + version: v1 + name: myapp + namespace: codewizard +spec: + selector: + matchLabels: + app: myapp + author: nirgeier@gmail.com + env: codeWizard-cluster + version: v1 + template: + metadata: + labels: + app: myapp + author: nirgeier@gmail.com + env: codeWizard-cluster + version: v1 + spec: + containers: + - image: __image__ + name: myapp + - image: nginx + name: nginx diff --git a/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/01-FromEnv/ConfigMap.yaml b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/01-FromEnv/ConfigMap.yaml new file mode 100644 index 0000000..de41670 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/01-FromEnv/ConfigMap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + env: qa + key1: value1 +kind: ConfigMap +metadata: + name: configMapFromEnv-c9655hf97k diff --git a/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/01-FromEnv/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/01-FromEnv/kustomization.yaml new file mode 100644 index 0000000..ecb61f1 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/01-FromEnv/kustomization.yaml @@ -0,0 +1,13 @@ +# kustomization.yaml for ConfigMap +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +configMapGenerator: + # Generate config file from env file + - name: configMapFromEnv + env: .env +# +# Optional flag to mark if we want hashed suffix or not +# +# generatorOptions: +# disableNameSuffixHash: true \ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/02-FromFile/ConfigMap.yaml b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/02-FromFile/ConfigMap.yaml new file mode 100644 index 0000000..6dc4306 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/02-FromFile/ConfigMap.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +data: + .env: |- + key1=value1 + env=qa +kind: ConfigMap +metadata: + name: configFromFile-456kgtfd6m diff --git a/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/02-FromFile/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/02-FromFile/kustomization.yaml new file mode 100644 index 0000000..d6200fd --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/02-FromFile/kustomization.yaml @@ -0,0 +1,14 @@ +# kustomization.yaml for ConfigMap +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Generate config file from file +configMapGenerator: + - name: configFromFile + files: + - .env +# +# Optional flag to mark if we want hashed suffix or not +# +#generatorOptions: +# disableNameSuffixHash: true \ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/03-FromLiteral/ConfigMap.yaml b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/03-FromLiteral/ConfigMap.yaml new file mode 100644 index 0000000..30cf321 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/03-FromLiteral/ConfigMap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + Key1: value1 + Key2: value2 +kind: ConfigMap +metadata: + name: configFromLiterals-h777b4gdf5 diff --git a/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/03-FromLiteral/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/03-FromLiteral/kustomization.yaml new file mode 100644 index 0000000..f3a886f --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/03-generators/ConfigMap/03-FromLiteral/kustomization.yaml @@ -0,0 +1,16 @@ +# kustomization.yaml for ConfigMap +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +configMapGenerator: + # Generate config file from key value pairs file + # Generate configMap from direct input + - name: configFromLiterals + literals: + - Key1=value1 + - Key2=value2 +# +# Optional flag to mark if we want hashed suffix or not +# +#generatorOptions: +# disableNameSuffixHash: true \ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/03-generators/Secret/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/03-generators/Secret/kustomization.yaml new file mode 100644 index 0000000..4c45ea9 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/03-generators/Secret/kustomization.yaml @@ -0,0 +1,11 @@ +# kustomization.yaml for ConfigMap +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +secretGenerator: + # Generate secret from env file + - name: secretMapFromFile + env: .env + type: Opaque +generatorOptions: + disableNameSuffixHash: true diff --git a/mkdocs-site/08-Kustomization/samples/03-generators/Secret/output.yaml b/mkdocs-site/08-Kustomization/samples/03-generators/Secret/output.yaml new file mode 100644 index 0000000..879b163 Binary files /dev/null and b/mkdocs-site/08-Kustomization/samples/03-generators/Secret/output.yaml differ diff --git a/mkdocs-site/08-Kustomization/samples/04-images/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/04-images/kustomization.yaml new file mode 100644 index 0000000..5a17d91 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/04-images/kustomization.yaml @@ -0,0 +1,14 @@ +# kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +bases: + - ../_base + +images: + # The image as its defined in the Deployment file + - name: __image__ + # The new name to set + newName: my-registry/my-image + # optional: image tag + newTag: v3 diff --git a/mkdocs-site/08-Kustomization/samples/04-images/output.yaml b/mkdocs-site/08-Kustomization/samples/04-images/output.yaml new file mode 100644 index 0000000..647958c --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/04-images/output.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: postgres + name: postgres + namespace: codewizard +spec: + ports: + - port: 5432 + selector: + app: postgres + type: NodePort +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp + namespace: codewizard +spec: + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + spec: + containers: + - image: my-registry/my-image:v3 + name: myapp + - image: nginx + name: nginx diff --git a/mkdocs-site/08-Kustomization/samples/05-Namespace/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/05-Namespace/kustomization.yaml new file mode 100644 index 0000000..5dc7de2 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/05-Namespace/kustomization.yaml @@ -0,0 +1,9 @@ +# kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Add the desired namespace to all resources +namespace: kustomize-namespace + +bases: + - ../_base \ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/05-Namespace/output.yaml b/mkdocs-site/08-Kustomization/samples/05-Namespace/output.yaml new file mode 100644 index 0000000..299d8db --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/05-Namespace/output.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: postgres + name: postgres + namespace: kustomize-namespace +spec: + ports: + - port: 5432 + selector: + app: postgres + type: NodePort +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp + namespace: kustomize-namespace +spec: + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + spec: + containers: + - image: __image__ + name: myapp + - image: nginx + name: nginx diff --git a/mkdocs-site/08-Kustomization/samples/06-Prefix-Suffix/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/06-Prefix-Suffix/kustomization.yaml new file mode 100644 index 0000000..39b1ded --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/06-Prefix-Suffix/kustomization.yaml @@ -0,0 +1,10 @@ +# kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Add the desired Prefix to all resources +namePrefix: prefix-codeWizard- +nameSuffix: -suffix-codeWizard + +bases: + - ../_base diff --git a/mkdocs-site/08-Kustomization/samples/06-Prefix-Suffix/output.yaml b/mkdocs-site/08-Kustomization/samples/06-Prefix-Suffix/output.yaml new file mode 100644 index 0000000..56ac116 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/06-Prefix-Suffix/output.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: postgres + name: prefix-codeWizard-postgres-suffix-codeWizard + namespace: codewizard +spec: + ports: + - port: 5432 + selector: + app: postgres + type: NodePort +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prefix-codeWizard-myapp-suffix-codeWizard + namespace: codewizard +spec: + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + spec: + containers: + - image: __image__ + name: myapp + - image: nginx + name: nginx diff --git a/mkdocs-site/08-Kustomization/samples/07-replicas/deployment.yaml b/mkdocs-site/08-Kustomization/samples/07-replicas/deployment.yaml new file mode 100644 index 0000000..0c5f933 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/07-replicas/deployment.yaml @@ -0,0 +1,13 @@ +# deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment +spec: + replicas: 5 + selector: + name: deployment + template: + containers: + - name: container + image: registry/conatiner:latest diff --git a/mkdocs-site/08-Kustomization/samples/07-replicas/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/07-replicas/kustomization.yaml new file mode 100644 index 0000000..1f58d79 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/07-replicas/kustomization.yaml @@ -0,0 +1,10 @@ +# kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +replicas: + - name: deployment + count: 10 + +resources: + - deployment.yaml diff --git a/mkdocs-site/08-Kustomization/samples/07-replicas/output.yaml b/mkdocs-site/08-Kustomization/samples/07-replicas/output.yaml new file mode 100644 index 0000000..4efcf38 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/07-replicas/output.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment +spec: + replicas: 10 + selector: + name: deployment + template: + containers: + - image: registry/conatiner:latest + name: container diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/kustomization.yaml new file mode 100644 index 0000000..567ed7d --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/kustomization.yaml @@ -0,0 +1,11 @@ +# kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +bases: + - ../../_base + +patchesStrategicMerge: +- patch-memory.yaml +- patch-replicas.yaml +- patch-service.yaml \ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/output.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/output.yaml new file mode 100644 index 0000000..99e8df8 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/output.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: postgres + name: postgres + namespace: codewizard +spec: + ports: + - port: 15432 + - port: 5432 + selector: + app: postgres + type: NodePort +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp + namespace: codewizard +spec: + replicas: 3 + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + spec: + containers: + - name: patch-name + resources: + limits: + memory: 512Mi + - image: __image__ + name: myapp + - image: nginx + name: nginx diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/patch-memory.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/patch-memory.yaml new file mode 100644 index 0000000..57b728f --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/patch-memory.yaml @@ -0,0 +1,16 @@ +# File: patch-memory.yaml +# Patch limits.memory +apiVersion: apps/v1 +kind: Deployment +# Set the desired deployment to patch +metadata: + name: myapp +spec: + # pathc the memory limit + template: + spec: + containers: + - name: patch-name + resources: + limits: + memory: 512Mi diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/patch-replicas.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/patch-replicas.yaml new file mode 100644 index 0000000..891bfc3 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/patch-replicas.yaml @@ -0,0 +1,9 @@ +# File: patch-replicas.yaml +apiVersion: apps/v1 +kind: Deployment +# Set the desired deployment to patch +metadata: + name: myapp +spec: + # This is the patch for this demo + replicas: 3 \ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/patch-service.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/patch-service.yaml new file mode 100644 index 0000000..27e2aec --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-add-update/patch-service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres +spec: + # The default port for postgres + ports: + # Add additional port to the ports list + - port: 15432 diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-delete/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-delete/kustomization.yaml new file mode 100644 index 0000000..7a4deb8 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-delete/kustomization.yaml @@ -0,0 +1,9 @@ +# kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +bases: + - ../../_base + +patchesStrategicMerge: +- patch-delete.yaml diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-delete/output.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-delete/output.yaml new file mode 100644 index 0000000..08163f1 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-delete/output.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: postgres + name: postgres + namespace: codewizard +spec: + ports: + - port: 5432 + selector: + app: postgres + type: NodePort +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp + namespace: codewizard +spec: + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + spec: + containers: + - image: nginx + name: nginx diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-delete/patch-delete.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-delete/patch-delete.yaml new file mode 100644 index 0000000..8e280a4 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-delete/patch-delete.yaml @@ -0,0 +1,15 @@ +# patch-delete.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp +spec: + template: + spec: + containers: + # Remove this section, in this demo it will remove the + # image with the `name: myapp` + - $patch: delete + name: myapp + image: __image__ + \ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/kustomization.yaml new file mode 100644 index 0000000..386fdec --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/kustomization.yaml @@ -0,0 +1,10 @@ +# kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +bases: + - ../../_base + +patchesStrategicMerge: +- patch-replace-ports.yaml +- patch-replace-image.yaml diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/output.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/output.yaml new file mode 100644 index 0000000..754530a --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/output.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: postgres + name: postgres + namespace: postgres +spec: + ports: + - port: 80 + selector: + app: postgres +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp + namespace: codewizard +spec: + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + spec: + containers: + - args: + - one + - two + image: nginx:latest + name: myapp diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/patch-replace-image.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/patch-replace-image.yaml new file mode 100644 index 0000000..4157787 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/patch-replace-image.yaml @@ -0,0 +1,19 @@ +# patch-replace.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp +spec: + template: + spec: + containers: + # Remove this section, in this demo it will remove the + # image with the `name: myapp` + - $patch: replace + - name: myapp + image: nginx:latest + args: + - one + - two + + \ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/patch-replace-ports.yaml b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/patch-replace-ports.yaml new file mode 100644 index 0000000..462042d --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/08-Patches/patch-replace/patch-replace-ports.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: postgres + labels: + app: postgres +spec: + selector: + app: postgres + $patch: replace + # Replace the current ports with port 80 + ports: + - port: 80 \ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/09-Add-Files/add-files.sh b/mkdocs-site/08-Kustomization/samples/09-Add-Files/add-files.sh new file mode 100644 index 0000000..43e4d5d --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/09-Add-Files/add-files.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Loop over the resources folder +for filePath in "_base"/* +do + # Add the yaml file to the kustomization file + kustomize edit add resource $filePath +done \ No newline at end of file diff --git a/mkdocs-site/08-Kustomization/samples/_base/deployment.yaml b/mkdocs-site/08-Kustomization/samples/_base/deployment.yaml new file mode 100644 index 0000000..d762e01 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/_base/deployment.yaml @@ -0,0 +1,20 @@ +# This is the base file for all the demos in this folder +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myapp +spec: + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + spec: + containers: + - name: myapp + image: __image__ + - name: nginx + image: nginx + diff --git a/mkdocs-site/08-Kustomization/samples/_base/kustomization.yaml b/mkdocs-site/08-Kustomization/samples/_base/kustomization.yaml new file mode 100644 index 0000000..a090f73 --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/_base/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: codewizard + +resources: + - deployment.yaml + - service.yaml diff --git a/mkdocs-site/08-Kustomization/samples/_base/service.yaml b/mkdocs-site/08-Kustomization/samples/_base/service.yaml new file mode 100644 index 0000000..02d883f --- /dev/null +++ b/mkdocs-site/08-Kustomization/samples/_base/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + labels: + app: postgres +spec: + selector: + app: postgres + # Service of type NodePort + type: NodePort + # The default port for postgres + ports: + - port: 5432 diff --git a/mkdocs-site/09-StatefulSet/PostgreSQL/Namespace.yaml b/mkdocs-site/09-StatefulSet/PostgreSQL/Namespace.yaml new file mode 100644 index 0000000..ae00351 --- /dev/null +++ b/mkdocs-site/09-StatefulSet/PostgreSQL/Namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: codewizard diff --git a/mkdocs-site/09-StatefulSet/PostgreSQL/base/ConfigMap.yaml b/mkdocs-site/09-StatefulSet/PostgreSQL/base/ConfigMap.yaml new file mode 100644 index 0000000..78eb0f0 --- /dev/null +++ b/mkdocs-site/09-StatefulSet/PostgreSQL/base/ConfigMap.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgres-config + labels: + app: postgres +data: + # The following names are the one defined in the official postgres docs + + # The name of the database we will use in this demo + POSTGRES_DB: codewizard + # the user name for this demo + POSTGRES_USER: codewizard + # The password for this demo + POSTGRES_PASSWORD: admin123 diff --git a/mkdocs-site/09-StatefulSet/PostgreSQL/base/PersistentVolumeClaim.yaml b/mkdocs-site/09-StatefulSet/PostgreSQL/base/PersistentVolumeClaim.yaml new file mode 100644 index 0000000..4d1385c --- /dev/null +++ b/mkdocs-site/09-StatefulSet/PostgreSQL/base/PersistentVolumeClaim.yaml @@ -0,0 +1,21 @@ +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: postgres-pv-claim + labels: + app: postgres +spec: + # in this demo we use GCP so we are using the 'standard' StorageClass + # We can of course define our own StorageClass resource + storageClassName: standard + + ### Access Modes + # The access modes are: + # ReadWriteOnce (RWO) - The volume can be mounted as read-write by a single node + # ReadWriteMany (RWX) - The volume can be mounted as read-write by a multiple nodes + # ReadOnlyMany (ROX) - The volume can be mounted as read-only by a multiple nodes + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi diff --git a/mkdocs-site/09-StatefulSet/PostgreSQL/base/Service.yaml b/mkdocs-site/09-StatefulSet/PostgreSQL/base/Service.yaml new file mode 100644 index 0000000..4cf9972 --- /dev/null +++ b/mkdocs-site/09-StatefulSet/PostgreSQL/base/Service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + labels: + app: postgres +spec: + selector: + app: postgres + # Service of type nodeport + type: NodePort + # The deafult port for postgres + ports: + - port: 5432 diff --git a/mkdocs-site/09-StatefulSet/PostgreSQL/base/StatefulSet.yaml b/mkdocs-site/09-StatefulSet/PostgreSQL/base/StatefulSet.yaml new file mode 100644 index 0000000..4bbb0d9 --- /dev/null +++ b/mkdocs-site/09-StatefulSet/PostgreSQL/base/StatefulSet.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres +spec: + replicas: 1 + # StatefulSet must contain a serviceName + serviceName: postgres + selector: + matchLabels: + app: postgres # has to match .spec.template.metadata.labels + template: + metadata: + labels: + app: postgres # has to match .spec.selector.matchLabels + spec: + containers: + - name: postgres + image: postgres:10.4 + imagePullPolicy: "IfNotPresent" + # The default DB port + ports: + - containerPort: 5432 + # Load the required configuration env values form the configMap + envFrom: + - configMapRef: + name: postgres-config + # Use volume for storage + volumeMounts: + - mountPath: /var/lib/postgresql/data + name: postgredb + # We can use PersistentVolume or PersistentVolumeClaim. + # In this sample we are useing PersistentVolumeClaim + volumes: + - name: postgredb + persistentVolumeClaim: + # reference to Pre-Define PVC + claimName: postgres-pv-claim diff --git a/mkdocs-site/09-StatefulSet/PostgreSQL/base/Storage.yaml b/mkdocs-site/09-StatefulSet/PostgreSQL/base/Storage.yaml new file mode 100644 index 0000000..eb96c41 --- /dev/null +++ b/mkdocs-site/09-StatefulSet/PostgreSQL/base/Storage.yaml @@ -0,0 +1,31 @@ +kind: PersistentVolume +apiVersion: v1 +metadata: + name: postgres-pv-volume + labels: + type: local + app: postgres +spec: + storageClassName: manual + #persistentVolumeReclaimPolicy: Retain + capacity: + storage: 5Gi + accessModes: + - ReadWriteMany + hostPath: + path: "/mnt/data" +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: postgres-pv-claim + namespace: codewizard + labels: + app: postgres +spec: + storageClassName: standard + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi diff --git a/mkdocs-site/09-StatefulSet/PostgreSQL/base/kustomization.yaml b/mkdocs-site/09-StatefulSet/PostgreSQL/base/kustomization.yaml new file mode 100644 index 0000000..2a289f7 --- /dev/null +++ b/mkdocs-site/09-StatefulSet/PostgreSQL/base/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +# Set the default namespace for all the resources +namespace: codewizard + +# The files to be processed +# Kustomization will re-order the kinds if required +resources: + - ConfigMap.yaml + - Service.yaml + - PersistentVolumeClaim.yaml + - StatefulSet.yaml diff --git a/mkdocs-site/09-StatefulSet/PostgreSQL/kustomization.yaml b/mkdocs-site/09-StatefulSet/PostgreSQL/kustomization.yaml new file mode 100644 index 0000000..7ade65b --- /dev/null +++ b/mkdocs-site/09-StatefulSet/PostgreSQL/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +# Set the default namespace for all the resources +namespace: codewizard + +bases: + - base/ +resources: + - Namespace.yaml diff --git a/mkdocs-site/09-StatefulSet/PostgreSQL/testDB.sh b/mkdocs-site/09-StatefulSet/PostgreSQL/testDB.sh new file mode 100644 index 0000000..6c0f81f --- /dev/null +++ b/mkdocs-site/09-StatefulSet/PostgreSQL/testDB.sh @@ -0,0 +1,38 @@ +### Test to see if the StatefulSet "saves" the state of the pods + +# Programaticlly get the port and the IP +export CLUSTER_IP=$(kubectl get nodes \ + --selector=node-role.kubernetes.io/control-plane \ + -o jsonpath='{$.items[*].status.addresses[?(@.type=="InternalIP")].address}') + +export NODE_PORT=$(kubectl get \ + services postgres \ + -o jsonpath="{.spec.ports[0].nodePort}" \ + -n codewizard) + +export POSTGRES_DB=$(kubectl get \ + configmap postgres-config \ + -o jsonpath='{.data.POSTGRES_DB}' \ + -n codewizard) + +export POSTGRES_USER=$(kubectl get \ + configmap postgres-config \ + -o jsonpath='{.data.POSTGRES_USER}' \ + -n codewizard) + +export PGPASSWORD=$(kubectl get \ + configmap postgres-config \ + -o jsonpath='{.data.POSTGRES_PASSWORD}' \ + -n codewizard) + +# Echo check to see if we have all the required variables +printenv | grep POST* + +# Connect to postgres and create table if required. +# Once the table exists - add row into the table +psql \ + -U ${POSTGRES_USER} \ + -h ${CLUSTER_IP} \ + -d ${POSTGRES_DB} \ + -p ${NODE_PORT} \ + -c "CREATE TABLE IF NOT EXISTS stateful (str VARCHAR); INSERT INTO stateful values (1); SELECT count(*) FROM stateful" \ No newline at end of file diff --git a/mkdocs-site/09-StatefulSet/index.html b/mkdocs-site/09-StatefulSet/index.html new file mode 100644 index 0000000..f4802c3 --- /dev/null +++ b/mkdocs-site/09-StatefulSet/index.html @@ -0,0 +1,238 @@ + 09 StatefulSet - KubernetesLabs

K8S Hands-on

Visitor Badge


StatefulSets

The Difference Between a Statefulset And a Deployment

Stateless application

  • A stateless application is one that does not care which network it is using, and it does not need permanent storage and can be scaled up and down without the need to re-use the same network or persistence.
  • Deployment is the suitable kind for Stateless applications.
  • The most trivial example of stateless app is a Web Server.

Stateful application

  • Stateful applications are apps which in order to work properly need to use the same resources, such as network, storage etc.
  • Usually with Stateful applications you will need to ensure that pods can reach each other through a unique identity that does not change (e.g., hostnames, IP).
  • The most trivial example of Stateful app is a database of any kind.

Stateful Notes

  • Like a Deployment, a StatefulSet manages Pods that are based on an identical container spec.
  • Unlike a Deployment, a StatefulSet maintains a sticky identity for each of their Pods.
  • These pods are created from the same spec, but are not interchangeable: each has a persistent identifier that it maintains across any rescheduling.
  • Deleting and/or scaling down a StatefulSet will not delete the volumes associated with the StatefulSet. This is done to ensure data safety.
  • StatefulSet keeps a unique identity for each Pod and assign the same identity to those pods when they are rescheduled (update, restart etc).
  • The storage for a given Pod must either be provisioned by a PersistentVolume provisioner, based on the requested storage class, or pre-provisioned by an admin.
  • StatefulSet manages the deployment and scaling of a set of Pods, and provides guarantees about the ordering and uniqueness of these Pods.
  • A stateful app needs to use a dedicated storage.

Stable Network Identity

  • A Stateful application node must have a unique hostname and IP address so that other nodes in the same application know how to reach it.
  • A ReplicaSet assign a random hostname and IP address to each Pod. In such a case, we must use a service which exposes those Pods for us.

Start and Termination Order

  • Each StatefulSet follows this naming pattern: $(statefulSet name)-$(ordinal)
  • Stateful applications restarted or re-created, following the creation order.
  • A ReplicaSet does not follow a specific order when starting or killing its pods.

StatefulSet Volumes

  • StatefulSet does not create a volume for you.
  • When a StatefulSet is deleted, the respective volumes are not deleted with it.

To address all these requirements, Kubernetes offers the StatefulSet primitive.


Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window


01. Create namespace and clear previous data if there is any

# If the namespace already exist and contains data form previous steps, lets clean it
+kubectl delete namespace codewizard
+
+# Create the desired namespace [codewizard]
+$ kubectl create namespace codewizard
+namespace/codewizard created
+

02. Create and test the Stateful application

  • In order to deploy the Stateful set we will need the following resources:

  • ConfigMap

  • Service
  • StatefulSet
  • PersistentVolumeClaim orPersistentVolume`

  • All the resources including kustomization script are defined inside the base folder


apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: postgres-config
+  labels:
+    app: postgres
+data:
+  # The following names are the one defined in the officail postgres docs
+
+  # The name of the database we will use in this demo
+  POSTGRES_DB: codewizard
+  # the user name for this demo
+  POSTGRES_USER: codewizard
+  # The password for this demo
+  POSTGRES_PASSWORD: admin123
+
  • Service.yaml

    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: postgres
    +  labels:
    +    app: postgres
    +spec:
    +  selector:
    +    app: postgres
    +  # Service of type nodeport
    +  type: NodePort
    +  # The deafult port for postgres
    +  ports:
    +    - port: 5432
    +

  • PersistentVolumeClaim.yaml

kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+  name: postgres-pv-claim
+  labels:
+    app: postgres
+spec:
+  # in this demo we use GCP so we are using the 'standard' StorageClass
+  # We can of course define our own StorageClass resource
+  storageClassName: standard
+
+  # The access modes are:
+  #   ReadWriteOnce - The volume can be mounted as read-write by a single node
+  #   ReadWriteMany - The volume can be mounted as read-write by a many node
+  #   ReadOnlyMany  - The volume can be mounted as read-only  by many nodes
+  accessModes:
+    - ReadWriteMany
+  resources:
+    requests:
+      storage: 1Gi
+
apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: postgres
+spec:
+  replicas: 1
+  # StatefulSet must contain a serviceName
+  serviceName: postgres
+  selector:
+    matchLabels:
+      app: postgres # has to match .spec.template.metadata.labels
+  template:
+    metadata:
+      labels:
+        app: postgres # has to match .spec.selector.matchLabels
+    spec:
+      containers:
+        - name: postgres
+          image: postgres:10.4
+          imagePullPolicy: "IfNotPresent"
+          # The default DB port
+          ports:
+            - containerPort: 5432
+          # Load the required configuration env values form the configMap
+          envFrom:
+            - configMapRef:
+                name: postgres-config
+          # Use volume for storage
+          volumeMounts:
+            - mountPath: /var/lib/postgresql/data
+              name: postgredb
+      # We can use PersistentVolume or PersistentVolumeClaim.
+      # In this sample we are useing PersistentVolumeClaim
+      volumes:
+        - name: postgredb
+          persistentVolumeClaim:
+            # reference to Pre-Define PVC
+            claimName: postgres-pv-claim
+

Note: You can use the kustomization file to create or apply all the above resources

# Generate and apply the required resources using kustomization
+kubectl kustomize PostgreSQL/ | kubectl apply -f -
+

03. Test the Stateful application

  • Use the - testDB.sh to test the StatefulSet
  • Don’t forget to set the execution flag chmod +x testDb.sh if required
### Test to see if the StatefulSet "saves" the state of the pods
+
+# Programmatically get the port and the IP
+export CLUSTER_IP=$(kubectl get nodes \
+            --selector=node-role.kubernetes.io/control-plane \
+            -o jsonpath='{$.items[*].status.addresses[?(@.type=="InternalIP")].address}')
+
+export NODE_PORT=$(kubectl get \
+            services postgres \
+            -o jsonpath="{.spec.ports[0].nodePort}" \
+            -n codewizard)
+
+export POSTGRES_DB=$(kubectl get \
+            configmap postgres-config \
+            -o jsonpath='{.data.POSTGRES_DB}' \
+            -n codewizard)
+
+export POSTGRES_USER=$(kubectl get \
+            configmap postgres-config \
+            -o jsonpath='{.data.POSTGRES_USER}' \
+            -n codewizard)
+
+export PGPASSWORD=$(kubectl get \
+            configmap postgres-config \
+            -o jsonpath='{.data.POSTGRES_PASSWORD}' \
+            -n codewizard)
+
+# Check to see if we have all the required variables
+printenv | grep POST*
+
+# Connect to postgres and create table if required.
+# Once the table exists - add row into the table
+# you can run this command as amny times as you like
+psql \
+    -U ${POSTGRES_USER} \
+    -h ${CLUSTER_IP} \
+    -d ${POSTGRES_DB} \
+    -p ${NODE_PORT} \
+    -c "CREATE TABLE IF NOT EXISTS stateful (str VARCHAR); INSERT INTO stateful values (1); SELECT count(*) FROM stateful"
+

04. Scale down the StatefulSet and check that its down

04.01. Scale down the Statefulset to 0

# scale down the `Statefulset` to 0
+kubectl scale statefulset postgres -n codewizard --replicas=0
+

04.02. Verify that the pods Terminated

# Wait until the pods will be terminated
+kubectl get pods -n codewizard --watch
+NAME         READY   STATUS    RESTARTS   AGE
+postgres-0   1/1     Running   0          32m
+postgres-0   1/1     Terminating   0      32m
+postgres-0   0/1     Terminating   0      32m
+postgres-0   0/1     Terminating   0      33m
+postgres-0   0/1     Terminating   0      33m
+

04.03. Verify that the DB is not reachable

  • If the DB is not reachable it mean that all the pods are down
psql \
+    -U ${POSTGRES_USER} \
+    -h ${CLUSTER_IP} \
+    -d ${POSTGRES_DB} \
+    -p ${NODE_PORT} \
+    -c "SELECT count(*) FROM stateful"
+
+# You should get output similar to this one:
+psql: error: could not connect to server: Connection refused
+        Is the server running on host "192.168.49.2" and accepting
+        TCP/IP connections on port 32570?
+

05. Scale up again and verify that we still have the prevoius data

05.01. scale up the Statefulset to 1 or more

# scale up the `Statefulset`
+kubectl scale statefulset postgres -n codewizard --replicas=1
+

05.02. Verify that the pods is in Running status

kubectl get pods -n codewizard --watch
+NAME         READY   STATUS    RESTARTS   AGE
+postgres-0   1/1     Running   0          5s
+

05.03. Verify that the pods is using the previous data

psql \
+    -U ${POSTGRES_USER} \
+    -h ${CLUSTER_IP} \
+    -d ${POSTGRES_DB} \
+    -p ${NODE_PORT} \
+    -c "SELECT count(*) FROM stateful"
+# The output should be similar to this one
+
+ count
+-------
+     2
+(1 row)
+
\ No newline at end of file diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/AuthorizationPolicy.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/AuthorizationPolicy.yaml new file mode 100644 index 0000000..d8cc23d --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/AuthorizationPolicy.yaml @@ -0,0 +1,19 @@ +apiVersion: security.istio.io/v1beta1 +kind: AuthorizationPolicy +metadata: + name: authorization-policy +spec: + selector: + matchLabels: + app: webserver + rules: + - from: + - source: + principals: ["cluster.local/ns/default/sa/sleep"] + to: + - operation: + methods: ["GET"] + when: + - key: request.headers[version] + values: ["v1", "v2"] + diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/Namespace.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/Namespace.yaml new file mode 100644 index 0000000..9a4f7be --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/Namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: codewizard + labels: + istio-injection: enabled \ No newline at end of file diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/kiali-service.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/kiali-service.yaml new file mode 100644 index 0000000..67525f1 --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/kiali-service.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + meta.helm.sh/release-name: kiali-server + meta.helm.sh/release-namespace: istio-system + labels: + app: kiali + app.kubernetes.io/instance: kiali + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: kiali + app.kubernetes.io/part-of: kiali + name: kiali + namespace: istio-system +spec: + ipFamilyPolicy: SingleStack + ports: + - name: http + port: 20001 + protocol: TCP + targetPort: 20001 + - name: http-metrics + port: 9090 + protocol: TCP + targetPort: 9090 + selector: + app.kubernetes.io/instance: kiali + app.kubernetes.io/name: kiali + sessionAffinity: None + type: ClusterIP diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/kustomization.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/kustomization.yaml new file mode 100644 index 0000000..ca866cc --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/kustomization.yaml @@ -0,0 +1,16 @@ +# kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: codewizard + +resources: + #- AuthorizationPolicy.yaml + - kiali-service.yaml + - Namespace.yaml + - proxy-deployment.yaml + - proxy-service.yaml + - web-server1-deployment.yaml + - web-server1-service.yaml + - web-server2-deployment.yaml + - web-server2-service.yaml + - web-server2-v2-service.yaml diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/proxy-deployment.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/proxy-deployment.yaml new file mode 100644 index 0000000..331c352 --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/proxy-deployment.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: proxy-server +spec: + progressDeadlineSeconds: 60 + replicas: 1 + selector: + matchLabels: + app: proxy-server + template: + metadata: + labels: + app: proxy-server + version: v1 + spec: + containers: + - name: proxy-server + image: docker.io/nirgeier/istio-proxy-sample + imagePullPolicy: Always + ports: + - name: web + protocol: TCP + containerPort: 5050 + env: + - name: PROXY_URL_TO_SERVE + value: http://webserver + resources: + limits: + cpu: 100m + requests: + cpu: 100m diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/proxy-service.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/proxy-service.yaml new file mode 100644 index 0000000..abe7fa0 --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/proxy-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: proxy-service +spec: + ports: + # We use the - as istion require + - name: http-web + port: 80 + protocol: TCP + targetPort: 5050 + selector: + app: proxy-server + type: LoadBalancer diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/web-server1-deployment.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/web-server1-deployment.yaml new file mode 100644 index 0000000..acf06be --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/web-server1-deployment.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: webserver + version: v1 + name: webserverv1 +spec: + selector: + matchLabels: + app: webserver + version: v1 + strategy: + type: Recreate + template: + metadata: + labels: + app: webserver + version: v1 + spec: + containers: + - env: + - name: SERVER_NAME + value: WebServerV1 + image: docker.io/nirgeier/istio-web-server-sample + imagePullPolicy: Always + name: simpleserver + ports: + - containerPort: 5050 + name: web + protocol: TCP + resources: + limits: + cpu: 100m + requests: + cpu: 100m diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/web-server1-service.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/web-server1-service.yaml new file mode 100644 index 0000000..2676639 --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/web-server1-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: webserverv1 +spec: + ports: + - name: http-web + port: 80 + protocol: TCP + targetPort: 5050 + selector: + app: webserver + version: v1 diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/web-server2-deployment.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/web-server2-deployment.yaml new file mode 100644 index 0000000..ef1d34e --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/web-server2-deployment.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: webserver + version: v2 + name: webserverv2 +spec: + replicas: 3 + selector: + matchLabels: + app: webserver + version: v2 + strategy: + type: Recreate + template: + metadata: + labels: + app: webserver + version: v2 + spec: + affinity: {} + containers: + - env: + - name: SERVER_NAME + value: WebServerV2 + image: docker.io/nirgeier/istio-web-server-sample + imagePullPolicy: Always + name: simpleserver + ports: + - name: web + protocol: TCP + containerPort: 5050 + resources: + limits: + cpu: 100m + requests: + cpu: 100m diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/web-server2-service.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/web-server2-service.yaml new file mode 100644 index 0000000..e28d561 --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/web-server2-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: webserver +spec: + ports: + - name: http-web + port: 80 + protocol: TCP + targetPort: 5050 + selector: + app: webserver diff --git a/mkdocs-site/10-Istio/01-demo-services/K8S/web-server2-v2-service.yaml b/mkdocs-site/10-Istio/01-demo-services/K8S/web-server2-v2-service.yaml new file mode 100644 index 0000000..5c3011d --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/K8S/web-server2-v2-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: webserverv2 +spec: + ports: + - name: http-web + port: 80 + protocol: TCP + targetPort: 5050 + selector: + app: webserver + version: v2 diff --git a/mkdocs-site/10-Istio/01-demo-services/buildImages.sh b/mkdocs-site/10-Istio/01-demo-services/buildImages.sh new file mode 100644 index 0000000..4a064fe --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/buildImages.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Login to docker hub in order to push the images +docker login -u nirgeier + +# Build and push the images +docker-compose build && docker-compose push \ No newline at end of file diff --git a/mkdocs-site/10-Istio/01-demo-services/docker-compose.yaml b/mkdocs-site/10-Istio/01-demo-services/docker-compose.yaml new file mode 100644 index 0000000..6ff7d8c --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/docker-compose.yaml @@ -0,0 +1,16 @@ +version: "3" +services: + proxy: + build: istio-proxy + image: ${PROXY_IMAGE_NAME} + ports: + - ${PROXY_PORT}:${PROXY_PORT} + environment: + port: ${PROXY_PORT} + web-server: + build: istio-web-server + image: ${SERVER_IMAGE_NAME} + ports: + - ${SERVER_PORT}:${SERVER_PORT} + environment: + port: ${SERVER_PORT} diff --git a/mkdocs-site/10-Istio/01-demo-services/mock-data/external-mock1.txt b/mkdocs-site/10-Istio/01-demo-services/mock-data/external-mock1.txt new file mode 100644 index 0000000..f4902b6 --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/mock-data/external-mock1.txt @@ -0,0 +1 @@ +This is the content of external-mock1.txt \ No newline at end of file diff --git a/mkdocs-site/10-Istio/01-demo-services/mock-data/external-mock2.txt b/mkdocs-site/10-Istio/01-demo-services/mock-data/external-mock2.txt new file mode 100644 index 0000000..4786389 --- /dev/null +++ b/mkdocs-site/10-Istio/01-demo-services/mock-data/external-mock2.txt @@ -0,0 +1 @@ +This is the content of external-mock2.txt \ No newline at end of file diff --git a/mkdocs-site/10-Istio/02-network-fault/K8S/kustomization.yaml b/mkdocs-site/10-Istio/02-network-fault/K8S/kustomization.yaml new file mode 100644 index 0000000..0d6ce01 --- /dev/null +++ b/mkdocs-site/10-Istio/02-network-fault/K8S/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# We assume since we are build upon demo 01 - we already have the namespace +namespace: codewizard + +resources: + - resources/web-server1-network-faults.yaml + #- resources/web-server1-network-faults.yaml diff --git a/mkdocs-site/10-Istio/02-network-fault/K8S/resources/web-server1-VirtualService.yaml b/mkdocs-site/10-Istio/02-network-fault/K8S/resources/web-server1-VirtualService.yaml new file mode 100644 index 0000000..c5dfb58 --- /dev/null +++ b/mkdocs-site/10-Istio/02-network-fault/K8S/resources/web-server1-VirtualService.yaml @@ -0,0 +1,19 @@ +# +# Add network fault to 50% of the traffic +# We should get error like this one: +# >> Proxying reply: fault filter abort - Took 3 milliseconds +# +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: webserver + namespace: codewizard +spec: + # hostname of a request that this VirtualService resource will match + hosts: + - webserver + # rules to direct any HTTP traffic that matches the above hostname + http: + - route: + - destination: + host: webserverv1 \ No newline at end of file diff --git a/mkdocs-site/10-Istio/02-network-fault/K8S/resources/web-server1-network-faults.yaml b/mkdocs-site/10-Istio/02-network-fault/K8S/resources/web-server1-network-faults.yaml new file mode 100644 index 0000000..bd619e0 --- /dev/null +++ b/mkdocs-site/10-Istio/02-network-fault/K8S/resources/web-server1-network-faults.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +metadata: + name: webserver +spec: + hosts: + - webserver + http: + - route: + - destination: + host: webserverv1 + fault: + abort: + percentage: + value: 50 + httpStatus: 400 \ No newline at end of file diff --git a/mkdocs-site/10-Istio/index.html b/mkdocs-site/10-Istio/index.html new file mode 100644 index 0000000..e4b6537 --- /dev/null +++ b/mkdocs-site/10-Istio/index.html @@ -0,0 +1,125 @@ + 10 Istio - KubernetesLabs

K8S Hands-on

Visitor Badge


Istio

Istio is an open-source service mesh that provides a way to manage microservices traffic, security, and observability in a Kubernetes cluster.


Pre Requirements

Open in Cloud Shell

CTRL + click to open in new window


Istio and Kiali

  • This guide provides a detailed walkthrough on installing and configuring Istio and Kiali on a Kubernetes cluster.
  • We will also learn how to visualize your service mesh with Istio and Kiali and create a demo Istio VirtualService.

01. Introduction

Istio

  • Istio is an open-source service mesh that provides a way to manage microservices traffic, security, and observability in a Kubernetes cluster.
  • It acts as a layer of infrastructure that sits between your services, intercepting and controlling the traffic between them.
  • Istio key features:
Feature Description
Traffic Management Istio enables sophisticated traffic control capabilities, such as routing, load balancing, retries, timeouts, and circuit breakers for microservices.
Service Discovery Automatically discovers services in the mesh, enabling dynamic routing and management of microservices without the need for manual configuration.
Load Balancing Istio provides various load balancing algorithms (round-robin, weighted, etc.) to distribute traffic between microservices, ensuring optimal performance.
Traffic Shaping Allows fine-grained control of traffic between services, such as A/B testing, canary releases, or blue/green deployments by defining routing rules.
Fault Injection Supports fault injection to simulate network failures, latency, or errors in microservices to test resilience and robustness of the application.
Mutual TLS (mTLS) Istio can automatically encrypt traffic between services using mutual TLS (mTLS) to ensure secure communication and provide strong identity-based access control.
Authentication & Authorization Provides identity and access management through role-based access control (RBAC) and integration with external identity providers (e.g., OAuth, JWT).
Telemetry & Observability Istio collects metrics, logs, and traces for monitoring service’s performance and behavior. It integrates with tools like Prometheus, Grafana, and Jaeger.
Distributed Tracing Istio integrates with tracing systems like Jaeger and Zipkin to provide end-to-end tracing for debugging and monitoring service interactions.
Policy Enforcement Istio provides fine-grained control over traffic policies, such as rate limiting, quotas, and security policies, using its Policy and Telemetry components.
Resilience & Retries Istio can retry failed requests, set timeouts, and apply circuit breakers to prevent cascading failures and enhance the reliability of services.
Sidecar Proxy (Envoy) Istio uses Envoy as a sidecar proxy to intercept and manage network traffic, providing a transparent proxy between microservices.
Automatic Sidecar Injection Istio automatically injects the Envoy proxy into application pods via Kubernetes annotations, simplifying the management of service communication.
Service Mesh Topology Visualizes and manages the network of microservices, allowing users to monitor how services interact with each other and troubleshoot issues.
Canary Deployments Supports canary releases and traffic splitting, which allows gradual rollout of new versions of services for safe deployments and testing.
Multi-cluster Support Istio supports a multi-cluster environment, allowing you to deploy services across different Kubernetes clusters while maintaining a unified service mesh.
Integration with Existing Tools Istio integrates seamlessly with other tools such as Prometheus, Grafana, Jaeger, and Kiali for observability, monitoring, and tracing.
Service-Level Agreements (SLAs) Provides mechanisms to define service-level objectives (SLOs) and monitor them, ensuring services meet expected performance and reliability standards.
  • Istio core components:
Components Description
Envoy Proxy A sidecar proxy that intercepts traffic to and from microservices.
Pilot Manages configuration and distributes traffic management rules.
Mixer Provides policy enforcement and telemetry data collection.
Citadel Handles security-related tasks like identity and certificate management.

Kiali

  • Kiali is a graphical user interface (GUI) for Istio.
  • It helps you visualize the service mesh and provides insights on how microservices are interacting with each other.
  • Kiali integrates deeply with Istio, allowing you to view:

    • The service mesh topology, showing services, traffic flow, and dependencies.
    • Metrics like request rates, latencies, and error rates.
    • Distributed tracing (if enabled) for better debugging and troubleshooting.
    • Istio configuration to visualize resources like VirtualServices, DestinationRules, and more.
  • Kiali simplifies the operation of Istio by offering an intuitive way to manage and visualize your service mesh.


Part 01 - Installing Istio and Kiali

Step 01: Install Istio Using Istioctl

  • Istio supplies an installer
  • Go to the Istio releases page and download the latest version of Istio.
  • Alternatively, use istioctl to install Istio, as follows:
# Install Istio using istioctl
+echo  "Installing Istio..."
+curl  -L https://istio.io/downloadIstio | sh -
+cd    istio-*
+
+# Add bin directory to your $PATH
+export PATH=$PWD/bin:$PATH
+
+# Install istio with all features enabled (demo profile)
+istioctl install --set profile=demo -y
+

Step 02: Verify Istio installation

  • Check the Istio system components in the istio-system namespace:
# Verify Istio installation
+kubectl get pods -n istio-system
+
  • You should see several pods, including the Istio control plane components like:
    • istiod
    • istio-ingressgateway
    • istio-egressgateway
kubectl get pods -n istio-system
+
NAME                                    READY   STATUS    RESTARTS   AGE
+istio-egressgateway-684f5dc857-bzww6    1/1     Running   0          21m
+istio-ingressgateway-6b5bd79c5c-9n8tg   1/1     Running   0          21m
+istiod-68885d595-vv2ft                  1/1     Running   0          22m
+

Step 03: Install Kiali

  • We will install Kiali using Helm:
# Add the Kiali Helm chart repository
+helm repo add kiali https://kiali.org/helm-charts
+helm repo update
+
  • Install Kiali:
# Install Kiali into the `istio-system` namespace 
+# this is the default namespace for Istio components
+#
+# Install Kiali with anonymous authentication
+#
+helm install  kiali-server              \
+              kiali/kiali-server        \
+              --namespace istio-system  \
+              --set auth.strategy="anonymous"
+

Step 04: Verify Kiali installation

  • Once installed, check the status of Kiali:
kubectl get pods -n istio-system
+
  • You should see the Kiali pod running, along with the Istio components from previous step.
NAME                                    READY   STATUS    RESTARTS   AGE
+istio-egressgateway-684f5dc857-bzww6    1/1     Running   0          28m
+istio-ingressgateway-6b5bd79c5c-9n8tg   1/1     Running   0          28m
+istiod-68885d595-vv2ft                  1/1     Running   0          28m
+kiali-68ccc848b6-j4q28                  1/1     Running   0          27m
+

Part 02 - Viewing the Network with Istio

  • Istio uses a sidecar proxy model, where an envoy proxy is deployed alongside each microservice pod.
  • This proxy intercepts and manages traffic between the services.

Step 01: Enable Istio Injection

  • You need to enable Istio sidecar injection for your Kubernetes namespace.
  • This will ensure that new pods in the default namespace will automatically have the envoy proxy sidecar injected.
  • For example, to enable injection in the default namespace:
kubectl label namespace default istio-injection=enabled
+

Step 02: Deploy Sample Application

  • To see Istio in action, deploy a sample application, such as Bookinfo, which is available in Istio’s demo repository.
# Deploy the sample application supplied by istio
+kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml
+

Step 03: Verify the Sample Application

  • To check that the application pods are running, execute the following command:
kubectl get pods
+

Step 04: Expose the Application

  • To expose the application via Istio's ingress gateway, create an Istio Gateway and VirtualService .
# This will expose the 'Bookinfo' application to the external world via Istio ingress gateway.
+kubectl   apply -f \
+          samples/bookinfo/networking/bookinfo-gateway.yaml
+

Part 03 - Visualizing the Network with Kiali

Step 01: Access Kiali Dashboard

  • Once Kiali is installed, you can access it’s dashboard.
  • First, port-forward to the Kiali service:
kubectl   port-forward        \
+          -n istio-system     \
+          svc/kiali 20001:20001
+
  • Now, open your browser and go to http://localhost:20001
  • Username : admin
  • Password : (Leave blank if anonymous access is enabled)

Step 02: Explore the Service Mesh Topology

  • Once inside the Kiali dashboard, open the Mesh View
  • You will see a graph of your services in the mesh.
  • The graph shows the interactions between microservices, along with traffic flows, success/error rates, and latency.
  • You can use the Kiali interface to:
    • Zoom in/out of the topology.
    • View detailed metrics for each service.
    • Understand the traffic flow, including retries, timeouts, and error rates.

Part 04: Creating a Demo Istio VirtualService

  • In Istio, VirtualServices are used to define the routing rules for your services.

Step 01: Define a VirtualService

  • Create a VirtualService resource to route traffic to the ratings service in the Bookinfo demo app.
apiVersion: networking.istio.io/v1alpha3
+kind: VirtualService
+metadata:
+  name: ratings-vs
+  namespace: default
+spec:
+  hosts:
+    - ratings
+  http:
+    - route:
+        - destination:
+            host: ratings
+            subset: v2
+

Step 02: Apply the VirtualService

  • Apply the VirtualService.
  • This will route all traffic for the ratings service to version v2.
kubectl apply -f ratings-virtualservice.yaml
+

Step 03: Verify the Routing

  • You can use Kiali to visualize the traffic flow and verify that routing is happening as expected.
  • The Kiali dashboard should reflect the new route configuration for ratings.

Conclusion

  • You have now successfully installed Istio and Kiali, set up a service mesh, and visualized your network’s behavior.
  • The combination of Istio's powerful traffic management features and Kiali's intuitive visualization interface makes it easier to manage and monitor microservices in a Kubernetes cluster.
\ No newline at end of file diff --git a/mkdocs-site/10-Istio/install.sh b/mkdocs-site/10-Istio/install.sh new file mode 100644 index 0000000..1ca004b --- /dev/null +++ b/mkdocs-site/10-Istio/install.sh @@ -0,0 +1,107 @@ +#!/bin/bash + +# Step 1: Install Istio using istioctl +echo "Installing Istio..." +curl -L https://istio.io/downloadIstio | sh - +cd istio-* +export PATH=$PWD/bin:$PATH +istioctl install --set profile=demo -y + +# Step 2: Install Kiali using Helm +echo "Installing Kiali..." +helm repo add kiali https://kiali.org/helm-charts +helm repo update +helm install kiali-server kiali/kiali-server --namespace istio-system --set auth.strategy="anonymous" +helm install \ + --namespace kiali-operator \ + --create-namespace \ + kiali-operator \ + kiali/kiali-operator + + +# Step 3: Enable Istio sidecar injection for all namespaces +echo "Enabling Istio sidecar injection for default namespace..." +kubectl label namespace default istio-injection=enabled +kubectl label namespace codewizard istio-injection=enabled +kubectl label namespace monitoring istio-injection=enabled + + +# Step 4: Deploy the Bookinfo demo application +echo "Deploying Bookinfo sample app..." +kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml + +# Step 5: Expose the Bookinfo app through Istio gateway +echo "Exposing Bookinfo app through Istio gateway..." +kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml + +# Step 6: Apply VirtualService to route traffic to v2 of ratings +echo "Creating VirtualService for ratings..." +cat < 11 CRD Custom Resource Definition - KubernetesLabs

K8S Hands-on

Visitor Badge


Custom resources definition (CRD)

Intro

  • Custom resources definition (CRD) was added to Kubernetes 1.7.
  • CRD added the ability to define custom objects/resources.

What is a Custom Resource Definition(CRD)

  • A resource is an endpoint in the Kubernetes API that stores a collection of API objects of a certain kind; for example, the builtin pods resource contains a collection of Pod objects.

  • A custom resource is an extension of the Kubernetes API that is not necessarily available in a default Kubernetes installation. It represents a customization of a particular Kubernetes installation. However, many core Kubernetes functions are now built using custom resources, making Kubernetes more modular.

  • Custom resources can appear and disappear in a running cluster through dynamic registration, and cluster admins can update custom resources independently of the cluster itself.

  • Once a custom resource is installed, users can create and access its objects using kubectl, just as they do for built-in resources like Pods.

  • The custom resource created is also stored in the etcd cluster with proper replication and lifecycle management.

\ No newline at end of file diff --git a/mkdocs-site/11-CRD-Custom-Resource-Definition/resources/crd-object.yaml b/mkdocs-site/11-CRD-Custom-Resource-Definition/resources/crd-object.yaml new file mode 100644 index 0000000..b10f5bd --- /dev/null +++ b/mkdocs-site/11-CRD-Custom-Resource-Definition/resources/crd-object.yaml @@ -0,0 +1,10 @@ +# The apiVerison is taken from the crd defintion +# ./ +apiVersion: "codewizard.co.il/v1" +kind: "CodeWizardCRD" +metadata: + name: "codewizard-object" +spec: + crdSpec: "--" # String + image: "--" # String + replicas: 3 # Integer diff --git a/mkdocs-site/11-CRD-Custom-Resource-Definition/resources/crd.yaml b/mkdocs-site/11-CRD-Custom-Resource-Definition/resources/crd.yaml new file mode 100644 index 0000000..c32618b --- /dev/null +++ b/mkdocs-site/11-CRD-Custom-Resource-Definition/resources/crd.yaml @@ -0,0 +1,53 @@ +# The required apiVersion for the CRD +apiVersion: apiextensions.k8s.io/v1 + +# The Kind is: 'CustomResourceDefinition' +kind: CustomResourceDefinition +metadata: + # name must match the spec fields below, and be in the form: . + # In this sample we define the name & group: + # Refer to the `spec.names` below + name: custom-crd.codewizard.co.il +spec: + # The CRD can be applied to either Namespaced or Cluster + # In this case we set it to Name + scope: Namespaced + + # group name to use for REST API: /apis// + # same group as defined under `metadata.name` + group: codewizard.co.il + + names: + # plural name to be used in the URL: /apis/// + plural: custom-crd + # singular name to be used as an alias on the CLI and for display + singular: cwcrd + # kind is normally the CamelCased singular type. Your resource manifests use this. + kind: CodeWizardCRD + # shortNames allow shorter string to match your resource on the CLI + shortNames: + - cwcrd + + # list of versions supported by this CustomResourceDefinition + versions: + - name: v1 + # Each version can be enabled/disabled by Served flag. + served: true + # One and only one version must be marked as the storage version. + storage: true + schema: + openAPIV3Schema: + required: ["spec"] + type: object + # The properties which be defined under the `spec` + properties: + spec: + type: object + # The properties which can be defined and their type + properties: + crdSpec: + type: string + image: + type: string + replicas: + type: integer diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/Namespace.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/Namespace.yaml new file mode 100644 index 0000000..3f3c716 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/Namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: wp-demo diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/all.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/all.yaml new file mode 100644 index 0000000..edbdcf0 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/all.yaml @@ -0,0 +1,156 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: wp-demo +--- +apiVersion: v1 +data: + mysql-password.txt: bm90MkhhcmQyR3Vlc3M= +kind: Secret +metadata: + name: mysql-pass-8ttc4k2t5f + namespace: wp-demo +type: Opaque +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: wordpress + tier: mysql + name: mysql + namespace: wp-demo +spec: + clusterIP: None + ports: + - port: 3306 + selector: + app: wordpress + tier: mysql +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: wordpress + name: wordpress + namespace: wp-demo +spec: + ports: + - port: 8089 + selector: + app: wordpress + tier: frontend + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: wordpress + tier: mysql + name: mysql + namespace: wp-demo +spec: + selector: + matchLabels: + app: wordpress + tier: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: wordpress + tier: mysql + spec: + containers: + - env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + key: mysql-password.txt + name: mysql-pass-8ttc4k2t5f + image: mysql:5.6 + name: mysql + ports: + - containerPort: 3306 + name: mysql + volumeMounts: + - mountPath: /var/lib/mysql + name: mysql-persistent-storage + volumes: + - name: mysql-persistent-storage + persistentVolumeClaim: + claimName: mysql-wordpress +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: wordpress + name: wordpress + namespace: wp-demo +spec: + selector: + matchLabels: + app: wordpress + tier: frontend + strategy: + type: Recreate + template: + metadata: + labels: + app: wordpress + tier: frontend + spec: + containers: + - env: + - name: WORDPRESS_DB_HOST + value: mysql + - name: WORDPRESS_DB_PASSWORD + valueFrom: + secretKeyRef: + key: mysql-password.txt + name: mysql-pass-8ttc4k2t5f + image: wordpress:4.8-apache + name: wordpress + ports: + - containerPort: 80 + name: wordpress + volumeMounts: + - mountPath: /var/www/html + name: wordpress-persistent-storage + volumes: + - name: wordpress-persistent-storage + persistentVolumeClaim: + claimName: wp-pv-claim +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: mysql-wordpress + tier: mysql + name: mysql-pvc + namespace: wp-demo +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: wordpress + name: wp-pv-claim + namespace: wp-demo +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/index.html b/mkdocs-site/12-Wordpress-MySQL-PVC/index.html new file mode 100644 index 0000000..64c04f6 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/index.html @@ -0,0 +1,78 @@ + 12 Wordpress MySQL PVC - KubernetesLabs

K8S Hands-on

Visitor Badge


WordPress, MySQL, PVC

  • In this tutorial you will deploy a WordPress site and a MySQL database.
  • You will use PersistentVolumes and PersistentVolumeClaims as storage.

Walkthrough

  • Patch minikube so we can use Service: LoadBalancer
# Source:
+# https://github.com/knative/serving/blob/b31d96e03bfa1752031d0bc4ae2a3a00744d6cd5/docs/creating-a-kubernetes-cluster.md#loadbalancer-support-in-minikube
+
+sudo ip route add \
+    $(cat ~/.minikube/profiles/minikube/config.json | \
+    jq -r ".KubernetesConfig.ServiceCIDR") \
+    via $(minikube ip)
+
+kubectl run minikube-lb-patch \
+    --replicas=1 \
+    --image=elsonrodriguez/minikube-lb-patch:0.1 \--namespace=kube-system
+
  • Create the desired Namespace
  • Create the MySQL resources:
    • Create Service
    • Create PersistentVolumeClaims
    • Create Deployment
    • Create password file
  • Create the WordPress resources:
    • Create Service
    • Create PersistentVolumeClaims
    • Create Deployment
  • Create a kustomization.yaml with:
    • Secret generator
    • MySQL resources
    • WordPress resources
  • Deploy the stack
  • Port forward from the host to the application
  • We use a port forward so we will be able to test and verify if the WordPress is actually running:
kubectl port-forward service/wordpress 8080:32267 -n wp-demo
+
\ No newline at end of file diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/kustomization.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/kustomization.yaml new file mode 100644 index 0000000..a70028a --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/kustomization.yaml @@ -0,0 +1,20 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +# Set the default namespace for all the resources +namespace: wp-demo + +# The files to be processed +# Kustomization will re-order the kinds if required +bases: + - /resources/MySQL + - /resources/Wordpress + +resources: + - Namespace.yaml + +# kubectl expose rc \ +# example \ +# --port=8765 \ +# --target-port=9376 \ +# -name=example-service \ +# --type=LoadBalancer \ No newline at end of file diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/original/all.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/original/all.yaml new file mode 100644 index 0000000..21d9b82 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/original/all.yaml @@ -0,0 +1,133 @@ +apiVersion: v1 +kind: Service +metadata: + name: wordpress-mysql + labels: + app: wordpress +spec: + ports: + - port: 3306 + selector: + app: wordpress + tier: mysql + clusterIP: None +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pv-claim + labels: + app: wordpress +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wordpress-mysql + labels: + app: wordpress +spec: + selector: + matchLabels: + app: wordpress + tier: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: wordpress + tier: mysql + spec: + containers: + - image: mysql:5.6 + name: mysql + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-pass + key: password + ports: + - containerPort: 3306 + name: mysql + volumeMounts: + - name: mysql-persistent-storage + mountPath: /var/lib/mysql + volumes: + - name: mysql-persistent-storage + persistentVolumeClaim: + claimName: mysql-pv-claim +--- +apiVersion: v1 +kind: Service +metadata: + name: wordpress + labels: + app: wordpress +spec: + ports: + - port: 80 + selector: + app: wordpress + tier: frontend + type: LoadBalancer +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: wp-pv-claim + labels: + app: wordpress +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wordpress + labels: + app: wordpress +spec: + selector: + matchLabels: + app: wordpress + tier: frontend + strategy: + type: Recreate + template: + metadata: + labels: + app: wordpress + tier: frontend + spec: + containers: + - image: wordpress:4.8-apache + name: wordpress + env: + - name: WORDPRESS_DB_HOST + value: wordpress-mysql + - name: WORDPRESS_DB_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-pass + key: password + ports: + - containerPort: 80 + name: wordpress + volumeMounts: + - name: wordpress-persistent-storage + mountPath: /var/www/html + volumes: + - name: wordpress-persistent-storage + persistentVolumeClaim: + claimName: wp-pv-claim diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/original/kustomization.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/original/kustomization.yaml new file mode 100644 index 0000000..70f4574 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/original/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +secretGenerator: +- name: mysql-pass + literals: + - password=YOUR_PASSWORD + +resources: + - all.yaml diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/original/wp.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/original/wp.yaml new file mode 100644 index 0000000..5b8d79e --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/original/wp.yaml @@ -0,0 +1,141 @@ +apiVersion: v1 +data: + password: WU9VUl9QQVNTV09SRA== +kind: Secret +metadata: + name: mysql-pass-c57bb4t7mf +type: Opaque +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: wordpress + name: wordpress-mysql +spec: + clusterIP: None + ports: + - port: 3306 + selector: + app: wordpress + tier: mysql +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: wordpress + name: wordpress +spec: + ports: + - port: 80 + selector: + app: wordpress + tier: frontend + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: wordpress + name: wordpress-mysql +spec: + selector: + matchLabels: + app: wordpress + tier: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: wordpress + tier: mysql + spec: + containers: + - env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: mysql-pass-c57bb4t7mf + image: mysql:5.6 + name: mysql + ports: + - containerPort: 3306 + name: mysql + volumeMounts: + - mountPath: /var/lib/mysql + name: mysql-persistent-storage + volumes: + - name: mysql-persistent-storage + persistentVolumeClaim: + claimName: mysql-pv-claim +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: wordpress + name: wordpress +spec: + selector: + matchLabels: + app: wordpress + tier: frontend + strategy: + type: Recreate + template: + metadata: + labels: + app: wordpress + tier: frontend + spec: + containers: + - env: + - name: WORDPRESS_DB_HOST + value: wordpress-mysql + - name: WORDPRESS_DB_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: mysql-pass-c57bb4t7mf + image: wordpress:4.8-apache + name: wordpress + ports: + - containerPort: 80 + name: wordpress + volumeMounts: + - mountPath: /var/www/html + name: wordpress-persistent-storage + volumes: + - name: wordpress-persistent-storage + persistentVolumeClaim: + claimName: wp-pv-claim +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: wordpress + name: mysql-pv-claim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: wordpress + name: wp-pv-claim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/01-mysql-service.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/01-mysql-service.yaml new file mode 100644 index 0000000..3fd9cf8 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/01-mysql-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: mysql + labels: + app: wordpress + tier: mysql +spec: + ports: + - port: 3306 + selector: + app: wordpress + tier: mysql + clusterIP: None diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/02-mysql-pvc.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/02-mysql-pvc.yaml new file mode 100644 index 0000000..5ea5d4b --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/02-mysql-pvc.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pvc + labels: + app: mysql-wordpress + tier: mysql +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/03-mysql-deployment.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/03-mysql-deployment.yaml new file mode 100644 index 0000000..2aa8e37 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/03-mysql-deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql + labels: + app: wordpress + tier: mysql +spec: + selector: + matchLabels: + app: wordpress + tier: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: wordpress + tier: mysql + spec: + containers: + - image: mysql:5.6 + name: mysql + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-pass + key: mysql-password.txt + ports: + - containerPort: 3306 + name: mysql + volumeMounts: + - name: mysql-persistent-storage + mountPath: /var/lib/mysql + volumes: + - name: mysql-persistent-storage + persistentVolumeClaim: + claimName: mysql-wordpress diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/kustomization.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/kustomization.yaml new file mode 100644 index 0000000..188b953 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/kustomization.yaml @@ -0,0 +1,16 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Generate the DB password from the given file +secretGenerator: + - name: mysql-pass + behavior: create + files: + - mysql-password.txt + +# The files to be processed +# Kustomization will re-order the kinds if required +resources: + - 01-mysql-service.yaml + - 02-mysql-pvc.yaml + - 03-mysql-deployment.yaml diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/mysql-password.txt b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/mysql-password.txt new file mode 100644 index 0000000..4cf31b1 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/MySQL/mysql-password.txt @@ -0,0 +1 @@ +not2Hard2Guess \ No newline at end of file diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/01-wordpress-service.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/01-wordpress-service.yaml new file mode 100644 index 0000000..cde0830 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/01-wordpress-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: wordpress + labels: + app: wordpress +spec: + ports: + - port: 8089 + selector: + app: wordpress + tier: frontend + type: LoadBalancer \ No newline at end of file diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/02-wordpress-pvc.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/02-wordpress-pvc.yaml new file mode 100644 index 0000000..7b3943d --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/02-wordpress-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: wp-pv-claim + labels: + app: wordpress +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi \ No newline at end of file diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/03-wordpress-deployment.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/03-wordpress-deployment.yaml new file mode 100644 index 0000000..27dd25e --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/03-wordpress-deployment.yaml @@ -0,0 +1,41 @@ + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wordpress + labels: + app: wordpress +spec: + selector: + matchLabels: + app: wordpress + tier: frontend + strategy: + type: Recreate + template: + metadata: + labels: + app: wordpress + tier: frontend + spec: + containers: + - image: wordpress:4.8-apache + name: wordpress + env: + - name: WORDPRESS_DB_HOST + value: mysql + - name: WORDPRESS_DB_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-pass + key: mysql-password.txt + ports: + - containerPort: 80 + name: wordpress + volumeMounts: + - name: wordpress-persistent-storage + mountPath: /var/www/html + volumes: + - name: wordpress-persistent-storage + persistentVolumeClaim: + claimName: wp-pv-claim diff --git a/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/kustomization.yaml b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/kustomization.yaml new file mode 100644 index 0000000..ab41c43 --- /dev/null +++ b/mkdocs-site/12-Wordpress-MySQL-PVC/resources/WordPress/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# The files to be processed +# Kustomization will re-order the kinds if required +resources: + - 01-wordpress-service.yaml + - 02-wordpress-pvc.yaml + - 03-wordpress-deployment.yaml + + diff --git a/mkdocs-site/13-HelmChart/codewizard-helm-demo-0.1.0.tgz b/mkdocs-site/13-HelmChart/codewizard-helm-demo-0.1.0.tgz new file mode 100644 index 0000000..d18f3ff Binary files /dev/null and b/mkdocs-site/13-HelmChart/codewizard-helm-demo-0.1.0.tgz differ diff --git a/mkdocs-site/13-HelmChart/codewizard-helm-demo/Chart.yaml b/mkdocs-site/13-HelmChart/codewizard-helm-demo/Chart.yaml new file mode 100644 index 0000000..da1b69f --- /dev/null +++ b/mkdocs-site/13-HelmChart/codewizard-helm-demo/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v2 +name: codewizard-helm-demo +description: A Helm chart demo for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. +# They're included as a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. +# +# Note: +# Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. +# In this demo we deploy nginx so we get the version from DockerHub +# https://hub.docker.com/_/nginx +appVersion: 1.19.7 \ No newline at end of file diff --git a/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/ConfigMap.yaml b/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/ConfigMap.yaml new file mode 100644 index 0000000..8554b0e --- /dev/null +++ b/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/ConfigMap.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "webserver.fullname" . }}-config + namespace: codewizard +data: + nginx.conf: |- + server { + listen 80; + server_name localhost; + + location / { + return 200 '{{ .Values.nginx.conf.message }}\n'; + add_header Content-Type text/plain; + } + + location /release/name { + return 200 '{{ .Release.Name }}\n'; + add_header Content-Type text/plain; + } + + location /release/revision { + return 200 '{{ .Release.Revision }}\n'; + add_header Content-Type text/plain; + } + } diff --git a/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/Deployment.yaml b/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/Deployment.yaml new file mode 100644 index 0000000..febf127 --- /dev/null +++ b/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/Deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "webserver.fullname" . }} + namespace: codewizard + labels: + {{- include "webserver.labels" . | nindent 4 }} +spec: +{{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} +{{- end }} + selector: + matchLabels: + {{- include "webserver.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/ConfigMap.yaml") . | sha256sum }} + labels: + {{- include "webserver.selectorLabels" . | nindent 8 }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + volumeMounts: + - name: nginx-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: nginx.conf + ports: + - name: http + containerPort: 80 + protocol: TCP + volumes: + - name: nginx-config + configMap: + name: {{ include "webserver.fullname" . }}-config \ No newline at end of file diff --git a/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/Namespace.yaml b/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/Namespace.yaml new file mode 100644 index 0000000..af12597 --- /dev/null +++ b/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/Namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: codewizard \ No newline at end of file diff --git a/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/Service.yaml b/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/Service.yaml new file mode 100644 index 0000000..2c86eec --- /dev/null +++ b/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/Service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "webserver.fullname" . }} + namespace: codewizard + labels: + {{- include "webserver.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "webserver.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/_helpers.tpl b/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/_helpers.tpl new file mode 100644 index 0000000..3e7f945 --- /dev/null +++ b/mkdocs-site/13-HelmChart/codewizard-helm-demo/templates/_helpers.tpl @@ -0,0 +1,58 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Note: + We truncate at 63 chars where required, + because some Kubernetes name fields are + limited to this length (by the DNS naming spec). +*/}} + +{{/* ............ Templates section ............ */}} + +{{/* Define the name of this demo webserver */}} +{{- define "webserver.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* + Create a default fully qualified app name. + If release name contains chart name it will be used as a full name. +*/}} +{{- define "webserver.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "webserver.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "webserver.labels" -}} +helm.sh/chart: {{ include "webserver.chart" . }} +{{ include "webserver.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "webserver.selectorLabels" -}} +app.kubernetes.io/name: {{ include "webserver.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} \ No newline at end of file diff --git a/mkdocs-site/13-HelmChart/codewizard-helm-demo/values.yaml b/mkdocs-site/13-HelmChart/codewizard-helm-demo/values.yaml new file mode 100644 index 0000000..935cbcd --- /dev/null +++ b/mkdocs-site/13-HelmChart/codewizard-helm-demo/values.yaml @@ -0,0 +1,17 @@ +replicaCount: 2 + +image: + repository: nginx + pullPolicy: IfNotPresent + tag: "" + +service: + type: ClusterIP + port: 80 + +autoscaling: + enabled: false + +nginx: + conf: + message: "CodeWizard Helm Demo" diff --git a/mkdocs-site/13-HelmChart/index.html b/mkdocs-site/13-HelmChart/index.html new file mode 100644 index 0000000..8ef5903 --- /dev/null +++ b/mkdocs-site/13-HelmChart/index.html @@ -0,0 +1,132 @@ + 13 HelmChart - KubernetesLabs

K8S Hands-on

Visitor Badge


Helm Chart

  • Welcome to the Helm Chart hands-on lab! In this tutorial, you’ll learn the essentials of Helm (version 3), the package manager for Kubernetes.
  • You’ll build, package, install, and manage applications using Helm charts, gaining practical experience with real Kubernetes resources.

Pre requirements

Open in Cloud Shell

CTRL + click to open in new window


What will you learn

  • What Helm is and why is it useful
  • Helm chart structure and key files
  • Common Helm commands for managing releases
  • How to create, pack, install, upgrade, and rollback a Helm chart
  • Troubleshooting and best practices

Introduction

  • Helm is the package manager for Kubernetes.
  • It simplifies the deployment, management, and upgrade of applications on your Kubernetes cluster.
  • Helm helps you manage Kubernetes applications by providing a way to define, install, and upgrade complex Kubernetes applications.
  • When packing applications as Helm charts, you gain a standardized and reusable approach for deploying and managing your services.

  • A Helm chart consists of a few files that define the Kubernetes resources that will be created when the chart is installed.

  • These files include the:
    • Chart.yaml file, which contains metadata about the chart, such as its name and version, and the chart’s dependencies and maintainers.
    • values.yaml file, which contains the configuration values for the chart.
    • The templates directory which contains the Kubernetes resource templates to be used to create the actual resources in the cluster.

Terminology

  • Chart

    • A Helm package is called a chart.
    • Charts are versioned, shareable packages that contain all the Kubernetes resources needed to run an application.
  • Release

    • A specific instance of a chart is called a release.
    • Each release is a deployed version of a chart, with its own configuration, resources, and revision history.
  • Repository

    • A collection of charts is stored in a Helm repository.
    • Helm charts can be hosted in public or private repositories for easy sharing and distribution.

Chart files and folders

Filename/Folder Description
Chart.yaml Contains metadata about the chart, including its name, version, dependencies, and maintainers.
values.yaml Defines default configuration values for the chart. Users can override these values during installation.
templates/ Directory containing Kubernetes manifest templates written in the Go template language.
charts/ Directory containing dependencies of the chart.
README.md Documentation for the chart, explaining how to use and configure it.
codewizard-helm-demo Helm Chart tructure
- Chart.yaml        # Defines chart metadata and values schema
+- values.yaml       # Default configuration values
+- templates/        # Deployment templates using Go templating language
+  - deployment.yaml # Deployment manifest template
+  - service.yaml    # Service manifest template
+- README.md         # Documentation for your chart 
+

Common Helm Commands

Here are some of the most common Helm commands you’ll use when working with Helm charts:

Command Description
helm create chart-name Create a new Helm chart with the specified name.
helm install release-name chart-path Install a Helm chart to your Kubernetes cluster.
helm upgrade release-name chart-path Upgrade an installed release with a new version of a chart.
helm uninstall release-name Uninstall a release from the Kubernetes cluster.
helm list List all installed Helm releases in the cluster.
helm status release-name Show the status of a deployed Helm release.
helm rollback release-name revision Rollback a release to a previous revision.
helm get all release-name Retrieve all information about a deployed release (e.g., templates, values).
helm show values chart-name Show the default values of a Helm chart.
helm template chart-name Generate the output of the Helm chart.
helm lint chart-path This command takes a path to a chart and runs a series of tests to verify that the chart is well-formed.
helm history chart-name This command takes a path to a chart and runs a series of tests to verify that the chart is well-formed.

Lab

Step 01 - Installing Helm

  • Before you can use the codewizard-helm-demo chart, you’ll need to install Helm on your local machine.

  • Helm install methods by OS:

OS Command
Linux curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 \| bash
MacOS brew install helm
Windows (via Chocolatey) choco install kubernetes-helm

Verify Installation

  • To confirm that Helm is installed correctly, run:
$ helm version
+
+## Expected output
+version.BuildInfo{Version:"xx", GitCommit:"xx", GitTreeState:"clean", GoVersion:"xx"}
+

Step 02 - Creating our Helm chart

  • Creating our custom codewizard-helm-demo Helm chart
  • The custom codewizard-helm-demo Helm chart is build upon the following K8S resources:

    • ConfigMap
    • Deployment
    • Service
  • As mentioned above, we will also have the following Helm resources:

    • Chart.yaml
    • values.yaml
    • templates/_helpers.tpl

Create a New Chart

  • First, we need to create a Helm chart using the helm create command.
  • This command will generate the necessary file structure for your new chart.
helm create codewizard-helm-demo
+
What is the result of this command?

Examine the chart structure!

cd codewizard-helm-demo
+

Write the chart content

  • Copy the content of the chart folder (in this lab) to the chart directory (overwriting the files).

Step 03 - Pack the chart

  • After we have created or customized our chart, we need to pack it as .tgz file, which can then be shared or installed.

helm package

Helm Package

helm package packages a chart into a versioned chart archive file.
If a path is given, this will “look” at that path for a chart which must contain a Chart.yaml file and then pack that directory.

helm package codewizard-helm-demo
+
  • This command will create a file called codewizard-helm-demo-<version>.tgz inside your current directory.

Step 04 - Validate the chart content

helm template

  • Helm allows you to generate the Kubernetes manifests based on the templates and values files without actually installing the chart.
  • This is useful to preview what the generated resources will look like:
helm template codewizard-helm-demo
+
+## This will output the rendered Kubernetes manifests to your terminal
+

Step 05 - Install the chart

  • Install the codewizard-helm-demo chart into Kubernetes cluster

The helm install command

  • This command installs a chart archive.
  • The install argument must be a chart reference, a path to a packed chart, a path to an unpacked chart directory or a URL.
  • To override values in a chart, use:
    • --values - pass in a file
    • --set - pass configuration from the command line
# Install the packed helm chart
+helm install codewizard-helm-demo codewizard-helm-demo-0.1.0.tgz
+

Step 06 - Verify the installation

  • Examine newly created Helm chart release, and all cluster created resources:
# List the installed helms
+helm ls
+
+# Check the resources
+kubectl get all -n codewizard
+

Step 07 - Test the service

  • Perform an HTTP GET request, send it to the newly created cluster service.
  • Confirm that the response contains the CodeWizard Helm Demo message passed from the values.yaml file.
kubectl run busybox         \
+        --image=busybox     \
+        --rm                \
+        -it                 \
+        --restart=Never     \
+        -- /bin/sh -c "wget -qO- http://codewizard-helm-demo.codewizard.svc.cluster.local"
+
+### Output: 
+CodeWizard Helm Demo
+

Step 08 - Upgrade the release to newer version

  • Perform a Helm upgrade on the codewizard-helm-demo release:
# upgrade and pass a different message than the one from the default values
+# Use the --set to pass the desired value
+helm  upgrade \
+  codewizard-helm-demo \
+  codewizard-helm-demo-0.1.0.tgz \
+  --set nginx.conf.message="Helm Rocks"
+

Step 09 - Check the upgrade

  • Perform another HTTP GET request.
  • Confirm that the response now has the updated message Helm Rocks:
kubectl run busybox         \
+        --image=busybox     \
+        --rm                \
+        -it                 \
+        --restart=Never     \
+        -- /bin/sh -c "wget -qO- http://codewizard-helm-demo.codewizard.svc.cluster.local"
+
+### Output: 
+Helm Rocks
+

Step 10 - History

  • Examine the codewizard-helm-demo release history

helm history

  • helm history prints historical revisions for a given release.
  • A default maximum of 256 revisions will be returned.
$ helm history codewizard-helm-demo
+
+### Sample output
+REVISION        UPDATED    STATUS          CHART                           APP VERSION     DESCRIPTION     
+1               ...        superseded      codewizard-helm-demo-0.1.0      1.19.7          Install complete
+2               ...        deployed        codewizard-helm-demo-0.1.0      1.19.7          Upgrade complete
+

Step 11 - Rollback

helm rollback

  • Rollback the codewizard-helm-demo release to previous version:
$ helm rollback codewizard-helm-demo
+
+### Output:
+Rollback was a success! Happy Helming!
+
  • Check again to verify that you get the original message!

Finalize & Cleanup

  • To remove all resources created by this lab, uninstall the codewizard-helm-demo release:
helm uninstall codewizard-helm-demo
+
  • (Optional) If you have created a dedicated namespace for this lab, you can delete it by runniung:
kubectl delete namespace codewizard
+

Troubleshooting

  • Helm not found:

Make sure Helm is installed and available in your PATH. Run the following to verify:

helm version
+


  • Pods not starting:

Check pod status and logs by running the following commands:

kubectl get pods -n codewizard
+kubectl describe pod <pod-name> -n codewizard
+kubectl logs <pod-name> -n codewizard
+


  • Service not reachable:

Ensure the service and pods are running by running the following commands:

kubectl get svc -n codewizard
+kubectl get pods -n codewizard
+


  • Values not updated after upgrade:

Double-check your --set or --values flags and confirm the upgrade by running:

helm get values codewizard-helm-demo
+

Next Steps

  • Try creating your own Helm chart for a different application.
  • Explore Helm chart repositories like Artifact Hub.
  • Learn about advanced Helm features, such as: dependencies, hooks, and chart testing.
  • Integrate Helm with CI/CD pipelines for automated deployments.
  • Read more in the official Helm documentation.
\ No newline at end of file diff --git a/mkdocs-site/13-HelmChart/install.sh b/mkdocs-site/13-HelmChart/install.sh new file mode 100644 index 0000000..44837a9 --- /dev/null +++ b/mkdocs-site/13-HelmChart/install.sh @@ -0,0 +1,22 @@ +# Remove old chart if its already exists +helm uninstall codewizard-helm-demo +sleep 10 + +# Pack the Helm in the desired folder +helm package codewizard-helm-demo + +# install the helm and view the output +helm install codewizard-helm-demo codewizard-helm-demo-0.1.0.tgz + +sleep 10 +# verify that the chart installed +kubectl get all -n codewizard + +# Check the response from the chart +kubectl delete pod busybox --force --grace-period=0 2&>/dev/null +kubectl run busybox \ + --image=busybox \ + --rm \ + -it \ + --restart=Never \ + -- /bin/sh -c "wget -qO- http://codewizard-helm-demo.codewizard.svc.cluster.local" diff --git a/mkdocs-site/14-Logging/fluentd-configuration/ConfigMap.yaml b/mkdocs-site/14-Logging/fluentd-configuration/ConfigMap.yaml new file mode 100644 index 0000000..f58f59d --- /dev/null +++ b/mkdocs-site/14-Logging/fluentd-configuration/ConfigMap.yaml @@ -0,0 +1,42 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: fluentd-conf + namespace: kube-system +data: + kubernetes.conf: " + + @type stdout + + + @type null + + + + @type file + path /var/log/fluent/docker.log + time_slice_format %Y%m%d + time_slice_wait 10m + time_format %Y%m%dT%H%M%S%z + compress gzip + utc + + + + @type tail + @id in_tail_container_logs + path /var/log/containers/*.log + pos_file /var/log/fluentd-containers.log.pos + tag kubernetes.* + read_from_head true + <% if is_v1 %> + + @type json + time_format %Y-%m-%dT%H:%M:%S.%NZ + + <% else %> + format json + time_format %Y-%m-%dT%H:%M:%S.%NZ + <% end %> + +" diff --git a/mkdocs-site/14-Logging/fluentd-configuration/kubernetes.conf b/mkdocs-site/14-Logging/fluentd-configuration/kubernetes.conf new file mode 100644 index 0000000..c538e92 --- /dev/null +++ b/mkdocs-site/14-Logging/fluentd-configuration/kubernetes.conf @@ -0,0 +1,32 @@ + + @type stdout + + + @type null + + + @type file + path /var/log/fluent/docker.log + time_slice_format %Y%m%d + time_slice_wait 10m + time_format %Y%m%dT%H%M%S%z + compress gzip + utc + + + @type tail + @id in_tail_container_logs + path /var/log/containers/*.log + pos_file /var/log/fluentd-containers.log.pos + tag kubernetes.* + read_from_head true +<% if is_v1 %> + + @type json + time_format %Y-%m-%dT%H:%M:%S.%NZ + +<% else %> + format json + time_format %Y-%m-%dT%H:%M:%S.%NZ +<% end %> + \ No newline at end of file diff --git a/mkdocs-site/14-Logging/index.html b/mkdocs-site/14-Logging/index.html new file mode 100644 index 0000000..a785d4c --- /dev/null +++ b/mkdocs-site/14-Logging/index.html @@ -0,0 +1,86 @@ + 14 Logging - KubernetesLabs

K8S Hands-on

Visitor Badge


Logging

  • Welcome to the Logging hands-on lab! In this tutorial, we will learn the essentials of Logging in Kubernetes clusters.
  • We will deploy a sample application, configure log collection, and explore logs using popular tools like Fluentd, Elasticsearch, and Kibana (EFK stack).

Pre requirements

Open in Cloud Shell

CTRL + click to open in new window


What will we learn?

  • Why Logging is important in Kubernetes
  • How to deploy a sample app that generates logs
  • How to collect logs using Fluentd
  • How to store and search logs with Elasticsearch
  • How to visualize logs with Kibana
  • Troubleshooting and best practices

Introduction

  • Logging is critical for monitoring, debugging, and auditing applications in Kubernetes.
  • Kubernetes does not provide a builtin, centralized Logging solution, but it allows us to integrate with many Logging stacks.
  • We will set up the EFK stack (Elasticsearch, Fluentd, Kibana) to collect, store, and visualize logs from our cluster.

Lab

Step 01 - Deploy a Sample Application

  • Deploy a simple Nginx application that generates access logs.
kubectl create deployment nginx --image=nginx
+kubectl expose deployment nginx --port=80 --type=NodePort
+
  • Check that the pod is running:
kubectl get pods
+

Step 02 - Deploy Elasticsearch

  • Deploy Elasticsearch using Helm:
helm repo add elastic https://helm.elastic.co
+helm repo update
+helm install elasticsearch elastic/elasticsearch --set replicas=1 --set minimumMasterNodes=1
+
  • Wait for the pod to be ready and check its status:
kubectl get pods
+

Step 03 - Deploy Kibana

  • Deploy Kibana using Helm:
helm install kibana elastic/kibana
+
  • Forward the Kibana port:
kubectl port-forward svc/kibana-kibana 5601:5601 &
+

If you are running this lab in Google Cloud Shell:

  1. After running the port-forward command above, click the Web Preview button in the Cloud Shell toolbar (usually at the top right).
  2. Enter port 5601 when prompted.
  3. This will open Kibana in a new browser tab at a URL like https://<cloudshell-id>.shell.cloud.google.com/?port=5601.
  4. If you see a warning about an untrusted connection, you can safely proceed.
  • Access Kibana at http://localhost:5601 (if running locally) or via the Cloud Shell Web Preview, as explained above.

Step 04 - Deploy Fluentd

  • Deploy Fluentd as a DaemonSet to collect logs from all nodes and forward them to Elasticsearch.
kubectl apply -f https://raw.githubusercontent.com/fluent/fluentd-kubernetes-daemonset/master/fluentd-daemonset-elasticsearch-rbac.yaml
+
  • Check that Fluentd pods are running:
kubectl get pods -l app=fluentd
+

Step 05 - Generate and View Logs

  • Access the Nginx service to generate logs:
minikube service nginx
+

In Kibana, configure an index pattern to view logs:

  1. Open Kibana in your browser (using the Cloud Shell Web Preview as described above).
  2. In the left menu, click Stack Management > Kibana > Index Patterns.
  3. Click Create index pattern.
  4. In the “Index pattern” field, enter fluentd-* (or logstash-* if your logs use that prefix).
  5. Click Next step.
  6. For the time field, select @timestamp and click Create index pattern.
  7. Go to Discover in the left menu to view and search your logs.

Explore the logs, search, and visualize traffic.


Troubleshooting

Pods not starting:
  • Check pod status and logs:
kubectl get pods
+kubectl describe pod <pod-name>
+kubectl logs <pod-name>
+


Kibana not reachable:
  • Ensure port-forward is running and no firewall is blocking port 5601.


No logs in Kibana:
  • Check Fluentd and Elasticsearch pod logs for errors.
  • Ensure index pattern is set up correctly in Kibana.

Cleanup

  • To remove all resources created by this lab:
helm uninstall elasticsearch
+helm uninstall kibana
+kubectl delete deployment nginx
+kubectl delete service nginx
+kubectl delete -f https://raw.githubusercontent.com/fluent/fluentd-kubernetes-daemonset/master/fluentd-daemonset-elasticsearch-rbac.yaml
+

Next Steps

  • Try deploying other logging stacks like Loki + Grafana.
  • Explore log aggregation, alerting, and retention policies.
  • Integrate logging with monitoring and alerting tools.
  • Read more in the Kubernetes logging documentation.
\ No newline at end of file diff --git a/mkdocs-site/14-Logging/resources/ClusterRole.yaml b/mkdocs-site/14-Logging/resources/ClusterRole.yaml new file mode 100644 index 0000000..681cb2e --- /dev/null +++ b/mkdocs-site/14-Logging/resources/ClusterRole.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: fluentd + namespace: kube-system +rules: + - apiGroups: + - "" + resources: + - pods + - namespaces + verbs: + - get + - list + - watch diff --git a/mkdocs-site/14-Logging/resources/ClusterRoleBinding.yaml b/mkdocs-site/14-Logging/resources/ClusterRoleBinding.yaml new file mode 100644 index 0000000..7da3109 --- /dev/null +++ b/mkdocs-site/14-Logging/resources/ClusterRoleBinding.yaml @@ -0,0 +1,13 @@ +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: fluentd + namespace: kube-system +roleRef: + kind: ClusterRole + name: fluentd + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: fluentd + namespace: kube-system diff --git a/mkdocs-site/14-Logging/resources/DaemonSet.yaml b/mkdocs-site/14-Logging/resources/DaemonSet.yaml new file mode 100644 index 0000000..5434768 --- /dev/null +++ b/mkdocs-site/14-Logging/resources/DaemonSet.yaml @@ -0,0 +1,63 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: fluentd-azureblob + namespace: kube-system + labels: + k8s-app: fluentd-logging + version: v1 +spec: + selector: + matchLabels: + k8s-app: fluentd-logging + version: v1 + template: + metadata: + labels: + k8s-app: fluentd-logging + version: v1 + spec: + serviceAccount: fluentd + serviceAccountName: fluentd + tolerations: + - key: node-role.kubernetes.io/control-plane + effect: NoSchedule + containers: + - name: fluentd-azureblob + image: fluent/fluentd-kubernetes-daemonset:v1-debian-azureblob + imagePullPolicy: Always + env: + - name: AZUREBLOB_ACCOUNT_NAME + value: "" + # Use AZUREBLOB_ACCOUNT_KEY for access key authorization, AZUREBLOB_SAS_TOKEN for shared access signature authorization, + # AZUREBLOB_CONNECTION_STRING to use the full connection string generated in the Azure Portal or neither to use Managed Service Identity. + - name: AZUREBLOB_ACCOUNT_KEY + value: "" + - name: AZUREBLOB_CONNECTION_STRING + value: "" + - name: AZUREBLOB_SAS_TOKEN + value: "" + - name: AZUREBLOB_CONTAINER + value: "" + - name: AZUREBLOB_LOG_PATH + value: "" + resources: + limits: + memory: 200Mi + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + - name: varlog + mountPath: /var/log + - name: varlibdockercontainers + mountPath: /var/lib/docker/containers + readOnly: true + terminationGracePeriodSeconds: 30 + volumes: + - name: varlog + hostPath: + path: /var/log + - name: varlibdockercontainers + hostPath: + path: /var/lib/docker/containers diff --git a/mkdocs-site/14-Logging/resources/ServiceAccount.yaml b/mkdocs-site/14-Logging/resources/ServiceAccount.yaml new file mode 100644 index 0000000..d49dfa7 --- /dev/null +++ b/mkdocs-site/14-Logging/resources/ServiceAccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: fluentd + namespace: kube-system diff --git a/mkdocs-site/14-Logging/resources/kustomization.yaml b/mkdocs-site/14-Logging/resources/kustomization.yaml new file mode 100644 index 0000000..1365718 --- /dev/null +++ b/mkdocs-site/14-Logging/resources/kustomization.yaml @@ -0,0 +1,7 @@ +namespace: kube-system + +resources: + - ServiceAccount.yaml + - ClusterRole.yaml + - ClusterRoleBinding.yaml + - DaemonSet.yaml diff --git a/mkdocs-site/14-Logging/runMe.sh b/mkdocs-site/14-Logging/runMe.sh new file mode 100644 index 0000000..2314c0c --- /dev/null +++ b/mkdocs-site/14-Logging/runMe.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Build the resources folder +kubectl kustomize resources/ > logger.yaml && kubectl delete -f logger.yaml +kubectl kustomize resources/ > logger.yaml && kubectl apply -f logger.yaml diff --git a/mkdocs-site/15-Prometheus-Grafana/archive/Dockerfile b/mkdocs-site/15-Prometheus-Grafana/archive/Dockerfile new file mode 100644 index 0000000..3a9d27d --- /dev/null +++ b/mkdocs-site/15-Prometheus-Grafana/archive/Dockerfile @@ -0,0 +1,12 @@ +FROM node + +WORKDIR /app + +# Copy the content of the application +COPY . . + +# Install requirements +RUN npm install + +# Run the server +CMD node . \ No newline at end of file diff --git a/mkdocs-site/15-Prometheus-Grafana/archive/docker-compose.yaml b/mkdocs-site/15-Prometheus-Grafana/archive/docker-compose.yaml new file mode 100644 index 0000000..c429985 --- /dev/null +++ b/mkdocs-site/15-Prometheus-Grafana/archive/docker-compose.yaml @@ -0,0 +1,39 @@ +version: "3" +services: + nodejs-application: + build: + context: ./app + container_name: nodejs-app + image: nodejs-application + ports: + - "5000:5000" + prometheus: + container_name: prometheus-svc + image: prom/prometheus + ports: + - "9091:9090" + command: --config.file=/etc/prometheus/prometheus.yaml + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yaml + grafana: + image: grafana/grafana + ports: + - "3000:3000" + environment: + - GF_AUTH_BASIC_ENABLED=false + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + grafana-dashboards: + image: alpine + depends_on: + - grafana + volumes: + - ./grafana-data:/grafana + command: > + /bin/sh -c " + apk add --no-cache curl + echo 'waiting for grafana' + sleep 5s + cd /grafana + curl --request POST http://grafana:3000/api/datasources --header 'Content-Type: application/json' -d @datasources.json + curl --request POST http://grafana:3000/api/dashboards/db --header 'Content-Type: application/json' -d @dashboard.json" diff --git a/mkdocs-site/15-Prometheus-Grafana/archive/package-lock.json b/mkdocs-site/15-Prometheus-Grafana/archive/package-lock.json new file mode 100644 index 0000000..d23c78b --- /dev/null +++ b/mkdocs-site/15-Prometheus-Grafana/archive/package-lock.json @@ -0,0 +1,904 @@ +{ + "name": "app", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^4.17.1", + "prom-client": "^13.1.0" + } + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", + "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.29", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", + "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", + "dependencies": { + "mime-db": "1.46.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/prom-client": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-13.1.0.tgz", + "integrity": "sha512-jT9VccZCWrJWXdyEtQddCDszYsiuWj5T0ekrPszi/WEegj3IZy6Mm09iOOVM86A4IKMWq8hZkT2dD9MaSe+sng==", + "dependencies": { + "tdigest": "^0.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dependencies": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/tdigest": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "dependencies": { + "bintrees": "1.0.1" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + } + }, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", + "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==" + }, + "mime-types": { + "version": "2.1.29", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", + "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", + "requires": { + "mime-db": "1.46.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "prom-client": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-13.1.0.tgz", + "integrity": "sha512-jT9VccZCWrJWXdyEtQddCDszYsiuWj5T0ekrPszi/WEegj3IZy6Mm09iOOVM86A4IKMWq8hZkT2dD9MaSe+sng==", + "requires": { + "tdigest": "^0.1.1" + } + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "tdigest": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "requires": { + "bintrees": "1.0.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } +} diff --git a/mkdocs-site/15-Prometheus-Grafana/archive/package.json b/mkdocs-site/15-Prometheus-Grafana/archive/package.json new file mode 100644 index 0000000..bd98fee --- /dev/null +++ b/mkdocs-site/15-Prometheus-Grafana/archive/package.json @@ -0,0 +1,12 @@ +{ + "name": "prometheus-grafana", + "version": "0.0.1", + "description": "15-Prometheus-grafana K8S demo app", + "main": "server.js", + "author": "Nir Geier nirgeier@gmail.com", + "license": "ISC", + "dependencies": { + "express": "^4.17.1", + "prom-client": "^13.1.0" + } +} \ No newline at end of file diff --git a/mkdocs-site/15-Prometheus-Grafana/archive/prometheus.yaml b/mkdocs-site/15-Prometheus-Grafana/archive/prometheus.yaml new file mode 100644 index 0000000..a99ad09 --- /dev/null +++ b/mkdocs-site/15-Prometheus-Grafana/archive/prometheus.yaml @@ -0,0 +1,8 @@ +global: + scrape_interval: 5s + evaluation_interval: 30s +scrape_configs: + - job_name: k8s-premetheus-grafana-demo + honor_labels: true + static_configs: + - targets: ["app:5000"] diff --git a/mkdocs-site/15-Prometheus-Grafana/archive/server.js b/mkdocs-site/15-Prometheus-Grafana/archive/server.js new file mode 100644 index 0000000..9dae057 --- /dev/null +++ b/mkdocs-site/15-Prometheus-Grafana/archive/server.js @@ -0,0 +1,58 @@ +'use strict'; + +const + // Express server + express = require('express'), + app = express(), + // Prometheus client for node.js + client = require('prom-client'), + + // Prometheus metric (Counter)- count number of requests + counter = new client.Counter({ + name: 'node_request_operations_total', + help: 'The total number of processed requests' + }), + // Prometheus metric (Histogram)- duration of requests in seconds + histogram = new client.Histogram({ + name: 'node_request_duration_seconds', + help: 'Histogram for the duration in seconds.', + buckets: [1, 2, 5, 6, 10] + }), + + PORT = 5000, + HOST = '127.0.0.1'; + + +// Probe every 5th second. +client.collectDefaultMetrics({ timeout: 5000 }); + +app.get('/', (req, res) => { + + // Simulate a sleep + let start = new Date(), + simulateTime = 1000; + + setTimeout(function (argument) { + // execution time simulated with setTimeout function + var end = new Date() - start + histogram.observe(end / 1000); //convert to seconds + }, simulateTime) + + // Increment the counter on every new request + counter.inc(); + + // Send reply to the user + res.send('Hello world\n'); +}); + + +// Metrics endpoint for the collector +app.get('/metrics', (req, res) => { + res.set('Content-Type', client.register.contentType) + res.end(client.register.metrics()) +}) + +// Start the server +app.listen(PORT, HOST, () => { + console.log(`Server is running on http://${HOST}:${PORT}`); +}); diff --git a/mkdocs-site/15-Prometheus-Grafana/demo.sh b/mkdocs-site/15-Prometheus-Grafana/demo.sh new file mode 100644 index 0000000..f86de9e --- /dev/null +++ b/mkdocs-site/15-Prometheus-Grafana/demo.sh @@ -0,0 +1,137 @@ +#!/bin/bash + +#set -x + +export CLUSTER_NAME=prometheus-cluster +export PROMETHEUS_NS=prometheus-stack + +# Install kind if not already installed +# eval "$(/opt/homebrew/bin/brew shellenv)" +# brew install kind derailed/k9s/k9s + +## Add helm charts +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo update + +# Create the demo folder +rm -rf demo +mkdir -p demo +cd demo + +# Create the cluster.yaml +# cat << EOF > cluster.yaml +# ### +# ### Auto Generated file from the script. +# ### Do not edit !!! +# ### +# ### +# apiVersion: kind.x-k8s.io/v1alpha4 +# kind: Cluster +# nodes: +# - role: control-plane +# kubeadmConfigPatches: +# - | +# kind: InitConfiguration +# nodeRegistration: +# kubeletExtraArgs: +# # +# # node-labels: +# # only allow the ingress controller to run on a +# # specific node(s) matching the label selector +# # +# node-labels: "ingress-ready=true" +# # +# # extraPortMappings: +# # allow the local host to make requests to the +# # Ingress controller over ports 80/443 +# # +# extraPortMappings: +# - containerPort: 80 +# hostPort: 8080 +# protocol: TCP +# - containerPort: 443 +# hostPort: 6443 +# protocol: TCP +# - role: worker +# - role: worker +# EOF + +# # Delete old cluster +# kind delete \ +# cluster \ +# --name $CLUSTER_NAME + +# # Start the cluster +# kind create \ +# cluster \ +# --name $CLUSTER_NAME \ +# --config ./cluster.yaml + +# # Wait for nodes +# kubectl wait node \ +# --all \ +# --for condition=ready \ +# --timeout=600s + +# Verify that the cluster is running +kubectl get nodes -o wide +kind get clusters + +# Insatll prometeus +kubectl delete ns $PROMETHEUS_NS +kubectl create ns $PROMETHEUS_NS + +# Swicth to the new namespace as default namespace +kubectl config \ + set-context $(kubectl config current-context) \ + --namespace=$PROMETHEUS_NS + +### +### Install prometheus +### + +### Install prometheus-stack +helm install \ + prometheus-stack \ + prometheus-community/kube-prometheus-stack + +### Check the installation +kubectl get \ + pods -l "release=prometheus-stack" \ + -n $PROMETHEUS_NS + +kubectl wait \ + pod -l "release=prometheus-stack" \ + --for=condition=ready \ + -n $PROMETHEUS_NS + +## Open the Grafan ports +kubectl port-forward \ + svc/$PROMETHEUS_NS-grafana \ + -n $PROMETHEUS_NS \ + 3000:80 & + +kubectl port-forward \ + svc/$PROMETHEUS_NS-kube-prom-prometheus \ + -n $PROMETHEUS_NS \ + 9090:9090 & + +# Extract the values of the secret +export GRAFANA_USER_NAME=$(kubectl get secret \ + prometheus-stack-grafana \ + -o jsonpath='{.data.admin-user}' \ + | base64 -d) + +export GRAFANA_PASSWORD=$(kubectl get secret \ + prometheus-stack-grafana \ + -o jsonpath='{.data.admin-password}' \ + | base64 -d) + +echo '' +echo '' +echo 'User : ' $GRAFANA_USER_NAME +echo 'Password: ' $GRAFANA_PASSWORD +echo '' +echo '' + + \ No newline at end of file diff --git a/mkdocs-site/15-Prometheus-Grafana/demo/cluster.yaml b/mkdocs-site/15-Prometheus-Grafana/demo/cluster.yaml new file mode 100644 index 0000000..29e56f1 --- /dev/null +++ b/mkdocs-site/15-Prometheus-Grafana/demo/cluster.yaml @@ -0,0 +1,34 @@ +### +### Auto Generated file from the script. +### Do not edit !!! +### +### +apiVersion: kind.x-k8s.io/v1alpha4 +kind: Cluster +nodes: +- role: control-plane + kubeadmConfigPatches: + - | + kind: InitConfiguration + nodeRegistration: + kubeletExtraArgs: + # + # node-labels: + # only allow the ingress controller to run on a + # specific node(s) matching the label selector + # + node-labels: "ingress-ready=true" + # + # extraPortMappings: + # allow the local host to make requests to the + # Ingress controller over ports 80/443 + # + extraPortMappings: + - containerPort: 80 + hostPort: 8080 + protocol: TCP + - containerPort: 443 + hostPort: 6443 + protocol: TCP +- role: worker +- role: worker diff --git a/mkdocs-site/15-Prometheus-Grafana/index.html b/mkdocs-site/15-Prometheus-Grafana/index.html new file mode 100644 index 0000000..02559ab --- /dev/null +++ b/mkdocs-site/15-Prometheus-Grafana/index.html @@ -0,0 +1,110 @@ + 15 Prometheus Grafana - KubernetesLabs

K8S Hands-on

Visitor Badge


Prometheus and Grafana Monitoring Lab

  • In this lab, we will learn how to set up and configure *Prometheus and Grafana for monitoring a Kubernetes cluster.
  • You will install Prometheus to collect metrics from the cluster and Grafana to visualize those metrics.
  • By the end of this lab, you will have a functional monitoring stack that provides insights into the health and performance of your Kubernetes environment.

Pre requirements

Open in Cloud Shell

CTRL + click to open in new window


Prometheus and Grafana Setup and Configuration Guide

  • This guide serves as a comprehensive walkthrough of the steps to set up Prometheus and Grafana on your Kubernetes cluster.
  • It includes hands-on steps for installing Prometheus using Helm, configuring Prometheus to collect metrics, setting up Grafana to visualize key metrics, and automating the setup using a bash script.

Introduction to Prometheus and Grafana

Prometheus

  • Prometheus is an open-source systems monitoring and alerting toolkit designed for reliability and scalability.
  • It collects and stores metrics as time-series data, providing powerful querying capabilities.
  • It is commonly used in Kubernetes environments for monitoring cluster health, application performance, and infrastructure.

Grafana

  • Grafana is a popular open-source data visualization tool that works well with Prometheus.
  • It allows you to create dashboards and visualize metrics in real-time, providing insights into system performance and application health.
  • Grafana supports a wide range of visualization options, including graphs, heatmaps, tables, and more.
  • Together, Prometheus and Grafana provide a powerful stack for monitoring and alerting in Kubernetes.

Part 01 - Installing Prometheus and Grafana

Helm Charts

We will use Helm, to deploy Prometheus and Grafana.

Step 01 - Add Prometheus and Grafana Helm Repositories

  • Let’s add the official Helm charts for Prometheus and Grafana:
# Add Prometheus community Helm repository
+helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
+# Add Grafana Helm repository
+helm repo add grafana https://grafana.github.io/helm-charts
+# Update your Helm repositories to make sure they are up-to-date
+helm repo update
+

Step 02 - Install Prometheus Stack

  • Prometheus is installed using the prometheus-stack Helm chart.
# Install Prometheus 
+#          Alertmanager
+#          Node Exporter
+# Create the `monitoring` namespace if it does not exist.
+helm install  prometheus                                  \
+              --namespace monitoring                      \
+              --create-namespace                          \
+              prometheus-community/kube-prometheus-stack  
+
+# Verify the status of the release using the following:
+helm status prometheus -n monitoring
+

Step 03 - Install Grafana

  • Now, let’s install Grafana.
  • Grafana will be deployed in the same monitoring namespace.
helm install grafana grafana/grafana --namespace monitoring
+
+# Verify the status of the release using the following:
+helm status grafana -n monitoring
+

Step 04 - Access Grafana

  • Grafana will expose a service in your Kubernetes cluster.
  • To access it, you need a password and port forwarding.
# In order to get the Grafana admin password, run the following command:
+kubectl get secret grafana          \
+            --namespace monitoring  \
+            -o jsonpath='{.data.admin-password}' | base64 --decode ; echo
+
+# Set the port forwarding so you can access the service using your browsers
+kubectl port-forward            \
+        --namespace monitoring  \
+        service/grafana 3000:80
+
  • Verify that you can access **Grafana
  • Open your browser and navigate to http://localhost:3000
  • The default login is:
    • Username : admin
    • Password : (the password you retrieved earlier)

Accessing Grafana on Google Cloud Shell

If you are running your cluster in Google Cloud Shell, you cannot use localhost for port forwarding. Instead, use the Cloud Shell Web Preview:

  1. Run the port-forward command as usual:
    kubectl port-forward --namespace monitoring service/grafana 3000:80
    +
  2. In Google Cloud Shell, click the “Web Preview” button (top right) and select “Preview on port 3000”.
  3. Grafana will open in a new browser tab.
  4. Username: admin
  5. Password: (the password you retrieved earlier)

Note: You can use any available port (e.g., 3000, 3001) in the port-forward command, just match it in the Web Preview.


Part 02 - Configuring Prometheus

  • Prometheus can collect various metrics from your Kubernetes cluster automatically if the right exporters are enabled.
  • The kube-prometheus-stack chart that you installed earlier automatically configures Prometheus to scrape a number of Kubernetes components (like kubelet, node-exporter, and kube-state-metrics) for various metrics.

Step 01 - Verify Prometheus Metrics Collection

  • You can check if Prometheus is correctly scraping metrics by navigating to Prometheus’ web UI.
# Port-forward the Prometheus service:
+kubectl port-forward            \
+        --namespace monitoring  \
+        svc/prometheus-operated 9090:9090
+
  • Verify that you can access Prometheus
  • Open http://localhost:9090
  • In the expression field paste the following:
# This query will show the current status of the `kube-state-metrics` job
+up{job="kube-state-metrics"}
+

Accessing Prometheus on Google Cloud Shell

If you are running your cluster in Google Cloud Shell, you cannot use localhost for port forwarding. Instead, use the Cloud Shell Web Preview:

  1. Run the port-forward command as usual:
    kubectl port-forward --namespace monitoring svc/prometheus-operated 9090:9090
    +
  2. In Google Cloud Shell, click the “Web Preview” button (top right) and select “Preview on port 9090”.
  3. Prometheus will open in a new browser tab.

Note: You can use any available port (e.g., 9090, 9091) in the port-forward command, just match it in the Web Preview.


Part 03 - Configuring Grafana

  • In this part we will set grafana to display the Cluster’s CPUs, Memory, and Requests.
  • Grafana dashboards can be configured to display real-time metrics for CPU, memory, and requests.
  • Prometheus stores these metrics and Grafana will query Prometheus to display them.

Step 01 - Add Prometheus as a Data Source in Grafana

  1. Log into Grafana at: http://localhost:3000, or use the Cloud Shell Web Preview.
  2. Click on the hamburger icon on the left sidebar to open the Configuration menu.
  3. Click on Data Sources.
  4. Click Add data source and choose Prometheus.
  5. In the URL field, enter the Prometheus server URL: http://prometheus-operated:9090.
  6. Click Save & Test to confirm that the connection is working.

Step 02 - Create a Dashboard to Display Metrics

  • Next step is to create a dashboard and panels to display the desired metrics.
  • To create a dashboard in Grafana for CPU, memory, and requests do the following:

  • In Grafana, open the left sidebar menu and select Dashboard.

  • Click Add visualization.
  • Choose Data Source (as we defined it previously).
  • In the panel editor, click on the Code option (right side of the query builder).
  • Enter the below queries to visualize metric(s): Note: To add new query click on the + Add query
  • Save the dashboard.


  • CPU Usage
sum(rate(container_cpu_usage_seconds_total{namespace="default", container!="", container!="POD"}[5m])) by (pod, namespace)
+
  • Memory Usage :
sum(container_memory_usage_bytes{namespace="default", container!="", container!="POD"}) by (pod, namespace)
+
  • Request Count :
sum(rate(http_requests_total{job="kubelet", cluster="", namespace="default"}[5m])) by (pod, namespace)
+

Step 03 - Get Number of Pods in the Cluster

  • To track the number of pods running in the cluster, add new panel with the following query:
# This query counts the number of pods running in all the namespaces
+count(kube_pod_info{}) by (namespace)
+
  • Add another query which will count the number of pods under the namespace monitoring:
count(kube_pod_info{namespace="monitoring"}) by (namespace)
+

Tip

We have already defined query based upon namespaces before.... You can use the same approach to filter by other labels as well.

Step 04: Customize the Panel

  • Change the visualization by changing the Graph Style
\ No newline at end of file diff --git a/mkdocs-site/15-Prometheus-Grafana/install.sh b/mkdocs-site/15-Prometheus-Grafana/install.sh new file mode 100644 index 0000000..7eac954 --- /dev/null +++ b/mkdocs-site/15-Prometheus-Grafana/install.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Step 1: Add Helm Repositories +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo add grafana https://grafana.github.io/helm-charts +helm repo update + +# Step 2: Install Prometheus Stack +helm install prometheus prometheus-community/kube-prometheus-stack --namespace monitoring --create-namespace + +# Step 3: Install Grafana +helm install grafana grafana/grafana --namespace monitoring + +# Step 4: Wait for Prometheus and Grafana to be deployed +echo "Waiting for Prometheus and Grafana to be deployed..." +kubectl rollout status deployment/prometheus-operated -n monitoring +kubectl rollout status deployment/grafana -n monitoring + +# Step 5: Get Grafana Admin Password +GRAFANA_PASSWORD=$(kubectl get secret --namespace monitoring grafana -o jsonpath='{.data.admin-password}' | base64 --decode) + +# Step 6: Port-forward Grafana +echo "Grafana is ready. Port forwarding to localhost:3000..." +kubectl port-forward --namespace monitoring service/grafana 3000:80 & + +# Step 7: Output Grafana credentials +echo "Grafana URL: http://localhost:3000" +echo "Username: admin" +echo "Password: $GRAFANA_PASSWORD" + +# Optional: Print instructions for manual steps +echo "To configure Grafana and Prometheus, please follow the instructions provided in the guide." + +kubectl port-forward --namespace monitoring svc/prometheus-operated 9090:9090 & diff --git a/mkdocs-site/16-Affinity-Taint-Tolleration/index.html b/mkdocs-site/16-Affinity-Taint-Tolleration/index.html new file mode 100644 index 0000000..40bf302 --- /dev/null +++ b/mkdocs-site/16-Affinity-Taint-Tolleration/index.html @@ -0,0 +1,262 @@ + 16 Affinity Taint Tolleration - KubernetesLabs

K8S Hands-on

Visitor Badge


Node Affinity, Taints, and Tolerations Lab

  • In this lab, we will explore Kubernetes mechanisms for controlling Pod placement on Nodes.
  • We will learn how to use Node Affinity, Taints, and Tolerations to schedule Pods on specific Nodes based on labels, constraints, and preferences.
  • By the end of this lab, you will understand how to control where Pods run in your cluster and how to reserve Nodes for specific workloads.

Pre requirements

Open in Cloud Shell

CTRL + click to open in new window


Introduction to Pod Scheduling

Kubernetes provides several mechanisms to control which Nodes your Pods run on:

Node Affinity

  • Node Affinity allows us to constrain which Nodes our Pods can be scheduled on based on Node labels.
  • It’s a more expressive and flexible version of nodeSelector.
  • There are two types:
  • requiredDuringSchedulingIgnoredDuringExecution: Hard requirement - Pod will not be scheduled unless the rule is met.
  • preferredDuringSchedulingIgnoredDuringExecution: Soft preference - Scheduler will try to enforce but will still schedule the Pod if it can’t.

Taints and Tolerations

  • Taints are applied to Nodes and allow a Node to repel a set of Pods.
  • Tolerations are applied to Pods and allow (but do not require) Pods to schedule onto Nodes with matching Taints.
  • Taints and Tolerations work together to ensure that Pods are not scheduled onto inappropriate Nodes.
  • Use cases include:
    • Dedicating Nodes to specific workloads
    • Reserving Nodes with special hardware (GPUs, SSDs)
    • Isolating problematic Pods

Part 01 - Node Affinity

  • In this section, we will learn how to use Node Affinity to schedule Pods on specific Nodes.

Step 01 - Label Your Nodes

  • First, let’s label some Nodes to use with Node Affinity:
# Get list of nodes
+kubectl get nodes
+
+# Label a node with environment=production
+kubectl label nodes <node-name> environment=production
+
+# Label another node with environment=development
+kubectl label nodes <node-name> environment=development
+
+# Verify the labels
+kubectl get nodes --show-labels
+

Step 02 - Create a Pod with Required Node Affinity

  • Create a Pod that must run on a Node with environment=production:
apiVersion: v1
+kind: Pod
+metadata:
+  name: affinity-required-pod
+spec:
+  affinity:
+    nodeAffinity:
+      requiredDuringSchedulingIgnoredDuringExecution:
+        nodeSelectorTerms:
+        - matchExpressions:
+          - key: environment
+            operator: In
+            values:
+            - production
+  containers:
+  - name: nginx
+    image: nginx:latest
+
  • Apply the Pod:
# Create the Pod
+kubectl apply -f affinity-required-pod.yaml
+
+# Check which Node the Pod is running on
+kubectl get pod affinity-required-pod -o wide
+
+# Verify it's running on the production Node
+kubectl describe pod affinity-required-pod | grep Node:
+

Step 03 - Create a Pod with Preferred Node Affinity

  • Create a Pod that prefers to run on a Node with environment=development:
apiVersion: v1
+kind: Pod
+metadata:
+  name: affinity-preferred-pod
+spec:
+  affinity:
+    nodeAffinity:
+      preferredDuringSchedulingIgnoredDuringExecution:
+      - weight: 1
+        preference:
+          matchExpressions:
+          - key: environment
+            operator: In
+            values:
+            - development
+  containers:
+  - name: nginx
+    image: nginx:latest
+
  • Apply the Pod:
# Create the Pod
+kubectl apply -f affinity-preferred-pod.yaml
+
+# Check which Node the Pod is running on
+kubectl get pod affinity-preferred-pod -o wide
+
+# This Pod will prefer the development Node but can run elsewhere if needed
+

Step 04 - Experiment with Node Affinity Operators

  • Node Affinity supports several operators:

    • In: Label value is in the list of values
    • NotIn: Label value is not in the list of values
    • Exists: Label key exists (value does not matter)
    • DoesNotExist: Label key does not exist
    • Gt: Label value is greater than the specified value (numeric comparison)
    • Lt: Label value is less than the specified value (numeric comparison)
  • Example with multiple conditions:

apiVersion: v1
+kind: Pod
+metadata:
+  name: affinity-multiple-conditions
+spec:
+  affinity:
+    nodeAffinity:
+      requiredDuringSchedulingIgnoredDuringExecution:
+        nodeSelectorTerms:
+        - matchExpressions:
+          - key: environment
+            operator: In
+            values:
+            - production
+            - staging
+          - key: disk-type
+            operator: Exists
+  containers:
+  - name: nginx
+    image: nginx:latest
+

Part 02 - Taints and Tolerations

  • In this section, we will learn how to use Taints and Tolerations to control Pod scheduling.

Step 01 - Understanding Taint Effects

  • Taints have three effects:

    • NoSchedule: Pods without matching tolerations will not be scheduled on the Node.
    • PreferNoSchedule: Scheduler will try to avoid placing Pods without tolerations, but it’s not guaranteed.
    • NoExecute: Existing Pods without tolerations will be evicted, and new ones won’t be scheduled.

Step 02 - Apply a Taint to a Node

  • Let’s taint a Node to dedicate it for special workloads:
# Apply a taint to a Node
+kubectl taint nodes <node-name> dedicated=special-workload:NoSchedule
+
+# Verify the taint
+kubectl describe node <node-name> | grep Taints
+

Step 03 - Create a Pod Without Toleration

  • Let’s try to create a Pod without a toleration:
apiVersion: v1
+kind: Pod
+metadata:
+  name: pod-without-toleration
+spec:
+  containers:
+  - name: nginx
+    image: nginx:latest
+
# Create the Pod
+kubectl apply -f pod-without-toleration.yaml
+
+# Check the Pod status - it should not be scheduled on the tainted Node
+kubectl get pod pod-without-toleration -o wide
+
+# If all your Nodes are tainted, the Pod will remain Pending
+kubectl describe pod pod-without-toleration
+

Step 04 - Create a Pod With Toleration

  • Now let’s create a Pod that tolerates the taint:
apiVersion: v1
+kind: Pod
+metadata:
+  name: pod-with-toleration
+spec:
+  tolerations:
+  - key: "dedicated"
+    operator: "Equal"
+    value: "special-workload"
+    effect: "NoSchedule"
+  containers:
+  - name: nginx
+    image: nginx:latest
+
# Create the Pod
+kubectl apply -f pod-with-toleration.yaml
+
+# This Pod can now be scheduled on the tainted Node
+kubectl get pod pod-with-toleration -o wide
+

Step 05 - Understanding Toleration Operators

  • Tolerations support two operators:

    • Equal: Requires exact match of key, value, and effect
    • Exists: Only checks for key existence (value is ignored)
  • Example with Exists operator:

apiVersion: v1
+kind: Pod
+metadata:
+  name: pod-with-exists-toleration
+spec:
+  tolerations:
+  - key: "dedicated"
+    operator: "Exists"
+    effect: "NoSchedule"
+  containers:
+  - name: nginx
+    image: nginx:latest
+

Step 06 - NoExecute Effect

  • The NoExecute effect is special - it evicts running Pods:
# Apply a NoExecute taint
+kubectl taint nodes <node-name> maintenance=true:NoExecute
+
+# Any Pods on this Node without matching toleration will be evicted
+kubectl get pods -o wide --watch
+
  • Let’s add a toleration with tolerationSeconds to delay eviction:
apiVersion: v1
+kind: Pod
+metadata:
+  name: pod-with-delayed-eviction
+spec:
+  tolerations:
+  - key: "maintenance"
+    operator: "Equal"
+    value: "true"
+    effect: "NoExecute"
+    tolerationSeconds: 300  # Pod will be evicted after 5 minutes
+  containers:
+  - name: nginx
+    image: nginx:latest
+

Part 03 - Combining Affinity, Taints, and Tolerations

  • We can combine Node Affinity with Taints and Tolerations for fine-grained control.

Step 01 - Create a Dedicated Node Pool

  • Let’s simulate a dedicated Node pool for GPU workloads:
# Label a Node for GPU workload
+kubectl label nodes <node-name> hardware=gpu
+
+# Taint the Node to prevent non-GPU Pods
+kubectl taint nodes <node-name> nvidia.com/gpu=true:NoSchedule
+

Step 02 - Deploy a GPU Workload

  • Let’s create a Pod that requires GPU Nodes:
apiVersion: v1
+kind: Pod
+metadata:
+  name: gpu-workload
+spec:
+  affinity:
+    nodeAffinity:
+      requiredDuringSchedulingIgnoredDuringExecution:
+        nodeSelectorTerms:
+        - matchExpressions:
+          - key: hardware
+            operator: In
+            values:
+            - gpu
+  tolerations:
+  - key: "nvidia.com/gpu"
+    operator: "Equal"
+    value: "true"
+    effect: "NoSchedule"
+  containers:
+  - name: gpu-app
+    image: nvidia/cuda:11.0-base
+    command: ["nvidia-smi"]
+
# Apply the Pod
+kubectl apply -f gpu-workload.yaml
+
+# Verify it's scheduled on the GPU Node
+kubectl get pod gpu-workload -o wide
+

Part 04 - Cleanup and Best Practices

Step 01 - Remove Taints

# Remove a taint from a Node (add a minus sign at the end)
+kubectl taint nodes <node-name> dedicated=special-workload:NoSchedule-
+
+# Remove all taints with a specific key
+kubectl taint nodes <node-name> nvidia.com/gpu-
+

Step 02 - Remove Labels

# Remove a label from a Node (add a minus sign at the end)
+kubectl label nodes <node-name> environment-
+kubectl label nodes <node-name> hardware-
+

Step 03 - Delete Test Pods

# Delete all test Pods
+kubectl delete pod affinity-required-pod affinity-preferred-pod
+kubectl delete pod pod-with-toleration pod-without-toleration
+kubectl delete pod gpu-workload
+

Best Practices

  1. Use Node Affinity for preferences, Taints/Tolerations for hard requirements.
  2. Label Nodes consistently across your cluster (e.g., node-role, hardware-type, environment).
  3. Document your taints - team members need to know why Nodes are tainted.
  4. Use PreferNoSchedule for soft isolation instead of NoSchedule when appropriate.
  5. Combine with Pod Priority for more sophisticated scheduling strategies.
  6. Test eviction behavior before using NoExecute in production.
  7. Use tolerationSeconds to gracefully handle Node maintenance.
  8. Monitor unschedulable Pods - they indicate scheduling constraint conflicts.

Summary

In this lab, you learned:

  • How to use Node Affinity to schedule Pods on specific Nodes based on labels.
  • The difference between required and preferred affinity rules.
  • How to use Taints to repel Pods from Nodes.
  • How to use Tolerations to allow Pods on tainted Nodes.
  • The three taint effects: NoSchedule, PreferNoSchedule, and NoExecute.
  • How to combine affinity, taints, and tolerations for complex scheduling scenarios.
  • Best practices for Pod placement in production clusters.

Additional Resources

\ No newline at end of file diff --git a/mkdocs-site/17-PodDisruptionBudgets-PDB/index.html b/mkdocs-site/17-PodDisruptionBudgets-PDB/index.html new file mode 100644 index 0000000..433df3d --- /dev/null +++ b/mkdocs-site/17-PodDisruptionBudgets-PDB/index.html @@ -0,0 +1,126 @@ + 17 PodDisruptionBudgets PDB - KubernetesLabs

K8S Hands-on

Visitor Badge


Pod Disruption Budgets (PDB)

  • In this lab, we will learn about Pod Disruption Budgets (PDB) in Kubernetes.
  • We will explore how to define and implement PDBs to ensure application availability during voluntary disruptions, such as node maintenance or cluster upgrades.
  • By the end of this lab, you will understand how to create and manage Pod Disruption Budgets to maintain the desired level of service availability in your Kubernetes cluster.

Pre requirements

Open in Cloud Shell

CTRL + click to open in new window


PodDisruptionBudgets: Budgeting the Number of Faults to Tolerate

  • A pod disruption budget is an indicator of the number of disruptions that can be tolerated at a given time for a class of pods (a budget of faults).

  • Disruptions may be caused by deliberate or accidental Pod deletion.

  • Whenever a disruption to the pods in a service is calculated to cause the service to drop below the budget, the operation is paused until it can maintain the budget. This means that the drain event could be temporarily halted while it waits for more pods to become available such that the budget isn’t crossed by evicting the pods.

  • You can specify Pod Disruption Budgets for Pods managed by these built-in Kubernetes controllers:

    • Deployment
    • ReplicationController
    • ReplicaSet
    • StatefulSet
  • For this tutorial you should get familier with Kubernetes Eviction Policies, as it demonstrates how Pod Disruption Budgets handle evictions.

  • As in the Kubernetes Eviction Policies tutorial, we start with

    eviction-hard="memory.available<480M"
    +


Sample

  • In the below sample we will configure a Pod Disruption Budget which insure that we will always have at least 1 Nginx instance.

  • First we need an Nginx Deployment:

apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+  namespace: codewizard
+  labels:
+    app: nginx # <- We will use this name below
+...
+
  • Now we can create the Pod Disruption Budget:
apiVersion: policy/v1beta1
+kind: PodDisruptionBudget
+metadata:
+  name: nginx-pdb
+spec:
+  minAvailable: 1 # <--- This will insure that we will have at least 1
+  selector:
+    matchLabels:
+      app: nginx # <- The deployment app label 
+

Walkthrough

01. start minikube with Feature Gates

02. Check Node Pressure(s)


Step 01 - Start minikube with Feature Gates

  • Run thwe following command to start minikube with the required Feature Gates and Eviction Signals:
minikube start \
+    --extra-config=kubelet.eviction-hard="memory.available<480M" \
+    --extra-config=kubelet.eviction-pressure-transition-period="30s" \
+    --extra-config=kubelet.feature-gates="ExperimentalCriticalPodAnnotation=true"
+
  • For more details about Feature Gates, read here.

  • For more details about eviction-signals, read here.

Step 02 - Check Node Pressure(s)

  • Check to see the Node conditions, if we have any kind of “Pressure”, by running the following:
kubectl describe node minikube | grep MemoryPressure
+
+# Output should be similar to :
+Conditions:
+  Type             Status  Reason                       Message
+  ----             ------  ------                       -------
+  MemoryPressure   False   KubeletHasSufficientMemory   kubelet has sufficient memory available
+  DiskPressure     False   KubeletHasNoDiskPressure     kubelet has no disk pressure
+  PIDPressure      False   KubeletHasSufficientPID      kubelet has sufficient PID available
+  Ready            True    KubeletReady                 kubelet is posting ready status. AppArmor enabled
+  ...
+Allocated resources:
+  (Total limits may be over 100 percent, i.e., overcommitted.)
+  Resource           Requests    Limits
+  --------           --------    ------
+  cpu                750m (37%)  0 (0%)
+  memory             140Mi (6%)  340Mi (16%)
+  ephemeral-storage  0 (0%)      0 (0%)  
+

Step 03 - Create 3 Pods using 50 MB each

  • Create a file named 50MB-ram.yaml with the following content:
# ./resources/50MB-ram.yaml
+...
+
+# 3 replicas
+spec:
+  replicas: 3
+
+# resources request and limits
+resources:
+  requests:
+    memory: "50Mi"
+    cpu: "250m"
+  limits:
+    memory: "128Mi"
+    cpu: "500m"
+
  • Create the pods with the following command:
kubectl apply -f resources/50MB-ram.yaml
+

Step 04 - Check Memory Pressure

  • Now let’s check the Node conditions again to see if we have MemoryPressure:

$ kubectl describe node minikube | grep MemoryPressure
+
+# Output should be similar to 
+MemoryPressure   False   ...   KubeletHasSufficientMemory   kubelet has sufficient memory available
+
- As we can see, we still have sufficient memory available.

\ No newline at end of file diff --git a/mkdocs-site/17-PodDisruptionBudgets-PDB/resources/50MB-ram.yaml b/mkdocs-site/17-PodDisruptionBudgets-PDB/resources/50MB-ram.yaml new file mode 100644 index 0000000..a3bddfa --- /dev/null +++ b/mkdocs-site/17-PodDisruptionBudgets-PDB/resources/50MB-ram.yaml @@ -0,0 +1,24 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: busybox +spec: + replicas: 3 + selector: + matchLabels: + app: busybox + template: + metadata: + labels: + app: busybox + spec: + containers: + - name: busybox + image: busybox + resources: + requests: + memory: "50Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" diff --git a/mkdocs-site/17-PodDisruptionBudgets-PDB/resources/Deployment.yaml b/mkdocs-site/17-PodDisruptionBudgets-PDB/resources/Deployment.yaml new file mode 100644 index 0000000..2babe59 --- /dev/null +++ b/mkdocs-site/17-PodDisruptionBudgets-PDB/resources/Deployment.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + namespace: codewizard + labels: + app: nginx +spec: + replicas: 2 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 80 + resources: + limits: + memory: 256Mi + cpu: 500ms diff --git a/mkdocs-site/17-PodDisruptionBudgets-PDB/resources/PodDisruptionBudget.yaml b/mkdocs-site/17-PodDisruptionBudgets-PDB/resources/PodDisruptionBudget.yaml new file mode 100644 index 0000000..6084849 --- /dev/null +++ b/mkdocs-site/17-PodDisruptionBudgets-PDB/resources/PodDisruptionBudget.yaml @@ -0,0 +1,9 @@ +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + name: nginx-pdb +spec: + minAvailable: 1 # <--- This will insure that we will have at least 1 + selector: + matchLabels: + app: nginx \ No newline at end of file diff --git a/mkdocs-site/17-PodDisruptionBudgets-PDB/startMinikube.sh b/mkdocs-site/17-PodDisruptionBudgets-PDB/startMinikube.sh new file mode 100644 index 0000000..728cae6 --- /dev/null +++ b/mkdocs-site/17-PodDisruptionBudgets-PDB/startMinikube.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# For more details about Feature Gates read: +# https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/#feature-stages +# +# For more details about eviction-signals +# https://kubernetes.io/docs/tasks/administer-cluster/out-of-resource/#eviction-signals + +minikube start \ + --extra-config=kubelet.eviction-hard="memory.available<480M" \ + --extra-config=kubelet.eviction-pressure-transition-period="30s" \ + --extra-config=kubelet.feature-gates="ExperimentalCriticalPodAnnotation=true" + +kubectl describe node minikube | grep MemoryPressure \ No newline at end of file diff --git a/mkdocs-site/18-ArgoCD/ArgoCD.sh b/mkdocs-site/18-ArgoCD/ArgoCD.sh new file mode 100644 index 0000000..0ad6e26 --- /dev/null +++ b/mkdocs-site/18-ArgoCD/ArgoCD.sh @@ -0,0 +1,63 @@ +# Define the desired namespace +NAMESPACE=argocd + +# start the minikube cluster +# minikube start + +# Download ArgoCD CLI +VERSION=$(curl --silent "https://api.github.com/repos/argoproj/argo-cd/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') +sudo curl -sSL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/$VERSION/argocd-linux-amd64 +sudo chmod +x /usr/local/bin/argocd + +# Deploy ArgoCD - create namespace +kubectl create namespace $NAMESPACE + +# Set the default namesapce +kubectl config set-context --current --namespace=$NAMESPACE + +# Change the argocd-server service type to LoadBalancer: +#kubectl patch svc argocd-server -n $NAMESPACE -p '{"spec": {"type": "NodePort"}}' + +# Set the new desired Deployment +cat << EOF > kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: argocd +resources: + - https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml + +patchesStrategicMerge: +- patch-replace.yaml +EOF + +# Set the desired patch +cat << EOF > patch-replace.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: argocd-server +spec: + selector: + matchLabels: + app.kubernetes.io/name: argocd-server + template: + spec: + containers: + - name: argocd-server + command: + - argocd-server + - --insecure + - --staticassets + - /shared/app +EOF + +kubectl kustomize . | kubectl apply -f - +sleep 30 + +echo '---------------------------------------------------------------' +echo 'User : admin' +echo 'Password: ' $(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d) +echo '---------------------------------------------------------------' + + +kubectl port-forward svc/argocd-server -n argocd 8085:80 diff --git a/mkdocs-site/18-ArgoCD/index.html b/mkdocs-site/18-ArgoCD/index.html new file mode 100644 index 0000000..63e5e24 --- /dev/null +++ b/mkdocs-site/18-ArgoCD/index.html @@ -0,0 +1,304 @@ + 18 ArgoCD - KubernetesLabs

K8S Hands-on

Visitor Badge


ArgoCD

  • In this tutorial, we will learn the essentials of ArgoCD, a declarative GitOps continuous delivery tool for Kubernetes.
  • We will install ArgoCD, deploy applications, sync resources from Git repositories, and gain practical experience with GitOps workflows.

Pre Requirements

  • K8S cluster - Setting up minikube cluster instruction
  • kubectl configured to interact with your cluster
  • A Git repository (GitHub, GitLab, or Bitbucket) for storing application manifests
  • Basic understanding of Kubernetes resources (Deployments, Services, etc.)

Open in Cloud Shell

CTRL + click to open in new window


What will we learn?

  • What ArgoCD is and why it’s useful for GitOps.
  • How to install and configure ArgoCD on Kubernetes.
  • ArgoCD core concepts: Applications, Projects, and Sync.
  • How to deploy applications from Git repositories.
  • Application health and sync status monitoring.
  • Rollback and sync strategies.
  • Best practices for GitOps workflows.

What is ArgoCD?

  • ArgoCD is a declarative, GitOps continuous delivery tool for Kubernetes.
  • It follows the GitOps pattern where Git repositories are the source of truth for defining the desired application state.
  • ArgoCD automates the deployment of the desired application states in the specified target environments.

Why ArgoCD?

  • GitOps Workflow: Uses Git as the single source of truth.
  • Automated Deployment: Automatically syncs your Kubernetes cluster with Git repositories.
  • Application Health Monitoring: Continuous monitoring of deployed applications.
  • Rollback Capabilities: Easy rollback to previous versions.
  • Multi-Cluster Support: Manage applications across multiple clusters.
  • SSO Integration: Supports various SSO providers for authentication.
  • RBAC: Fine-grained access control.
  • Audit Trail: Full audit trail of all operations.

Terminology

  • Application

    • An ArgoCD Application is a Kubernetes resource object representing a deployed application instance in an environment.
    • It defines the source repository, target cluster, and sync policies.
  • Project

    • An ArgoCD Project provides a logical grouping of applications.
    • Projects are useful for organizing applications and implementing RBAC.
  • Sync

    • Sync is the process of making a live cluster state match the desired state defined in Git.
    • Sync can run manually or automatically.
  • Sync Status

    • Indicates whether the live state matches the Git state.
    • Status can be: Synced, OutOfSync, or Unknown.
  • Health Status

    • Indicates the health of the application resources.
    • Status can be: Healthy, Progressing, Degraded, Suspended, or Missing.

ArgoCD Architecture

Component Description
API Server Exposes the API consumed by Web UI, CLI, and CI/CD systems
Repository Server Maintains a local cache of Git repositories holding application manifests
Application Controller Monitors running applications and compares the current live state against the desired state
Dex Identity service for integrating with external identity providers
Redis Used for caching

Common ArgoCD CLI Commands

Command Description
argocd login <server> Login to ArgoCD server
argocd app create <app-name> Create a new application
argocd app list List all applications
argocd app get <app-name> Get application details
argocd app sync <app-name> Sync (deploy) an application
argocd app delete <app-name> Delete an application
argocd app set <app-name> Update application parameters
argocd app diff <app-name> Show differences between Git and live state
argocd app history <app-name> Show application deployment history
argocd app rollback <app-name> <revision> Rollback to a previous revision

Lab

Part 01 - Installing ArgoCD

Step 01 - Create an ArgoCD Namespace

  • Create a dedicated namespace for ArgoCD:
kubectl create namespace argocd
+

Step 02 - Install ArgoCD

  • Install ArgoCD using the official installation manifest:
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
+

Step 03 - Verify Installation

  • Check that all ArgoCD pods are running:
kubectl get pods -n argocd
+
  • Expected output should show all pods in Running status:
NAME                                  READY   STATUS    RESTARTS   AGE
+argocd-application-controller-0       1/1     Running   0          2m
+argocd-dex-server-xxx                 1/1     Running   0          2m
+argocd-redis-xxx                      1/1     Running   0          2m
+argocd-repo-server-xxx                1/1     Running   0          2m
+argocd-server-xxx                     1/1     Running   0          2m
+

Step 04 - Expose ArgoCD Server

  • By default, the ArgoCD API server is not exposed externally.
  • We’ will use port-forwarding to access it, by running:
kubectl port-forward svc/argocd-server -n argocd 8080:443
+
  • Alternatively, you can change the service type to LoadBalancer or create an Ingress:
# Change to LoadBalancer (for cloud environments)
+kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
+
+# Or use NodePort (for local/development)
+kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort"}}'
+

Step 05 - Get Initial Admin Password

  • The initial password for the admin user is auto-generated and stored as a secret by running the following command:
# Get the initial admin password
+kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo
+

Note

Save this password - you’ll need it to login to the ArgoCD UI later.

Step 06 - Access ArgoCD UI

  • Open your browser and navigate to:

    • Port-forward: https://localhost:8080
    • LoadBalancer: Use the external IP
    • NodePort: Use http://<node-ip>:<node-port>
  • Login with:

    • Username: admin
    • Password: (from Step 05)

Part 02 - Installing ArgoCD CLI

Step 01 - Download and Install ArgoCD CLI

  • Install the ArgoCD CLI based on your operating system:

Linux:

curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
+sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
+rm argocd-linux-amd64
+

macOS:

brew install argocd
+

Windows (via Chocolatey):

choco install argocd-cli
+

Step 02 - Verify CLI Installation

argocd version --short
+

Step 03 - Login via CLI

# If using port-forward
+argocd login localhost:8080 --insecure
+
+# You'll be prompted for username and password
+
argocd account update-password
+

Part 03 - Deploying Your First Application

Step 01 - Prepare a Git Repository

  • For this lab, we will use a sample Git repository with Kubernetes manifests.
  • You can use the ArgoCD example repository or your own:
# Sample repository URL
+https://github.com/argoproj/argocd-example-apps.git
+

Step 02 - Create an Application via CLI

  • Create an ArgoCD application that deploys the guestbook app:
argocd app create guestbook \
+  --repo https://github.com/argoproj/argocd-example-apps.git \
+  --path guestbook \
+  --dest-server https://kubernetes.default.svc \
+  --dest-namespace default
+
Command Explanation
  • --repo: The Git repository URL
  • --path: Path within the repository where manifests are located
  • --dest-server: Target Kubernetes cluster (default is the cluster where ArgoCD is installed)
  • --dest-namespace: Target namespace for deployment

Step 03 - View Application Status

# List all applications
+argocd app list
+
+# Get detailed information about the application
+argocd app get guestbook
+

Step 04 - Sync the Application

  • Initially, the application status will be OutOfSync.
  • Sync it to deploy by running:
argocd app sync guestbook
+

Step 05 - Verify Deployment

# Check the deployed resources
+kubectl get all -n default
+
+# You should see the guestbook deployment, service, and pods
+

Step 06 - Access the Application

# Port-forward to access the guestbook service
+kubectl port-forward svc/guestbook-ui -n default 8081:80
+
+# Open browser to http://localhost:8081
+

Part 04 - Creating Application via UI

Step 01 - Access ArgoCD UI

  • Navigate to the ArgoCD UI at https://localhost:8080

Step 02 - Create a New Application

  1. Click on ”+ NEW APP” button.
  2. Fill in the following details:

    • Application Name: helm-guestbook
    • Project: default
    • Sync Policy: Manual (or Automatic for auto-sync)
    • Repository URL: https://github.com/argoproj/argocd-example-apps.git
    • Revision: HEAD
    • Path: helm-guestbook
    • Cluster URL: https://kubernetes.default.svc
    • Namespace: default
  3. Click “CREATE”.

Step 03 - View Application in UI

  • You should now be able to see the helm-guestbook application tile in the UI.
  • Click on it to see the application topology.

Step 04 - Sync via UI

  • Click the “SYNC” button.
  • Select the resources you want to sync (or select all).
  • Click “SYNCHRONIZE”.

Step 05 - Monitor Sync Progress

  • Watch the sync progress in real-time.
  • The UI will show each resource being created/updated.
  • Once completed, all resources should show as Healthy and Synced.

Part 05 - Application Management

Step 01 - View Application Details

# Get full application details
+argocd app get guestbook
+
+# View application parameters
+argocd app get guestbook --show-params
+

Step 02 - View Sync History

# View deployment history
+argocd app history guestbook
+

Step 03 - View Differences

# Show differences between Git and live state
+argocd app diff guestbook
+

Step 04 - Manual Sync with Options

# Sync with prune (removes resources not in Git)
+argocd app sync guestbook --prune
+
+# Sync specific resources
+argocd app sync guestbook --resource Deployment:guestbook-ui
+
+# Dry-run sync
+argocd app sync guestbook --dry-run
+

Part 06 - Auto-Sync and Self-Healing

Step 01 - Enable Auto-Sync

  • Enable automatic synchronization so ArgoCD automatically deploys changes from Git:
argocd app set guestbook --sync-policy automated
+

Step 02 - Enable Self-Healing

  • Enable self-healing to automatically fix out-of-sync resources:
argocd app set guestbook --self-heal
+

Step 03 - Enable Auto-Prune

  • Enable auto-prune to automatically delete resources removed from Git:
argocd app set guestbook --auto-prune
+

Step 04 - Test Auto-Sync

  1. Make a change to your Git repository (e.g., change replica count).
  2. Commit and push the change.
  3. Watch as ArgoCD automatically detects and syncs the change:
# Watch the application sync status
+watch argocd app get guestbook
+

Step 05 - Test Self-Healing

  1. Manually modify a deployed resource by running:
# Manually scale the deployment
+kubectl scale deployment guestbook-ui --replicas=5
+
  1. Watch as ArgoCD detects the drift and automatically restores the desired state:
# ArgoCD will revert the replica count to what's in Git
+argocd app get guestbook
+

Part 07 - Rollback

Step 01 - View Application History

# View all deployment revisions
+argocd app history guestbook
+

Example output:

ID  DATE                 REVISION
+0   2025-11-10 10:15:30  abc123 (HEAD)
+1   2025-11-10 10:20:45  def456
+2   2025-11-10 10:25:30  ghi789
+

Step 02 - Rollback to Previous Revision

# Rollback to revision ID 1
+argocd app rollback guestbook 1
+

Step 03 - Verify Rollback

# Verify the application state
+argocd app get guestbook
+
+# Check deployed resources
+kubectl get all -n default
+

Part 08 - Working with Helm Charts

Step 01 - Create Helm-Based Application

argocd app create nginx-helm \
+  --repo https://charts.bitnami.com/bitnami \
+  --helm-chart nginx \
+  --revision 15.1.0 \
+  --dest-server https://kubernetes.default.svc \
+  --dest-namespace default
+

Step 02 - Set Helm Values

# Set Helm values
+argocd app set nginx-helm \
+  --helm-set service.type=NodePort \
+  --helm-set replicaCount=3
+

Step 03 - Sync Helm Application

argocd app sync nginx-helm
+

Step 04 - View Helm Parameters

# View all Helm parameters
+argocd app get nginx-helm --show-params
+

Part 09 - Working with Kustomize

Step 01 - Create Kustomize-Based Application

argocd app create kustomize-guestbook \
+  --repo https://github.com/argoproj/argocd-example-apps.git \
+  --path kustomize-guestbook \
+  --dest-server https://kubernetes.default.svc \
+  --dest-namespace default
+

Step 02 - Sync Kustomize Application

argocd app sync kustomize-guestbook
+

Step 03 - Verify Deployment

kubectl get all -n default -l app=kustomize-guestbook
+

Part 10 - Projects and RBAC

Step 01 - Create a New Project

argocd proj create my-project \
+  --description "My demo project" \
+  --src https://github.com/argoproj/argocd-example-apps.git \
+  --dest https://kubernetes.default.svc,default \
+  --dest https://kubernetes.default.svc,my-namespace
+

Step 02 - List Projects

argocd proj list
+

Step 03 - View Project Details

argocd proj get my-project
+

Step 04 - Create Application in Project

argocd app create my-app \
+  --project my-project \
+  --repo https://github.com/argoproj/argocd-example-apps.git \
+  --path guestbook \
+  --dest-server https://kubernetes.default.svc \
+  --dest-namespace default
+

Part 11 - Multi-Source Applications

Step 01 - Create Multi-Source Application

  • ArgoCD supports applications with multiple source repositories:
# Save as multi-source-app.yaml
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+  name: multi-source-app
+  namespace: argocd
+spec:
+  project: default
+  sources:
+    - repoURL: https://github.com/argoproj/argocd-example-apps.git
+      path: guestbook
+      targetRevision: HEAD
+    - repoURL: https://github.com/another-repo/configs.git
+      path: overlays/prod
+      targetRevision: HEAD
+  destination:
+    server: https://kubernetes.default.svc
+    namespace: default
+  syncPolicy:
+    automated:
+      prune: true
+      selfHeal: true
+

Step 02 - Apply Multi-Source Application

kubectl apply -f multi-source-app.yaml
+

Part 12 - Sync Windows and Waves

Step 01 - Configure Sync Windows

  • Sync windows allow you to define time periods when syncs are allowed or denied:
# Add a sync window to allow syncs only during business hours
+argocd proj windows add my-project \
+  --kind allow \
+  --schedule "0 9 * * 1-5" \
+  --duration 8h \
+  --applications "*"
+

Step 02 - Configure Sync Waves

  • Use annotations to control the order of resource synchronization:
apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: database
+  annotations:
+    argocd.argoproj.io/sync-wave: "0"  # Deploy first
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: backend
+  annotations:
+    argocd.argoproj.io/sync-wave: "1"  # Deploy after database
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: frontend
+  annotations:
+    argocd.argoproj.io/sync-wave: "2"  # Deploy last
+

Part 13 - Health Checks and Hooks

Step 01 - Custom Health Checks

  • Define custom health checks for your resources:
apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: argocd-cm
+  namespace: argocd
+data:
+  resource.customizations: |
+    cert-manager.io/Certificate:
+      health.lua: |
+        hs = {}
+        if obj.status ~= nil then
+          if obj.status.conditions ~= nil then
+            for i, condition in ipairs(obj.status.conditions) do
+              if condition.type == "Ready" and condition.status == "False" then
+                hs.status = "Degraded"
+                hs.message = condition.message
+                return hs
+              end
+              if condition.type == "Ready" and condition.status == "True" then
+                hs.status = "Healthy"
+                hs.message = condition.message
+                return hs
+              end
+            end
+          end
+        end
+        hs.status = "Progressing"
+        hs.message = "Waiting for certificate"
+        return hs
+

Step 02 - Sync Hooks

  • Use hooks to execute actions during sync:
apiVersion: batch/v1
+kind: Job
+metadata:
+  name: database-migration
+  annotations:
+    argocd.argoproj.io/hook: PreSync
+    argocd.argoproj.io/hook-delete-policy: HookSucceeded
+spec:
+  template:
+    spec:
+      containers:
+      - name: migration
+        image: myapp/migration:latest
+        command: ["./run-migrations.sh"]
+      restartPolicy: Never
+

Finalize & Cleanup

Clean Up Applications

# Delete all applications
+argocd app delete guestbook --cascade
+argocd app delete helm-guestbook --cascade
+argocd app delete nginx-helm --cascade
+argocd app delete kustomize-guestbook --cascade
+
+# Or delete via kubectl
+kubectl delete applications -n argocd --all
+

Uninstall ArgoCD

# Delete ArgoCD installation
+kubectl delete -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
+
+# Delete the namespace
+kubectl delete namespace argocd
+

Troubleshooting

ArgoCD Server Not Accessible

  • Check if the ArgoCD server pod is running:
kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server
+
  • Check the service:
kubectl get svc -n argocd argocd-server
+

Application Stuck in Progressing State

  • Check application details:
argocd app get <app-name>
+kubectl describe application <app-name> -n argocd
+
  • Check pod logs:
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller
+

Sync Fails with Permission Errors

  • Verify RBAC settings:
argocd proj get <project-name>
+
  • Check if the destination namespace exists:
kubectl get namespace <namespace>
+

Out of Sync Status

  • View the differences:
argocd app diff <app-name>
+
  • Force sync:
argocd app sync <app-name> --force
+

Repository Connection Issues

  • Test repository connectivity:
argocd repo add <repo-url> --username <username> --password <password>
+argocd repo list
+

Best Practices

GitOps Workflow

  1. Single Source of Truth: Keep all Kubernetes manifests in Git.
  2. Branch Strategy: Use branches for different environments (dev, staging, prod).
  3. Pull Requests: Use PRs for all changes with proper reviews.
  4. Automated Testing: Validate manifests before merging.
  5. Rollback Strategy: Use Git revert for rollbacks.

Application Organization

  1. Use Projects: Organize applications by team or environment.
  2. Naming Conventions: Use clear, consistent naming.
  3. Sync Policies: Choose appropriate sync policies per environment.
  4. Resource Limits: Set proper resource limits in manifests.
  5. Health Checks: Define custom health checks for complex resources.

Security

  1. RBAC: Implement fine-grained access control.
  2. SSO Integration: Use SSO for authentication.
  3. Secret Management: Use sealed-secrets or external secret managers.
  4. Network Policies: Restrict ArgoCD network access.
  5. Audit Logging: Enable and monitor audit logs.

Monitoring

  1. Notifications: Configure notifications for sync failures.
  2. Metrics: Monitor ArgoCD metrics with Prometheus.
  3. Dashboards: Create Grafana dashboards for visibility.
  4. Alerts: Set up alerts for critical failures.
  5. Regular Reviews: Periodically review application health.

Next Steps

  • Explore ApplicationSets for managing multiple applications.
  • Integrate ArgoCD with CI/CD pipelines.
  • Set up notifications using Slack, email, or webhooks.
  • Implement Progressive Delivery with Argo Rollouts.
  • Configure SSO integration with your identity provider.
  • Set up multi-cluster management.
  • Explore ArgoCD Image Updater for automated image updates.
  • Read the official ArgoCD documentation
  • Join the ArgoCD community

Additional Resources

\ No newline at end of file diff --git a/mkdocs-site/18-ArgoCD/install.sh b/mkdocs-site/18-ArgoCD/install.sh new file mode 100644 index 0000000..0a132f6 --- /dev/null +++ b/mkdocs-site/18-ArgoCD/install.sh @@ -0,0 +1,3 @@ + + +kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo diff --git a/mkdocs-site/18-ArgoCD/kustomization.yaml b/mkdocs-site/18-ArgoCD/kustomization.yaml new file mode 100644 index 0000000..71b4516 --- /dev/null +++ b/mkdocs-site/18-ArgoCD/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: argocd +resources: + - https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml + +patchesStrategicMerge: +- patch-replace.yaml diff --git a/mkdocs-site/18-ArgoCD/patch-replace.yaml b/mkdocs-site/18-ArgoCD/patch-replace.yaml new file mode 100644 index 0000000..5839054 --- /dev/null +++ b/mkdocs-site/18-ArgoCD/patch-replace.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: argocd-server +spec: + selector: + matchLabels: + app.kubernetes.io/name: argocd-server + template: + spec: + containers: + - name: argocd-server + command: + - argocd-server + - --insecure + - --staticassets + - /shared/app diff --git a/mkdocs-site/19-CustomScheduler/codeWizardScheduler.sh b/mkdocs-site/19-CustomScheduler/codeWizardScheduler.sh new file mode 100644 index 0000000..e544d91 --- /dev/null +++ b/mkdocs-site/19-CustomScheduler/codeWizardScheduler.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# Author: Nir Geier + +# `set -o pipefail` +# When executing the sequence of commands connected to the pipe, +# as long as any one command returns a non-zero value, +# the entire pipe returns a non-zero value, +# even if the last command returns 0. +# +# In other words, the chain of command will fail if any of the command fail + +set -eo pipefail +# `set -x` +# Shell mode, where all executed commands are printed to the terminal +# Remark if you dont wish to view full log +set -x + +# Start minikube if required +source ../../scripts/startMinikube.sh + +# Deploy our demo pods +kubectl kustomize ./resources | kubectl apply -f - + +# Start the API and listen on port 8081 +kubectl proxy --port=8081 & + +# Syntax: +# ${parameter:-word} +# If parameter is unset or null, +# the expansion of word is substituted. +# Otherwise, +# the value of parameter is substituted. + +# You can set those paramters out side of this script +# export CLUSTER_URL= +CLUSTER_URL="${CLUSTER_URL:-127.0.0.1:8081}" +CUSTOM_SCHEDULER="${CUSTOM_SCHEDULER:-codeWizardScheduler}" + +# Scheduler should always run +while true; do + # Get a list of all our pods in pending state + for POD in $(kubectl get pods \ + --server ${CLUSTER_URL} \ + --output jsonpath='{.items..metadata.name}' \ + --field-selector=status.phase==Pending); + do + + # Get the desired schedulerName if the pod has defined any schedulerName + CUSTOM_SCHEDULER_NAME=$(kubectl get pod ${POD} \ + --output jsonpath='{.spec.schedulerName}') + + # Check if the desired schedulerName is our custome one + # If its a match this is where our custom scheduler will "jump in" + if [ "${CUSTOM_SCHEDULER_NAME}" == "${CUSTOM_SCHEDULER}" ]; + then + # Get the pod namespace + NAMESPACE=$(kubectl get pod ${POD} \ + --output jsonpath='{.metadata.namespace}') + + # Get an array for of all the nodes + NODES=($(kubectl get nodes \ + --server ${CLUSTER_URL} \ + --output jsonpath='{.items..metadata.name}')); + + # Store a number for the length of our NODES array + NODES_LENGTH=${#NODES[@]} + + # Randomly select a node from the array + # $RANDOM % $NODES_LENGTH will be the remainder + # of a random number divided by the length of our nodes + # In the case of 1 node this is always ${NODES[0]} + NODE=${NODES[$[$RANDOM % $NODES_LENGTH]]} + + # Bind the current pod to the node selected above + # The "binding" is done using API call to pods/.../binding + curl \ + --request POST \ + --silent \ + --fail \ + --header "Content-Type:application/json" \ + --data '{"apiVersion":"v1", + "kind": "Binding", + "metadata": { + "name": "'${POD}'" + }, + "target": { + "apiVersion": "v1", + "kind": "Node", + "name": "'${NODE}'" + } + }' \ + http://${CLUSTER_URL}/api/v1/namespaces/${NAMESPACE}/pods/${POD}/binding/ >/dev/null \ + && echo "${POD} was assigned to ${NODE}" \ + || echo "Failed to assign ${POD} to ${NODE}" + fi + done + # Current scheduling done, sleep and wake up for the next iteration + echo "Scheduler ig going to sleep" + + sleep 15s +done \ No newline at end of file diff --git a/mkdocs-site/19-CustomScheduler/index.html b/mkdocs-site/19-CustomScheduler/index.html new file mode 100644 index 0000000..0653e37 --- /dev/null +++ b/mkdocs-site/19-CustomScheduler/index.html @@ -0,0 +1,124 @@ + 19 CustomScheduler - KubernetesLabs

K8S Hands-on

Visitor Badge


Writing custom Scheduler

  • Scheduling is the process of selecting a node for a pod to run on.
  • In this lab we will write our own pods scheduler.
  • It is probably not something that you will ever need to do, but still it’s a good practice to understand how scheduling works in K8S and how you can extend it.

Pre Requirements

  • K8S cluster - Setting up minikube cluster instruction
  • kubectl configured to interact with your cluster
  • A Git repository (GitHub, GitLab, or Bitbucket) for storing application manifests
  • Basic understanding of Kubernetes resources (Deployments, Services, etc.)

Open in Cloud Shell

CTRL + click to open in new window


Custom Scheduler

  • See further information in the official documentation: Scheduler Configuration
  • To schedule a given pod using a specific scheduler, specify the name of the scheduler in that specification .spec.schedulerName.

A bit about scheduler

  • Scheduling happens in a series of stages that are exposed through extension points.
  • We can define several scheduling Profile. A scheduling Profile allows you to configure the different stages of scheduling in the kube-scheduler

Sample KubeSchedulerConfiguration

###
+# Sample KubeSchedulerConfiguration
+###
+#
+# You can configure `kube-scheduler` to run more than one profile.
+# Each profile has an associated scheduler name and can have a different
+# set of plugins configured in its extension points.
+
+# With the following sample configuration, 
+# the scheduler will run with two profiles: 
+# - default plugins 
+# - all scoring plugins disabled
+
+apiVersion: kubescheduler.config.k8s.io/v1beta1
+kind: KubeSchedulerConfiguration
+profiles:
+  - schedulerName: default-scheduler
+  - schedulerName: no-scoring-scheduler
+    plugins:
+      preScore:
+        disabled:
+        - name: '*'
+      score:
+        disabled:
+        - name: '*'
+
  • Once you have your scheduler code, you can use it in your pod scheduler:
# In this sample we use deployment but it will apply to any pod
+...
+apiVersion: apps/v1
+kind: Deployment
+spec:
+    spec:
+      # This is the import part of this file.
+      # Here we define our custom scheduler
+      schedulerName: CodeWizardScheduler # <------
+      containers:
+      - name: nginx
+        image: nginx
+

Sample bash scheduler

  • The “trick” is loop over all the waiting pods and search for the custom scheduler match in spec.schedulerName
...
+  # Get a list of all our pods in pending state
+  for POD in $(kubectl  get pods \
+                        --server ${CLUSTER_URL} \
+                        --all-namespaces \
+                        --output jsonpath='{.items..metadata.name}' \
+                        --field-selector=status.phase==Pending); 
+    do
+
+    # Get the desired schedulerName if th epod has defined any schedulerName
+    CUSTOM_SCHEDULER_NAME=$(kubectl get pod ${POD} \
+                                    --output jsonpath='{.spec.schedulerName}')
+
+    # Check if the desired schedulerName is our custome one
+    # If its a match this is where our custom scheduler will "jump in"
+    if [ "${CUSTOM_SCHEDULER_NAME}" == "${CUSTOM_SCHEDULER}" ]; 
+      then
+        # Do your magic here ......
+        # Schedule the PODS as you wish
+    fi
+    ...
+
\ No newline at end of file diff --git a/mkdocs-site/19-CustomScheduler/resources/Deployment.yaml b/mkdocs-site/19-CustomScheduler/resources/Deployment.yaml new file mode 100644 index 0000000..ba1bee2 --- /dev/null +++ b/mkdocs-site/19-CustomScheduler/resources/Deployment.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + # This is the import part of this file. + # Here we define our custom scheduler + schedulerName: codeWizardScheduler + containers: + - name: nginx + image: nginx + resources: + limits: + memory: "64Mi" + cpu: "250m" + ports: + - containerPort: 80 \ No newline at end of file diff --git a/mkdocs-site/19-CustomScheduler/resources/Namespace.yaml b/mkdocs-site/19-CustomScheduler/resources/Namespace.yaml new file mode 100644 index 0000000..6e64f11 --- /dev/null +++ b/mkdocs-site/19-CustomScheduler/resources/Namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: codewizard \ No newline at end of file diff --git a/mkdocs-site/19-CustomScheduler/resources/_KubeSchedulerConfiguration.yaml b/mkdocs-site/19-CustomScheduler/resources/_KubeSchedulerConfiguration.yaml new file mode 100644 index 0000000..47d842d --- /dev/null +++ b/mkdocs-site/19-CustomScheduler/resources/_KubeSchedulerConfiguration.yaml @@ -0,0 +1,25 @@ +### +# Sample KubeSchedulerConfiguration +### +# +# You can configure `kube-scheduler` to run more than one profile. +# Each profile has an associated scheduler name and can have a different +# set of plugins configured in its extension points. + +# With the following sample configuration, +# the scheduler will run with two profiles: +# - default plugins +# - all scoring plugins disabled. + +apiVersion: kubescheduler.config.k8s.io/v1beta1 +kind: KubeSchedulerConfiguration +profiles: + - schedulerName: default-scheduler + - schedulerName: no-scoring-scheduler + plugins: + preScore: + disabled: + - name: '*' + score: + disabled: + - name: '*' \ No newline at end of file diff --git a/mkdocs-site/19-CustomScheduler/resources/kustomization.yaml b/mkdocs-site/19-CustomScheduler/resources/kustomization.yaml new file mode 100644 index 0000000..00d77fa --- /dev/null +++ b/mkdocs-site/19-CustomScheduler/resources/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +# Set the default namespace for all the resources +namespace: codewizard + +resources: + - Namespace.yaml + - Deployment.yaml diff --git a/mkdocs-site/20-CronJob/K8S/ClusterRoleBinding.yaml b/mkdocs-site/20-CronJob/K8S/ClusterRoleBinding.yaml new file mode 100644 index 0000000..67bbff9 --- /dev/null +++ b/mkdocs-site/20-CronJob/K8S/ClusterRoleBinding.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + # This name will be used in `subjects.name` + name: cronjob-rbac + # This name will be used in `subjects.namespace` + namespace: test +subjects: + - kind: ServiceAccount + # Same value as upper's `metadata.name` + name: default + # Same value as upper's `metadata.namespace` + namespace: test +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/mkdocs-site/20-CronJob/K8S/CronJob.yaml b/mkdocs-site/20-CronJob/K8S/CronJob.yaml new file mode 100644 index 0000000..3f0d662 --- /dev/null +++ b/mkdocs-site/20-CronJob/K8S/CronJob.yaml @@ -0,0 +1,26 @@ +apiVersion: batch/v1beta1 +kind: CronJob +metadata: + name: cron-test + namespace: test +spec: + schedule: '*/1 * * * *' + jobTemplate: + spec: + template: + spec: + containers: + - command: + - /bin/sh + - '-c' + - >- + apk update \ + && apk add curl \ + && curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ + && chmod +x ./kubectl \ + && ./kubectl get pods -A + image: alpine + imagePullPolicy: IfNotPresent + name: alpine-cronjob + dnsPolicy: ClusterFirst + restartPolicy: OnFailure \ No newline at end of file diff --git a/mkdocs-site/20-CronJob/K8S/Namespace.yaml b/mkdocs-site/20-CronJob/K8S/Namespace.yaml new file mode 100644 index 0000000..e3834ea --- /dev/null +++ b/mkdocs-site/20-CronJob/K8S/Namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: codewizard \ No newline at end of file diff --git a/mkdocs-site/20-CronJob/K8S/ServiceAccount.yaml b/mkdocs-site/20-CronJob/K8S/ServiceAccount.yaml new file mode 100644 index 0000000..0511483 --- /dev/null +++ b/mkdocs-site/20-CronJob/K8S/ServiceAccount.yaml @@ -0,0 +1,8 @@ +# NOTE: +# The service account `default:default` already exists in k8s cluster when the cluster is created. +# In this sample we will create a default user under our namespace +apiVersion: v1 +kind: ServiceAccount +metadata: + name: default + namespace: test diff --git a/mkdocs-site/20-CronJob/K8S/kustomization.yaml b/mkdocs-site/20-CronJob/K8S/kustomization.yaml new file mode 100644 index 0000000..2ceedeb --- /dev/null +++ b/mkdocs-site/20-CronJob/K8S/kustomization.yaml @@ -0,0 +1,8 @@ +namespace: codewizard +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ClusterRoleBinding.yaml + - CronJob.yaml + - Namespace.yaml + - ServiceAccount.yaml diff --git a/mkdocs-site/20-CronJob/index.html b/mkdocs-site/20-CronJob/index.html new file mode 100644 index 0000000..e07584d --- /dev/null +++ b/mkdocs-site/20-CronJob/index.html @@ -0,0 +1,95 @@ + 20 CronJob - KubernetesLabs

K8S Hands-on

Visitor Badge


CronJobs

  • In this lab, we will learn how to create and manage CronJobs in Kubernetes.
  • A CronJob creates Jobs on a time-based schedule. It is useful for running periodic and recurring tasks, such as backups or report generation.

Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window


What is a CronJob?

  • A CronJob in Kubernetes runs Jobs on a time-based schedule, similar to Linux cron.
  • Useful for periodic tasks like backups, reports, or cleanup.

Step - 01: Create a CronJob YAML

  • Create a file named hello-cronjob.yaml with the following content:
apiVersion: batch/v1
+kind: CronJob
+metadata:
+    name: hello
+    namespace: default
+spec:
+    schedule: "*/1 * * * *" # Every 1 minute
+    jobTemplate:
+        spec:
+            template:
+                spec:
+                    containers:
+                    - name: hello
+                        image: busybox
+                        args:
+                        - /bin/sh
+                        - -c
+                        - date; echo Hello from the Kubernetes CronJob!
+                    restartPolicy: OnFailure
+

Step - 02: Apply the CronJob

kubectl apply -f hello-cronjob.yaml
+

Step - 03: Verify CronJob Creation

kubectl get cronjob hello
+

Step - 04: Check CronJob and Jobs

  • List CronJobs:
kubectl get cronjobs
+
  • List Jobs created by the CronJob:
kubectl get jobs
+
  • List Pods created by Jobs:
kubectl get pods
+

Step - 05: View Job Output

  • Get the name of a pod created by the CronJob, then view its logs:
kubectl logs <pod-name>
+

Example output:

Mon Nov 10 12:00:00 UTC 2025
+Hello from the Kubernetes CronJob!
+

Step - 06: Clean Up

  • Delete the CronJob and its Jobs:
kubectl delete cronjob hello
+kubectl delete jobs --all
+

Questions:

  • What happens if the job takes longer than the schedule interval?
  • How would you change the schedule to run every 5 minutes?
  • How can you limit the number of successful or failed jobs to keep?
\ No newline at end of file diff --git a/mkdocs-site/21-Auditing/demo.sh b/mkdocs-site/21-Auditing/demo.sh new file mode 100644 index 0000000..16e4067 --- /dev/null +++ b/mkdocs-site/21-Auditing/demo.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +# Debug mode +set -x + +# Stop minikube if its running and delet prevoiud data +minikube stop + +# Set the minikube home directory +export MINIKUBE_HOME=~/.minikube + +# The AuditPolicy file +AUDIT_POLICY_FILE=$MINIKUBE_HOME/files/etc/ssl/certs/Audit-Policy.yaml + +# Create the desired folder(s) +mkdir -p resources +mkdir -p logs + +# Check to see if we have a pre defined Audit Policy file +if [[ ! -f $AUDIT_POLICY_FILE ]]; +then +# Create the Policy file if its not exist +cat < $AUDIT_POLICY_FILE +# Log all requests at the Metadata level. +apiVersion: audit.k8s.io/v1 +kind: Policy +rules: +- level: Metadata +EOF +fi; + +# Start minikube with the AuditPolicy +minikube start \ + --extra-config=apiserver.audit-policy-file=$AUDIT_POLICY_FILE \ + --extra-config=apiserver.audit-log-path=${PWD}/logs/audit.log \ + --extra-config=kubelet.cgroup-driver=systemd \ + --alsologtostderr \ + -v=8 + +# Test the audit policy +kubectl create ns TestAudit + +# Print out the Audit log +kubectl logs kube-apiserver-minikube -n kube-system | grep audit.k8s.io/v1 \ No newline at end of file diff --git a/mkdocs-site/21-Auditing/resources/Audit-Policy.yaml b/mkdocs-site/21-Auditing/resources/Audit-Policy.yaml new file mode 100644 index 0000000..2086bf0 --- /dev/null +++ b/mkdocs-site/21-Auditing/resources/Audit-Policy.yaml @@ -0,0 +1,5 @@ + # Log all requests at the Metadata level.... + apiVersion: audit.k8s.io/v1 + kind: Policy + rules: + - level: Metadata diff --git a/mkdocs-site/21-KubeAPI/Dockerfile b/mkdocs-site/21-KubeAPI/Dockerfile new file mode 100644 index 0000000..4052324 --- /dev/null +++ b/mkdocs-site/21-KubeAPI/Dockerfile @@ -0,0 +1,10 @@ +FROM alpine + +# Update and install dependencies +RUN apk add --update nodejs npm curl + +# Copy the endpoint script +COPY api_query.sh . + +# Set the execution bit +RUN chmod +x api_query.sh . \ No newline at end of file diff --git a/mkdocs-site/21-KubeAPI/api_query.sh b/mkdocs-site/21-KubeAPI/api_query.sh new file mode 100644 index 0000000..1ece8cf --- /dev/null +++ b/mkdocs-site/21-KubeAPI/api_query.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +################################# +## Access the internal K8S API ## +################################# +# Point to the internal API server hostname +APISERVER=https://kubernetes.default.svc + +# Path to ServiceAccount token +# The service account is mapped by the K8S Api server in the pods +SERVICE_ACCOUNT_FOLDER=/var/run/secrets/kubernetes.io/serviceaccount + +# Read this Pod's namespace if required +# NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace) + +# Read the ServiceAccount bearer token +TOKEN=$(cat ${SERVICE_ACCOUNT_FOLDER}/token) + +# Reference the internal certificate authority (CA) +CACERT=${SERVICE_ACCOUNT_FOLDER}/ca.crt + +# Explore the API with TOKEN +curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api \ No newline at end of file diff --git a/mkdocs-site/21-KubeAPI/index.html b/mkdocs-site/21-KubeAPI/index.html new file mode 100644 index 0000000..15f541b --- /dev/null +++ b/mkdocs-site/21-KubeAPI/index.html @@ -0,0 +1,113 @@ + 21 KubeAPI - KubernetesLabs

K8S Hands-on

Visitor Badge


Kube API Access from Pod

  • In this lab, we will learn how to access the Kubernetes API from within a Pod.
  • We will create a simple Pod that runs a script to query the Kubernetes API server and retrieve information about the cluster.

Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window


Part 01 - Build the docker image

  • In order to demonstrate the API query we will build a custom docker image.
  • It is optional to use the pre-build image and skip this step.

Step 01 - The script which will be used for query K8S API

  • In order to be able to access K8S API from within a pod, we will be using the following script:
# `api_query.sh`
+
+#!/bin/sh
+
+#################################
+## Access the internal K8S API ##
+#################################
+# Point to the internal API server hostname
+API_SERVER_URL=https://kubernetes.default.svc
+
+# Path to ServiceAccount token
+# The service account is mapped by the K8S Api server in the pods
+SERVICE_ACCOUNT_FOLDER=/var/run/secrets/kubernetes.io/serviceaccount
+
+# Read this Pod's namespace if required
+# NAMESPACE=$(cat ${SERVICE_ACCOUNT_FOLDER}/namespace)
+
+# Read the ServiceAccount bearer token
+TOKEN=$(cat ${SERVICE_ACCOUNT_FOLDER}/token)
+
+# Reference the internal certificate authority (CA)
+CACERT=${SERVICE_ACCOUNT_FOLDER}/ca.crt
+
+# Explore the API with TOKEN and the Certificate
+curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${API_SERVER_URL}/api
+

Step 02 - Build the docker image

  • For the pod image we will use the following Dockerfile:
# `Dockerfile`
+
+FROM    alpine
+
+# Update and install dependencies
+RUN     apk add --update nodejs npm curl
+
+# Copy the endpoint script
+COPY    api_query.sh .
+
+# Set the execution bit
+RUN     chmod +x api_query.sh .
+

Part 02 - Deploy the Pod to K8S

  • Once the image is ready, we can deploy it as a pod to the cluster.
  • The required resources are under the k8s folder.

Step 01 - Run kustomization to deploy

  • Deploy to the cluster
# Remove old content if any
+kubectl kustomize k8s | kubectl delete -f -
+
+# Deploy the content
+kubectl kustomize k8s | kubectl apply -f -
+

Step 02 - Query the K8S API

  • Run the following script to verify that the connection to the API is working:
# Get the deployment pod name
+POD_NAME=$(kubectl get pod -A -l app=monitor-app -o jsonpath="{.items[0].metadata.name}")
+
+# Print out the logs to verify that the pods is connected to the API
+kubectl exec -it -n codewizard $POD_NAME sh ./api_query.sh
+
\ No newline at end of file diff --git a/mkdocs-site/21-KubeAPI/k8s/Deployment.yaml b/mkdocs-site/21-KubeAPI/k8s/Deployment.yaml new file mode 100644 index 0000000..9cec04b --- /dev/null +++ b/mkdocs-site/21-KubeAPI/k8s/Deployment.yaml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: monitor-app +spec: + selector: + matchLabels: + app: monitor-app + template: + metadata: + labels: + app: monitor-app + spec: + containers: + - name: monitor-app + image: nirgeier/monitor-app + args: + - sleep + - "86400" + resources: + limits: + memory: "128Mi" + cpu: "500m" + ports: + - containerPort: 8080 diff --git a/mkdocs-site/21-KubeAPI/k8s/Namespace.yaml b/mkdocs-site/21-KubeAPI/k8s/Namespace.yaml new file mode 100644 index 0000000..af12597 --- /dev/null +++ b/mkdocs-site/21-KubeAPI/k8s/Namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: codewizard \ No newline at end of file diff --git a/mkdocs-site/21-KubeAPI/k8s/kustomization.yaml b/mkdocs-site/21-KubeAPI/k8s/kustomization.yaml new file mode 100644 index 0000000..07c789e --- /dev/null +++ b/mkdocs-site/21-KubeAPI/k8s/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: codewizard + +commonLabels: + app: monitor-app + +resources: + - Namespace.yaml + - Deployment.yaml \ No newline at end of file diff --git a/mkdocs-site/21-KubeAPI/script.sh b/mkdocs-site/21-KubeAPI/script.sh new file mode 100644 index 0000000..25a49eb --- /dev/null +++ b/mkdocs-site/21-KubeAPI/script.sh @@ -0,0 +1,17 @@ +#!/bin/bash -x + +# Build +docker build -t nirgeier/monitor-app . + +# push image to docker hub +docker push nirgeier/monitor-app + +# Deploy the pod to the cluster +kubectl kustomize k8s | kubectl delete -f - +kubectl kustomize k8s | kubectl apply -f - + +# Get the deployment pod name +POD_NAME=$(kubectl get pod -A -l app=monitor-app -o jsonpath="{.items[0].metadata.name}") + +# Print out the logs to verify that the pods is conneted to the API +kubectl exec -it -n codewizard $POD_NAME sh ./api_query.sh \ No newline at end of file diff --git a/mkdocs-site/22-Rancher/01-pre-requirements.sh b/mkdocs-site/22-Rancher/01-pre-requirements.sh new file mode 100644 index 0000000..11832c1 --- /dev/null +++ b/mkdocs-site/22-Rancher/01-pre-requirements.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +### Install k3 & other tools on MacOS +# brew install k3d kubectl helm + +# In case you are not using mac - +curl -sfL https://get.k3s.io | sh - + +# Install cmctl +# cmctl is a CLI tool that can help you to manage cert-manager resources inside your cluster. +# https://cert-manager.io/docs/usage/cmctl/ +OS=$(go env GOOS); +ARCH=$(go env GOARCH); + +## create forlder for the installation +mkdir -p cmctl +cd cmctl +## Download cmctl +### -> cmctl is a CLI tool that can help you to manage cert-manager resources inside your cluster. +curl -sSL -o cmctl.tar.gz https://github.com/cert-manager/cert-manager/releases/download/v1.8.0/cmctl-$OS-$ARCH.tar.gz +# Extract the xzip file +tar xzf cmctl.tar.gz +# Add it to the path +sudo mv cmctl /usr/local/bin + +# Delete the installtion fodler +cd .. +rm -rf cmctl + +### Install k3s - Will be used later on for Rancher +wget -q -O - https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash + +### Add the required helm charts +helm repo add rancher https://releases.rancher.com/server-charts/latest +helm repo add jetstack https://charts.jetstack.io + +# Update the charts repository +helm repo update diff --git a/mkdocs-site/22-Rancher/02-install.sh b/mkdocs-site/22-Rancher/02-install.sh new file mode 100644 index 0000000..5ea3c10 --- /dev/null +++ b/mkdocs-site/22-Rancher/02-install.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +set -x +### Use this (( for tee the output to a file)) +(( + +### Variables +RANCHER_HOST=rancher.k3d.localhost +CLUSTER_NAME=rancher-cluster +CERT_MANAGER_RELEASE=v1.8.0 +API_PORT=6555 +SSL_PORT=6443 + +### Clear prevoius content +# docker stop $(docker ps -aq) +# docker rm $(docker ps -aq) + +### Remove all docker leftovers (containers, network etc) +docker system prune -f + +kubectl delete namespace cattle-system +kubectl delete namespace cert-manager + +### Remove old cluster in case there are some leftovers +k3d cluster \ + delete \ + $CLUSTER_NAME + +### Create a k3d cluster. Use the loadbalancer provided by k3d +k3d cluster \ + create \ + --wait \ + $CLUSTER_NAME \ + --servers 1 \ + --agents 3 \ + --api-port $API_PORT \ + --kubeconfig-switch-context \ + --port $SSL_PORT:443@loadbalancer + # --k3s-arg "--disable=traefik@server:*" \ + # --k3s-arg '--kubelet-arg=eviction-hard=imagefs.available<1%,nodefs.available<1%@agent:*' \ + # --k3s-arg '--kubelet-arg=eviction-minimum-reclaim=imagefs.available=1%,nodefs.available=1%@agent:*' \ + # --k3s-arg '--kube-apiserver-arg=feature-gates=EphemeralContainers=false@server:*' + +### Verify the installation +kubectl cluster-info +k3d cluster list + +### Add the k3s to the kubeconfig +k3d kubeconfig merge \ + $CLUSTER_NAME \ + --kubeconfig-switch-context + +### Create the namespace(s) for Rancher & cert-manager +#kubectl create namespace cattle-system +#kubectl create namespace cert-manager + +### Install Cert-manager +helm install \ + --wait \ + --create-namespace \ + --set installCRDs=true \ + --namespace cert-manager \ + --set prometheus.enabled=true \ + --version $CERT_MANAGER_RELEASE \ + cert-manager jetstack/cert-manager + +### Verify cert-manager installation +kubectl rollout \ + status \ + deploy/cert-manager \ + --namespace cert-manager + +### Install racnher +helm install \ + --wait \ + --create-namespace \ + rancher rancher/rancher \ + --namespace cattle-system \ + --set hostname=$RANCHER_HOST + +### Verify rancher installation +kubectl rollout status \ + deploy/racnher \ + -n cattle-system + +### Check that the cert-manager API is ready +### We expect to see the foloowing message: 'The cert-manager API is ready' +cmctl check api + +### Open broswer in: https://rancher.k3d.localhost +###### +###### Important, once on this page type; thisisunsafe +###### + +### Get the rancher password +kubectl get secret --namespace cattle-system bootstrap-secret -o go-template='{{.data.bootstrapPassword|base64decode}}{{"\n"}}' + +### Verify the cluster nodes +kubectl get nodes + +### Get the pods status in the background +kubectl get pods -A --watch & + +) 2>&1 ) | tee install.txt diff --git a/mkdocs-site/22-Rancher/03-rancher-airgap.sh b/mkdocs-site/22-Rancher/03-rancher-airgap.sh new file mode 100644 index 0000000..eb7625b --- /dev/null +++ b/mkdocs-site/22-Rancher/03-rancher-airgap.sh @@ -0,0 +1,88 @@ + +### +### rke2 relase +RKE2_RELEASE="https://github.com/rancher/rke2/releases/download/v1.30.3-rc4%2Brke2r1" + + +# # Setup network +# ip link add dummy0 type dummy +# ip link set dummy0 up +# ip addr add 203.0.113.254/31 dev dummy0 +# ip route add default via 203.0.113.255 dev dummy0 metric 1000 + +# Install helm +wget https://get.helm.sh/helm-v3.15.3-linux-amd64.tar.gz +tar -zxvf helm-v3.15.3-linux-amd64.tar.gz +mv linux-amd64/helm /usr/local/bin/helm + +# Download load rke2 binary +wget $RKE2_RELEASE/rke2.linux-amd64 +chmod +x rke2.linux-amd64 +mv rke2.linux-amd64 /usr/local/bin/rke2 + +# Get kubectl +curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +chmod +x kubectl +mv kubectl /usr/local/bin/kubectl + +# Download load rke2 installation files +mkdir ~/rke2-artifacts && cd ~/rke2-artifacts/ +curl -OLs $RKE2_RELEASE/rke2-images.linux-amd64.tar.zst +curl -OLs $RKE2_RELEASE/rke2.linux-amd64.tar.gz +curl -OLs $RKE2_RELEASE/sha256sum-amd64.txt +curl -sfL https://get.rke2.io --output install.sh + +INSTALL_RKE2_ARTIFACT_PATH=~/rke2-artifacts sh install.sh +# systemctl enable rke2-server.service +# systemctl start rke2-server.service +rke2 server + +# Set the kubeconfig +mkdir -p ~/.kube +ln -s /etc/rancher/rke2/rke2.yaml ~/.kube/config + +# Install k9s +wget https://github.com/derailed/k9s/releases/download/v0.32.5/k9s_Linux_amd64.tar.gz +gunzip k9s_Linux_amd64.tar.gz +tar -xvf k9s_Linux_amd64.tar +chmod +x k9s +mv k9s /usr/local/bin/k9s + +# Set the kubeconfig +mkdir -p ~/.kube/ +cp /etc/rancher/rke2/rke2.yaml ~/.kube/config + +# Check the server status +kubectl get pods -A + +# Add the rancher helm repository +helm repo add rancher-latest https://releases.rancher.com/server-charts/latest + +# Download the helm +helm fetch rancher-latest/rancher +helm repo add jetstack https://charts.jetstack.io +helm repo update +helm fetch jetstack/cert-manager + +curl -L -o cert-manager-crd.yaml https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.crds.yaml +kubectl create ns cert-manager +kubectl apply -n cert-manager -f cert-manager-crd.yaml +helm --debug install cert-manager --create-namespace -n cert-manager cert-manager-v1.15.2.tgz + + + +# Setuup the registry for rancher +# cat << "EOF" > /etc/rancher/rke2/registries.yaml +# mirrors: +# docker.io: +# endpoint: +# - "https://globalrepo.pe.jfrog.io/remote-docker-hub" +# EOF + +# docker pull quay.io/jetstack/cert-manager-ctl +# docker pull quay.io/jetstack/cert-manager-acmesolver +# docker pull quay.io/jetstack/cert-manager-cainjector +# docker pull quay.io/jetstack/cert-manager-webhook + +# helm repo add jetstack https://charts.jetstack.io + diff --git a/mkdocs-site/23-MetricServer/components.yaml b/mkdocs-site/23-MetricServer/components.yaml new file mode 100644 index 0000000..e5fdd91 --- /dev/null +++ b/mkdocs-site/23-MetricServer/components.yaml @@ -0,0 +1,197 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + k8s-app: metrics-server + name: metrics-server + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + k8s-app: metrics-server + rbac.authorization.k8s.io/aggregate-to-admin: "true" + rbac.authorization.k8s.io/aggregate-to-edit: "true" + rbac.authorization.k8s.io/aggregate-to-view: "true" + name: system:aggregated-metrics-reader +rules: +- apiGroups: + - metrics.k8s.io + resources: + - pods + - nodes + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + k8s-app: metrics-server + name: system:metrics-server +rules: +- apiGroups: + - "" + resources: + - nodes/metrics + verbs: + - get +- apiGroups: + - "" + resources: + - pods + - nodes + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + k8s-app: metrics-server + name: metrics-server-auth-reader + namespace: kube-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: extension-apiserver-authentication-reader +subjects: +- kind: ServiceAccount + name: metrics-server + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + k8s-app: metrics-server + name: metrics-server:system:auth-delegator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +subjects: +- kind: ServiceAccount + name: metrics-server + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + k8s-app: metrics-server + name: system:metrics-server +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:metrics-server +subjects: +- kind: ServiceAccount + name: metrics-server + namespace: kube-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + k8s-app: metrics-server + name: metrics-server + namespace: kube-system +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: https + selector: + k8s-app: metrics-server +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + k8s-app: metrics-server + name: metrics-server + namespace: kube-system +spec: + selector: + matchLabels: + k8s-app: metrics-server + strategy: + rollingUpdate: + maxUnavailable: 0 + template: + metadata: + labels: + k8s-app: metrics-server + spec: + containers: + - args: + - --cert-dir=/tmp + - --secure-port=4443 + - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname + - --kubelet-use-node-status-port + - --metric-resolution=15s + - --kubelet-insecure-tls + image: k8s.gcr.io/metrics-server/metrics-server:v0.6.1 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /livez + port: https + scheme: HTTPS + periodSeconds: 10 + name: metrics-server + ports: + - containerPort: 4443 + name: https + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /readyz + port: https + scheme: HTTPS + initialDelaySeconds: 20 + periodSeconds: 10 + resources: + requests: + cpu: 100m + memory: 200Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + volumeMounts: + - mountPath: /tmp + name: tmp-dir + nodeSelector: + kubernetes.io/os: linux + priorityClassName: system-cluster-critical + serviceAccountName: metrics-server + volumes: + - emptyDir: {} + name: tmp-dir +--- +apiVersion: apiregistration.k8s.io/v1 +kind: APIService +metadata: + labels: + k8s-app: metrics-server + name: v1beta1.metrics.k8s.io +spec: + group: metrics.k8s.io + groupPriorityMinimum: 100 + insecureSkipTLSVerify: true + service: + name: metrics-server + namespace: kube-system + version: v1beta1 + versionPriority: 100 diff --git a/mkdocs-site/23-MetricServer/kubelet-config-1.23.yaml b/mkdocs-site/23-MetricServer/kubelet-config-1.23.yaml new file mode 100644 index 0000000..a15c9b5 --- /dev/null +++ b/mkdocs-site/23-MetricServer/kubelet-config-1.23.yaml @@ -0,0 +1,57 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: kubelet-config-1.23 + namespace: kube-system +data: + kubelet: | + apiVersion: kubelet.config.k8s.io/v1beta1 + authentication: + anonymous: + enabled: false + webhook: + cacheTTL: 0s + enabled: true + x509: + clientCAFile: /var/lib/minikube/certs/ca.crt + authorization: + mode: Webhook + webhook: + cacheAuthorizedTTL: 0s + cacheUnauthorizedTTL: 0s + cgroupDriver: systemd + clusterDNS: + - 10.96.0.10 + clusterDomain: cluster.local + cpuManagerReconcilePeriod: 0s + evictionHard: + imagefs.available: 0% + nodefs.available: 0% + nodefs.inodesFree: 0% + evictionPressureTransitionPeriod: 0s + failSwapOn: false + fileCheckFrequency: 0s + healthzBindAddress: 127.0.0.1 + healthzPort: 10248 + httpCheckFrequency: 0s + imageGCHighThresholdPercent: 100 + imageMinimumGCAge: 0s + serverTLSBootstrap: true + kind: KubeletConfiguration + logging: + flushFrequency: 0 + options: + json: + infoBufferSize: "0" + verbosity: 0 + memorySwap: {} + nodeStatusReportFrequency: 0s + nodeStatusUpdateFrequency: 0s + rotateCertificates: true + runtimeRequestTimeout: 0s + shutdownGracePeriod: 0s + shutdownGracePeriodCriticalPods: 0s + staticPodPath: /etc/kubernetes/manifests + streamingConnectionIdleTimeout: 0s + syncFrequency: 0s + volumeStatsAggPeriod: 0s diff --git a/mkdocs-site/23-MetricServer/runMe.sh b/mkdocs-site/23-MetricServer/runMe.sh new file mode 100644 index 0000000..36c6dfc --- /dev/null +++ b/mkdocs-site/23-MetricServer/runMe.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Start minikube +minikube start + +# Download the metric-server resources +wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml + +# Apply metric-server +# We know that it will not work under minikube so we will need to fix it +kubectl apply -f components.yaml + +# Check if the metrics-server is working +# We expect to get the following error +### >>> Error from server (ServiceUnavailable): the server is currently unable to handle the request (get nodes.metrics.k8s.io) +kubectl top nodes + +kubectl get deployment metrics-server -n kube-system +# NAME READY UP-TO-DATE AVAILABLE AGE +# metrics-server 0/1 1 0 71s + +# View the error +## We should see error like this: +## "Failed to scrape node" err="Get \"https://192.168.49.2:10250/metrics/resource\": +## x509: cannot validate certificate for 192.168.49.2 because it doesn't contain any IP SANs" node="minikube" +kubectl logs -n kube-system deploy/metrics-server + +### +### Fixing the error +### +# We need to fix the tls before we can install the mertric-server + +# Get the kubelet configuration +KUBELET_CONFIG=$(kubectl get configmap -n kube-system --no-headers -o custom-columns=":metadata.name" | grep kubelet-config) +kubectl edit configmap $KUBELET_CONFIG -n kube-system + +## Add to the following configuration under the `kubelet` ConfigMap +serverTLSBootstrap: true + +# We also need to fix the metric server and add the following line under the metric-server Deploymet + +# Edit the deploymnet and add the required lines under the spec +### +### vi components.yaml (~line 140) +### +### spec: +### containers: +### - args +- --kubelet-insecure-tls + +# Stop and start minikube +minikube stop && minikube start + +# Uninstall and re-install the metrics-server +kubectl delete -f components.yaml +kubectl apply -f components.yaml + +# Verify that now the metric server is working +kubectl top nodes +kubectl top pods -A \ No newline at end of file diff --git a/mkdocs-site/24-HelmOperator/index.html b/mkdocs-site/24-HelmOperator/index.html new file mode 100644 index 0000000..ef53215 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/index.html @@ -0,0 +1,231 @@ + 22 HelmOperator - KubernetesLabs

K8S Hands-on

Visitor Badge


Helm Operator

  • An in-depth Helm-based operator tutorial.
  • The Helm Operator is a Kubernetes operator, allowing one to declaratively manage Helm chart releases.

Pre-Requirements

Open in Cloud Shell
CTRL + click to open in new window

  • Docker
  • kubectl
  • operator-sdk installed and configured
  • cluster-admin permissions

Install operator-sdk

# Grab the ARCH and OS
+export ARCH=$(case $(uname -m) in x86_64) echo -n amd64 ;; aarch64) echo -n arm64 ;; *) echo -n $(uname -m) ;; esac)
+export OS=$(uname | awk '{print tolower($0)}')
+
+# Get the desired download URL
+export OPERATOR_SDK_DL_URL=https://github.com/operator-framework/operator-sdk/releases/download/v1.23.0
+
+# Download the Operator binaries
+curl -LO ${OPERATOR_SDK_DL_URL}/operator-sdk_${OS}_${ARCH}
+
+# Install the release binary in your PATH
+chmod +x operator-sdk_${OS}_${ARCH} && sudo mv operator-sdk_${OS}_${ARCH} /usr/local/bin/operator-sdk
+

Step 01 - Create a new project

  • Use the CLI to create a new Helm-based nginx-operator project:
# Create the desired folder
+mkdir nginx-operator
+
+# Switch to the desired folder
+cd nginx-operator
+
+# Create the helm operator
+operator-sdk                \
+        init                \
+        --kind    Nginx     \
+        --group   demo      \
+        --plugins helm      \
+        --version v1alpha1  \
+        --domain  codewizard.co.il
+
  • This creates the nginx-operator project specifically for watching the Nginx resource with APIVersion demo.codewizard.co.il/v1alpha1 and Kind Nginx.

Operator SDK Project Layout

  • The command will generate the following structure:
.
+├── Dockerfile
+├── Makefile
+├── PROJECT
+├── config
+   ├── crd
+      ├── bases
+         └── demo.codewizard.co.il_nginxes.yaml
+      └── kustomization.yaml
+   ├── default
+      ├── kustomization.yaml
+      ├── manager_auth_proxy_patch.yaml
+      └── manager_config_patch.yaml
+   ├── manager
+      ├── controller_manager_config.yaml
+      ├── kustomization.yaml
+      └── manager.yaml
+   ├── manifests
+      └── kustomization.yaml
+   ├── prometheus
+      ├── kustomization.yaml
+      └── monitor.yaml
+   ├── rbac
+      ├── auth_proxy_client_clusterrole.yaml
+      ├── auth_proxy_role.yaml
+      ├── auth_proxy_role_binding.yaml
+      ├── auth_proxy_service.yaml
+      ├── kustomization.yaml
+      ├── leader_election_role.yaml
+      ├── leader_election_role_binding.yaml
+      ├── nginx_editor_role.yaml
+      ├── nginx_viewer_role.yaml
+      ├── role.yaml
+      ├── role_binding.yaml
+      └── service_account.yaml
+   ├── samples
+      ├── demo_v1alpha1_nginx.yaml
+      └── kustomization.yaml
+   └── scorecard
+       ├── bases
+          └── config.yaml
+       ├── kustomization.yaml
+       └── patches
+           ├── basic.config.yaml
+           └── olm.config.yaml
+├── helm-charts
+   └── nginx
+       ├── Chart.yaml
+       ├── templates
+          ├── NOTES.txt
+          ├── _helpers.tpl
+          ├── deployment.yaml
+          ├── hpa.yaml
+          ├── ingress.yaml
+          ├── service.yaml
+          ├── serviceaccount.yaml
+          └── tests
+              └── test-connection.yaml
+       └── values.yaml
+├── tree.txt
+└── watches.yaml
+
+16 directories, 44 files
+

Step 02 - Customize the operator logic

  • For this example the nginx-operator will execute the following reconciliation logic for each Nginx Custom Resource (CR):
  • Create an nginx Deployment, if it doesn’t exist.
  • Create an nginx Service, if it doesn’t exist.
  • Create an nginx Ingress, if it is enabled and doesn’t exist.
  • Update the Deployment, Service, and Ingress, if they already exist but don’t match the desired configuration as specified by the Nginx CR.
  • Ensure that the Deployment, Service, and optional Ingress all match the desired configuration (e.g. replica count, image, service type, etc) as specified by the Nginx CR.


Watch the Nginx CR

  • By default, the Nginx-operator watches Nginx resource events as shown in watches.yaml and executes Helm releases using the specified chart:
# Use the 'create api' subcommand to add watches to this file.
+- group: demo
+  version: v1alpha1
+  kind: Nginx
+  chart: helm-charts/nginx
+


Reviewing the Nginx Helm Chart

  • When a Helm operator project is created, the SDK creates an example Helm chart that contains a set of templates for a simple Nginx release.

  • For this example, we have templates for deployment, service, and ingress resources, along with a NOTES.txt template, which Helm chart developers use to convey helpful information about a release.


Understanding the Nginx CR spec

  • Helm uses a concept called values to provide customizations to a Helm chart’s defaults, which are defined in the Helm chart’s values.yaml file.

  • Overriding these defaults is as simple as setting the desired values in the CR spec.

  • Let’s use the number of replicas value as an example.

  • First, inspecting helm-charts/nginx/values.yaml, we can see that the chart has a value called replicaCount and it is set to 1 by default.

  • Let’s update the value to 3 - replicaCount: 3.

# Update `config/samples/demo_v1alpha1_nginx.yaml` to look like the following:
+apiVersion: demo.codewizard.co.il/v1alpha1
+kind: Nginx
+metadata:
+  name: nginx-sample
+spec:
+  #... (Around line 33)
+  replicaCount: 3 # <------- Adding our replicas count
+
  • Similarly, we see that the default service port is set to 80, but we would like to use 8888, so we will again update config/samples/demo_v1alpha1_nginx.yaml by adding the service port override.
# Update `config/samples/demo_v1alpha1_nginx.yaml` to look like the following:
+apiVersion: demo.codewizard.co.il/v1alpha1
+kind: Nginx
+metadata:
+  name: nginx-sample
+spec:
+  #... (Around line 36)
+  service:
+    port: 8888 # <------- Updating our service port
+

Step 03 - Build the operator’s image

# Login to your DockerHub / acr / ecr or any other registry account
+
+# Set the desired image name and tag
+
+# In the Makefile update the following line
+# Image URL to use all building/pushing image targets
+IMG ?= controller:latest
+
+# change it to your registry account
+IMG ?= nirgeier/helm_operator:latest
+
  • Now let’s build and push the image:
make docker-build docker-push
+

Step 04 - Deploy the operator to the cluster

make deploy
+
+# Verify that the operator is deployed
+kubectl get deployment -n nginx-operator-system
+

Step 05 - Create the custom Nginx

# Deploy the custom nginx we created earlier
+kubectl apply -f config/samples/demo_v1alpha1_nginx.yaml
+
+# Ensure that the nginx-operator created
+kubectl get deployment | grep nginx-sample
+
+# Check that we have 3 replicas as defined earlier
+kubectl get pods | grep nginx-sample
+
+# Check that the port is set to 8888
+kubectl get svc | grep nginx-sample
+

Step 06 - Check the operator logic

# Update the replicaCount and remove the port
+# Once we update the yaml we will check that the operator is working
+# and updating the desired values
+
+# Update the replicaCount in `config/samples/demo_v1alpha1_nginx.yaml`
+replicaCount: 5
+
+# Remark the service section in the yaml file
+# We wish to see that the operator will use the default values
+36   #service:
+37   #  port: 8888
+38   #  type: ClusterIP
+
  • Apply the changes:
# Apply the changes
+kubectl apply -f config/samples/demo_v1alpha1_nginx.yaml
+
  • Check to see that the operator is working as expected:
# Ensure that the nginx-operator still running
+kubectl get deployment | grep nginx-sample
+
+# Deploy the custom nginx we created earlier
+kubectl apply -f config/samples/demo_v1alpha1_nginx.yaml
+
+# Check that we have 5 replicas as defined earlier
+kubectl get pods | grep nginx-sample
+
+# Check that the port is set back to its default (80)
+kubectl get svc | grep nginx-sample
+

Step07 - Logging / Debugging

  • We can view the operator’s logs using the following command:
# View the operator logs
+kubectl logs deployment.apps/nginx-operator-controller-manager  -n nginx-operator-system -c manager
+
  • Review the CR status and events:
kubectl describe nginxes.demo.codewizard.co.il
+
\ No newline at end of file diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/Dockerfile b/mkdocs-site/24-HelmOperator/nginx-operator/Dockerfile new file mode 100644 index 0000000..9ba0967 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/Dockerfile @@ -0,0 +1,7 @@ +# Build the manager binary +FROM quay.io/operator-framework/helm-operator:v1.23.0 + +ENV HOME=/opt/helm +COPY watches.yaml ${HOME}/watches.yaml +COPY helm-charts ${HOME}/helm-charts +WORKDIR ${HOME} diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/Makefile b/mkdocs-site/24-HelmOperator/nginx-operator/Makefile new file mode 100644 index 0000000..fe51446 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/Makefile @@ -0,0 +1,194 @@ +# VERSION defines the project version for the bundle. +# Update this value when you upgrade the version of your project. +# To re-generate a bundle for another specific version without changing the standard setup, you can: +# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) +# - use environment variables to overwrite this value (e.g export VERSION=0.0.2) +VERSION ?= 0.0.1 + +# CHANNELS define the bundle channels used in the bundle. +# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") +# To re-generate a bundle for other specific channels without changing the standard setup, you can: +# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) +# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") +ifneq ($(origin CHANNELS), undefined) +BUNDLE_CHANNELS := --channels=$(CHANNELS) +endif + +# DEFAULT_CHANNEL defines the default channel used in the bundle. +# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") +# To re-generate a bundle for any other default channel without changing the default setup, you can: +# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) +# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") +ifneq ($(origin DEFAULT_CHANNEL), undefined) +BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) +endif +BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) + +# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. +# This variable is used to construct full image tags for bundle and catalog images. +# +# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both +# codewizard.co.il/nginx-operator-bundle:$VERSION and codewizard.co.il/nginx-operator-catalog:$VERSION. +IMAGE_TAG_BASE ?= codewizard.co.il/nginx-operator + +# BUNDLE_IMG defines the image:tag used for the bundle. +# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) +BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION) + +# BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command +BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) + +# USE_IMAGE_DIGESTS defines if images are resolved via tags or digests +# You can enable this value if you would like to use SHA Based Digests +# To enable set flag to true +USE_IMAGE_DIGESTS ?= false +ifeq ($(USE_IMAGE_DIGESTS), true) + BUNDLE_GEN_FLAGS += --use-image-digests +endif + +# Image URL to use all building/pushing image targets +#IMG ?= controller:latest +IMG ?= nirgeier/helm_operator:latest + +.PHONY: all +all: docker-build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Build + +.PHONY: run +run: helm-operator ## Run against the configured Kubernetes cluster in ~/.kube/config + $(HELM_OPERATOR) run + +.PHONY: docker-build +docker-build: ## Build docker image with the manager. + docker build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +##@ Deployment + +.PHONY: install +install: kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.PHONY: uninstall +uninstall: kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | kubectl delete -f - + +.PHONY: deploy +deploy: kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. + cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + $(KUSTOMIZE) build config/default | kubectl apply -f - + +.PHONY: undeploy +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/default | kubectl delete -f - + +OS := $(shell uname -s | tr '[:upper:]' '[:lower:]') +ARCH := $(shell uname -m | sed 's/x86_64/amd64/') + +.PHONY: kustomize +KUSTOMIZE = $(shell pwd)/bin/kustomize +kustomize: ## Download kustomize locally if necessary. +ifeq (,$(wildcard $(KUSTOMIZE))) +ifeq (,$(shell which kustomize 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p $(dir $(KUSTOMIZE)) ;\ + curl -sSLo - https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/v4.5.5/kustomize_v4.5.5_$(OS)_$(ARCH).tar.gz | \ + tar xzf - -C bin/ ;\ + } +else +KUSTOMIZE = $(shell which kustomize) +endif +endif + +.PHONY: helm-operator +HELM_OPERATOR = $(shell pwd)/bin/helm-operator +helm-operator: ## Download helm-operator locally if necessary, preferring the $(pwd)/bin path over global if both exist. +ifeq (,$(wildcard $(HELM_OPERATOR))) +ifeq (,$(shell which helm-operator 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p $(dir $(HELM_OPERATOR)) ;\ + curl -sSLo $(HELM_OPERATOR) https://github.com/operator-framework/operator-sdk/releases/download/v1.23.0/helm-operator_$(OS)_$(ARCH) ;\ + chmod +x $(HELM_OPERATOR) ;\ + } +else +HELM_OPERATOR = $(shell which helm-operator) +endif +endif + +.PHONY: bundle +bundle: kustomize ## Generate bundle manifests and metadata, then validate generated files. + operator-sdk generate kustomize manifests -q + cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG) + $(KUSTOMIZE) build config/manifests | operator-sdk generate bundle -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS) + operator-sdk bundle validate ./bundle + +.PHONY: bundle-build +bundle-build: ## Build the bundle image. + docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) . + +.PHONY: bundle-push +bundle-push: ## Push the bundle image. + $(MAKE) docker-push IMG=$(BUNDLE_IMG) + +.PHONY: opm +OPM = ./bin/opm +opm: ## Download opm locally if necessary. +ifeq (,$(wildcard $(OPM))) +ifeq (,$(shell which opm 2>/dev/null)) + @{ \ + set -e ;\ + mkdir -p $(dir $(OPM)) ;\ + curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.23.0/$(OS)-$(ARCH)-opm ;\ + chmod +x $(OPM) ;\ + } +else +OPM = $(shell which opm) +endif +endif + +# A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0). +# These images MUST exist in a registry and be pull-able. +BUNDLE_IMGS ?= $(BUNDLE_IMG) + +# The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0). +CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION) + +# Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image. +ifneq ($(origin CATALOG_BASE_IMG), undefined) +FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG) +endif + +# Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'. +# This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see: +# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator +.PHONY: catalog-build +catalog-build: opm ## Build a catalog image. + $(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT) + +# Push the catalog image. +.PHONY: catalog-push +catalog-push: ## Push a catalog image. + $(MAKE) docker-push IMG=$(CATALOG_IMG) diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/PROJECT b/mkdocs-site/24-HelmOperator/nginx-operator/PROJECT new file mode 100644 index 0000000..e38f748 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/PROJECT @@ -0,0 +1,16 @@ +domain: codewizard.co.il +layout: +- helm.sdk.operatorframework.io/v1 +plugins: + manifests.sdk.operatorframework.io/v2: {} + scorecard.sdk.operatorframework.io/v2: {} +projectName: nginx-operator +resources: +- api: + crdVersion: v1 + namespaced: true + domain: codewizard.co.il + group: demo + kind: Nginx + version: v1alpha1 +version: "3" diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/crd/bases/demo.codewizard.co.il_nginxes.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/crd/bases/demo.codewizard.co.il_nginxes.yaml new file mode 100644 index 0000000..e9d42e0 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/crd/bases/demo.codewizard.co.il_nginxes.yaml @@ -0,0 +1,44 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: nginxes.demo.codewizard.co.il +spec: + group: demo.codewizard.co.il + names: + kind: Nginx + listKind: NginxList + plural: nginxes + singular: nginx + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Nginx is the Schema for the nginxes API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of Nginx + type: object + x-kubernetes-preserve-unknown-fields: true + status: + description: Status defines the observed state of Nginx + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + served: true + storage: true + subresources: + status: {} diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/crd/kustomization.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/crd/kustomization.yaml new file mode 100644 index 0000000..3083ec8 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/crd/kustomization.yaml @@ -0,0 +1,6 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/demo.codewizard.co.il_nginxes.yaml +#+kubebuilder:scaffold:crdkustomizeresource diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/default/kustomization.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/default/kustomization.yaml new file mode 100644 index 0000000..7237ccb --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/default/kustomization.yaml @@ -0,0 +1,32 @@ +# Adds namespace to all resources. +namespace: nginx-operator-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: nginx-operator- + +# Labels to add to all resources and selectors. +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue + +resources: +- ../crd +- ../rbac +- ../manager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: +# Protect the /metrics endpoint by putting it behind auth. +# If you want your controller-manager to expose the /metrics +# endpoint w/o any authn/z, please comment the following line. +- manager_auth_proxy_patch.yaml + +# Mount the controller config file for loading manager configurations +# through a ComponentConfig type +#- manager_config_patch.yaml diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/default/manager_auth_proxy_patch.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 0000000..83d82f4 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,40 @@ +# This patch inject a sidecar container which is a HTTP proxy for the +# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.13.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" + - "--leader-election-id=nginx-operator" diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/default/manager_config_patch.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/default/manager_config_patch.yaml new file mode 100644 index 0000000..6c40015 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/default/manager_config_patch.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + args: + - "--config=controller_manager_config.yaml" + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + volumes: + - name: manager-config + configMap: + name: manager-config diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/manager/controller_manager_config.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/manager/controller_manager_config.yaml new file mode 100644 index 0000000..b4a5352 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/manager/controller_manager_config.yaml @@ -0,0 +1,20 @@ +apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 +kind: ControllerManagerConfig +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: 127.0.0.1:8080 + +leaderElection: + leaderElect: true + resourceName: 811c9dc5.codewizard.co.il +# leaderElectionReleaseOnCancel defines if the leader should step down volume +# when the Manager ends. This requires the binary to immediately end when the +# Manager is stopped, otherwise, this setting is unsafe. Setting this significantly +# speeds up voluntary leader transitions as the new leader don't have to wait +# LeaseDuration time first. +# In the default scaffold provided, the program ends immediately after +# the manager stops, so would be fine to enable this option. However, +# if you are doing or is intended to do any operation such as perform cleanups +# after the manager stops then its usage might be unsafe. +# leaderElectionReleaseOnCancel: true diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/manager/kustomization.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/manager/kustomization.yaml new file mode 100644 index 0000000..ac29ae6 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/manager/kustomization.yaml @@ -0,0 +1,16 @@ +resources: +- manager.yaml + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: +- files: + - controller_manager_config.yaml + name: manager-config +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: nirgeier/helm_operator + newTag: latest diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/manager/manager.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/manager/manager.yaml new file mode 100644 index 0000000..28ad41f --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/manager/manager.yaml @@ -0,0 +1,69 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - args: + - --leader-elect + - --leader-election-id=nginx-operator + image: nirgeier:helm_operator + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/manifests/kustomization.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/manifests/kustomization.yaml new file mode 100644 index 0000000..39cb5ff --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/manifests/kustomization.yaml @@ -0,0 +1,7 @@ +# These resources constitute the fully configured set of manifests +# used to generate the 'manifests/' directory in a bundle. +resources: +- bases/nginx-operator.clusterserviceversion.yaml +- ../default +- ../samples +- ../scorecard diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/prometheus/kustomization.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/prometheus/kustomization.yaml new file mode 100644 index 0000000..ed13716 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/prometheus/monitor.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/prometheus/monitor.yaml new file mode 100644 index 0000000..d19136a --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/prometheus/monitor.yaml @@ -0,0 +1,20 @@ + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true + selector: + matchLabels: + control-plane: controller-manager diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_client_clusterrole.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 0000000..51a75db --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_role.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_role.yaml new file mode 100644 index 0000000..80e1857 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_role_binding.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 0000000..ec7acc0 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_service.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_service.yaml new file mode 100644 index 0000000..71f1797 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/kustomization.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/kustomization.yaml new file mode 100644 index 0000000..731832a --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/kustomization.yaml @@ -0,0 +1,18 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 4 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml +- auth_proxy_client_clusterrole.yaml diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/leader_election_role.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/leader_election_role.yaml new file mode 100644 index 0000000..4190ec8 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/leader_election_role.yaml @@ -0,0 +1,37 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/leader_election_role_binding.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 0000000..1d1321e --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/nginx_editor_role.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/nginx_editor_role.yaml new file mode 100644 index 0000000..580dfc8 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/nginx_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit nginxes. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: nginx-editor-role +rules: +- apiGroups: + - demo.codewizard.co.il + resources: + - nginxes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - demo.codewizard.co.il + resources: + - nginxes/status + verbs: + - get diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/nginx_viewer_role.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/nginx_viewer_role.yaml new file mode 100644 index 0000000..e09e946 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/nginx_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view nginxes. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: nginx-viewer-role +rules: +- apiGroups: + - demo.codewizard.co.il + resources: + - nginxes + verbs: + - get + - list + - watch +- apiGroups: + - demo.codewizard.co.il + resources: + - nginxes/status + verbs: + - get diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/role.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/role.yaml new file mode 100644 index 0000000..6b236f1 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/role.yaml @@ -0,0 +1,83 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: manager-role +rules: +## +## Base operator rules +## +# We need to get namespaces so the operator can read namespaces to ensure they exist +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get +# We need to manage Helm release secrets +- apiGroups: + - "" + resources: + - secrets + verbs: + - "*" +# We need to create events on CRs about things happening during reconciliation +- apiGroups: + - "" + resources: + - events + verbs: + - create + +## +## Rules for demo.codewizard.co.il/v1alpha1, Kind: Nginx +## +- apiGroups: + - demo.codewizard.co.il + resources: + - nginxes + - nginxes/status + - nginxes/finalizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - pods + - services + - services/finalizers + - endpoints + - persistentvolumeclaims + - events + - configmaps + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - deployments + - daemonsets + - replicasets + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + +#+kubebuilder:scaffold:rules diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/role_binding.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/role_binding.yaml new file mode 100644 index 0000000..2070ede --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/service_account.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/service_account.yaml new file mode 100644 index 0000000..7cd6025 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/rbac/service_account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: controller-manager + namespace: system diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/samples/demo_v1alpha1_nginx.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/samples/demo_v1alpha1_nginx.yaml new file mode 100644 index 0000000..e2d12a5 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/samples/demo_v1alpha1_nginx.yaml @@ -0,0 +1,45 @@ +apiVersion: demo.codewizard.co.il/v1alpha1 +kind: Nginx +metadata: + name: nginx-sample +spec: + # Default values copied from /helm-charts/nginx/values.yaml + affinity: {} + autoscaling: + enabled: false + maxReplicas: 100 + minReplicas: 1 + targetCPUUtilizationPercentage: 80 + fullnameOverride: "" + image: + pullPolicy: IfNotPresent + repository: nginx + tag: "" + imagePullSecrets: [] + ingress: + annotations: {} + className: "" + enabled: false + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + nameOverride: "" + nodeSelector: {} + podAnnotations: {} + podSecurityContext: {} + replicaCount: 3 # <----- Our desired value + resources: {} + securityContext: {} + service: + port: 8888 # <----- Our desired value + type: ClusterIP + serviceAccount: + annotations: {} + create: true + name: "" + tolerations: [] + + diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/samples/kustomization.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/samples/kustomization.yaml new file mode 100644 index 0000000..3d40f0c --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/samples/kustomization.yaml @@ -0,0 +1,4 @@ +## Append samples you want in your CSV to this file as resources ## +resources: +- demo_v1alpha1_nginx.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/bases/config.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/bases/config.yaml new file mode 100644 index 0000000..c770478 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/bases/config.yaml @@ -0,0 +1,7 @@ +apiVersion: scorecard.operatorframework.io/v1alpha3 +kind: Configuration +metadata: + name: config +stages: +- parallel: true + tests: [] diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/kustomization.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/kustomization.yaml new file mode 100644 index 0000000..50cd2d0 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/kustomization.yaml @@ -0,0 +1,16 @@ +resources: +- bases/config.yaml +patchesJson6902: +- path: patches/basic.config.yaml + target: + group: scorecard.operatorframework.io + version: v1alpha3 + kind: Configuration + name: config +- path: patches/olm.config.yaml + target: + group: scorecard.operatorframework.io + version: v1alpha3 + kind: Configuration + name: config +#+kubebuilder:scaffold:patchesJson6902 diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/patches/basic.config.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/patches/basic.config.yaml new file mode 100644 index 0000000..90f7ef7 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/patches/basic.config.yaml @@ -0,0 +1,10 @@ +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - basic-check-spec + image: quay.io/operator-framework/scorecard-test:v1.23.0 + labels: + suite: basic + test: basic-check-spec-test diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/patches/olm.config.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/patches/olm.config.yaml new file mode 100644 index 0000000..b55840e --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/config/scorecard/patches/olm.config.yaml @@ -0,0 +1,50 @@ +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-bundle-validation + image: quay.io/operator-framework/scorecard-test:v1.23.0 + labels: + suite: olm + test: olm-bundle-validation-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-crds-have-validation + image: quay.io/operator-framework/scorecard-test:v1.23.0 + labels: + suite: olm + test: olm-crds-have-validation-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-crds-have-resources + image: quay.io/operator-framework/scorecard-test:v1.23.0 + labels: + suite: olm + test: olm-crds-have-resources-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-spec-descriptors + image: quay.io/operator-framework/scorecard-test:v1.23.0 + labels: + suite: olm + test: olm-spec-descriptors-test +- op: add + path: /stages/0/tests/- + value: + entrypoint: + - scorecard-test + - olm-status-descriptors + image: quay.io/operator-framework/scorecard-test:v1.23.0 + labels: + suite: olm + test: olm-status-descriptors-test diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/Chart.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/Chart.yaml new file mode 100644 index 0000000..e4a5333 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +appVersion: 1.16.0 +description: A Helm chart for Kubernetes +name: nginx +type: application +version: 0.1.0 diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/NOTES.txt b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/NOTES.txt new file mode 100644 index 0000000..918bb64 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "nginx.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "nginx.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "nginx.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "nginx.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/_helpers.tpl b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/_helpers.tpl new file mode 100644 index 0000000..ad9f432 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nginx.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "nginx.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nginx.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nginx.labels" -}} +helm.sh/chart: {{ include "nginx.chart" . }} +{{ include "nginx.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nginx.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nginx.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "nginx.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "nginx.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/deployment.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/deployment.yaml new file mode 100644 index 0000000..27a46f8 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nginx.fullname" . }} + labels: + {{- include "nginx.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "nginx.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "nginx.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "nginx.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/hpa.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/hpa.yaml new file mode 100644 index 0000000..9af6c1b --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "nginx.fullname" . }} + labels: + {{- include "nginx.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "nginx.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/ingress.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/ingress.yaml new file mode 100644 index 0000000..fc6576a --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "nginx.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "nginx.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/service.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/service.yaml new file mode 100644 index 0000000..7183073 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nginx.fullname" . }} + labels: + {{- include "nginx.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "nginx.selectorLabels" . | nindent 4 }} diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/serviceaccount.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/serviceaccount.yaml new file mode 100644 index 0000000..3650603 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "nginx.serviceAccountName" . }} + labels: + {{- include "nginx.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/tests/test-connection.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/tests/test-connection.yaml new file mode 100644 index 0000000..5879b4f --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "nginx.fullname" . }}-test-connection" + labels: + {{- include "nginx.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "nginx.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/values.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/values.yaml new file mode 100644 index 0000000..e7d6408 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/helm-charts/nginx/values.yaml @@ -0,0 +1,82 @@ +# Default values for nginx. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/mkdocs-site/24-HelmOperator/nginx-operator/watches.yaml b/mkdocs-site/24-HelmOperator/nginx-operator/watches.yaml new file mode 100644 index 0000000..3922bc1 --- /dev/null +++ b/mkdocs-site/24-HelmOperator/nginx-operator/watches.yaml @@ -0,0 +1,6 @@ +# Use the 'create api' subcommand to add watches to this file. +- group: demo.codewizard.co.il + version: v1alpha1 + kind: Nginx + chart: helm-charts/nginx +#+kubebuilder:scaffold:watch diff --git a/mkdocs-site/25-kubebuilder/runMe.sh b/mkdocs-site/25-kubebuilder/runMe.sh new file mode 100644 index 0000000..498eee8 --- /dev/null +++ b/mkdocs-site/25-kubebuilder/runMe.sh @@ -0,0 +1,4 @@ +kubebuilder init \ +--domain my.domain \ +--repo my.domain/guestbook \ +--plugins=kustomize/v2-alpha diff --git a/mkdocs-site/26-k9s/runMe.sh b/mkdocs-site/26-k9s/runMe.sh new file mode 100644 index 0000000..1494836 --- /dev/null +++ b/mkdocs-site/26-k9s/runMe.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Install k8s +curl -sS https://webinstall.dev/k9s | bash + + +# Shortcutrs + +# List all api-resources +CTRL + A + +# switch resource type +: + +# Filters: +/ diff --git a/mkdocs-site/27-krew/runMe.sh b/mkdocs-site/27-krew/runMe.sh new file mode 100644 index 0000000..661e085 --- /dev/null +++ b/mkdocs-site/27-krew/runMe.sh @@ -0,0 +1,32 @@ +#!/bin/bash +( + set -x; cd "$(mktemp -d)" && + OS="$(uname | tr '[:upper:]' '[:lower:]')" && + ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" && + KREW="krew-${OS}_${ARCH}" && + curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" && + tar zxvf "${KREW}.tar.gz" && + ./"${KREW}" install krew +) + +export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH" + +kubectl krew update +kubectl krew install \ + access-matrix \ + blame \ + count \ + debug-shell \ + get-all \ + ingress-rule \ + minio \ + modify-secret \ + node-admin \ + node-shell \ + pod-inspect \ + resource-capacity \ + sshd \ + view-cert \ + view-secret \ + view-utilization + diff --git a/mkdocs-site/28-kubeapps/runMe.sh b/mkdocs-site/28-kubeapps/runMe.sh new file mode 100644 index 0000000..529a496 --- /dev/null +++ b/mkdocs-site/28-kubeapps/runMe.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Install the helm package +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update +helm install -n kubeapps --create-namespace kubeapps bitnami/kubeapps + +# Create the namespace +kubectl create ns codewizard + +# Create the required service accountt +kubectl create \ + serviceaccount kubeapps-operator \ + -n codewizard + +# Create the required role binding +kubectl create \ + clusterrolebinding kubeapps-operator \ + --serviceaccount=codewizard:kubeapps-operator \ + --clusterrole=cluster-admin + +# Apply the secret +# It will generate the token for us +cat < KubernetesLabs

404 - Not found

\ No newline at end of file diff --git a/mkdocs-site/assets/images/Kubernetes-Logo.wine.png b/mkdocs-site/assets/images/Kubernetes-Logo.wine.png new file mode 100644 index 0000000..b7689d9 Binary files /dev/null and b/mkdocs-site/assets/images/Kubernetes-Logo.wine.png differ diff --git a/mkdocs-site/assets/images/devops_cycle.jpg b/mkdocs-site/assets/images/devops_cycle.jpg new file mode 100644 index 0000000..698614f Binary files /dev/null and b/mkdocs-site/assets/images/devops_cycle.jpg differ diff --git a/mkdocs-site/assets/images/favicon.png b/mkdocs-site/assets/images/favicon.png new file mode 100644 index 0000000..1cf13b9 Binary files /dev/null and b/mkdocs-site/assets/images/favicon.png differ diff --git a/mkdocs-site/assets/images/k8s-istio-gcp.png b/mkdocs-site/assets/images/k8s-istio-gcp.png new file mode 100644 index 0000000..1d44af5 Binary files /dev/null and b/mkdocs-site/assets/images/k8s-istio-gcp.png differ diff --git a/mkdocs-site/assets/images/k8s-logos.png b/mkdocs-site/assets/images/k8s-logos.png new file mode 100644 index 0000000..72a929b Binary files /dev/null and b/mkdocs-site/assets/images/k8s-logos.png differ diff --git a/mkdocs-site/assets/images/lab.jpg b/mkdocs-site/assets/images/lab.jpg new file mode 100644 index 0000000..37243f1 Binary files /dev/null and b/mkdocs-site/assets/images/lab.jpg differ diff --git a/mkdocs-site/assets/images/next.png b/mkdocs-site/assets/images/next.png new file mode 100644 index 0000000..9955fab Binary files /dev/null and b/mkdocs-site/assets/images/next.png differ diff --git a/mkdocs-site/assets/images/prev.png b/mkdocs-site/assets/images/prev.png new file mode 100644 index 0000000..11c7564 Binary files /dev/null and b/mkdocs-site/assets/images/prev.png differ diff --git a/mkdocs-site/assets/images/statefulSet.png b/mkdocs-site/assets/images/statefulSet.png new file mode 100644 index 0000000..de9bc66 Binary files /dev/null and b/mkdocs-site/assets/images/statefulSet.png differ diff --git a/mkdocs-site/assets/javascripts/bundle.50899def.min.js b/mkdocs-site/assets/javascripts/bundle.50899def.min.js new file mode 100644 index 0000000..ba1c1c3 --- /dev/null +++ b/mkdocs-site/assets/javascripts/bundle.50899def.min.js @@ -0,0 +1,16 @@ +"use strict";(()=>{var Wi=Object.create;var gr=Object.defineProperty;var Vi=Object.getOwnPropertyDescriptor;var Di=Object.getOwnPropertyNames,Vt=Object.getOwnPropertySymbols,zi=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,ao=Object.prototype.propertyIsEnumerable;var io=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,$=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&io(e,r,t[r]);if(Vt)for(var r of Vt(t))ao.call(t,r)&&io(e,r,t[r]);return e};var so=(e,t)=>{var r={};for(var o in e)yr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Vt)for(var o of Vt(e))t.indexOf(o)<0&&ao.call(e,o)&&(r[o]=e[o]);return r};var xr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Ni=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Di(t))!yr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=Vi(t,n))||o.enumerable});return e};var Lt=(e,t,r)=>(r=e!=null?Wi(zi(e)):{},Ni(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var co=(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 lo=xr((Er,po)=>{(function(e,t){typeof Er=="object"&&typeof po!="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(k){return!!(k&&k!==document&&k.nodeName!=="HTML"&&k.nodeName!=="BODY"&&"classList"in k&&"contains"in k.classList)}function p(k){var ft=k.type,qe=k.tagName;return!!(qe==="INPUT"&&a[ft]&&!k.readOnly||qe==="TEXTAREA"&&!k.readOnly||k.isContentEditable)}function c(k){k.classList.contains("focus-visible")||(k.classList.add("focus-visible"),k.setAttribute("data-focus-visible-added",""))}function l(k){k.hasAttribute("data-focus-visible-added")&&(k.classList.remove("focus-visible"),k.removeAttribute("data-focus-visible-added"))}function f(k){k.metaKey||k.altKey||k.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(k){o=!1}function d(k){s(k.target)&&(o||p(k.target))&&c(k.target)}function y(k){s(k.target)&&(k.target.classList.contains("focus-visible")||k.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(k.target))}function L(k){document.visibilityState==="hidden"&&(n&&(o=!0),X())}function X(){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 ee(){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(k){k.target.nodeName&&k.target.nodeName.toLowerCase()==="html"||(o=!1,ee())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",L,!0),X(),r.addEventListener("focus",d,!0),r.addEventListener("blur",y,!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 qr=xr((dy,On)=>{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var $a=/["'&<>]/;On.exports=Pa;function Pa(e){var t=""+e,r=$a.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Rt=="object"&&typeof Yr=="object"?Yr.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 Ui}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(D){try{return document.execCommand(D)}catch(A){return!1}}var d=function(A){var M=f()(A);return u("cut"),M},y=d;function L(D){var A=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[A?"right":"left"]="-9999px";var F=window.pageYOffset||document.documentElement.scrollTop;return M.style.top="".concat(F,"px"),M.setAttribute("readonly",""),M.value=D,M}var X=function(A,M){var F=L(A);M.container.appendChild(F);var V=f()(F);return u("copy"),F.remove(),V},ee=function(A){var M=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},F="";return typeof A=="string"?F=X(A,M):A instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(A==null?void 0:A.type)?F=X(A.value,M):(F=f()(A),u("copy")),F},J=ee;function k(D){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?k=function(M){return typeof M}:k=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},k(D)}var ft=function(){var A=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=A.action,F=M===void 0?"copy":M,V=A.container,Y=A.target,$e=A.text;if(F!=="copy"&&F!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&k(Y)==="object"&&Y.nodeType===1){if(F==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(F==="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($e)return J($e,{container:V});if(Y)return F==="cut"?y(Y):J(Y,{container:V})},qe=ft;function Fe(D){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Fe=function(M){return typeof M}:Fe=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},Fe(D)}function ki(D,A){if(!(D instanceof A))throw new TypeError("Cannot call a class as a function")}function no(D,A){for(var M=0;M0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof V.action=="function"?V.action:this.defaultAction,this.target=typeof V.target=="function"?V.target:this.defaultTarget,this.text=typeof V.text=="function"?V.text:this.defaultText,this.container=Fe(V.container)==="object"?V.container:document.body}},{key:"listenClick",value:function(V){var Y=this;this.listener=c()(V,"click",function($e){return Y.onClick($e)})}},{key:"onClick",value:function(V){var Y=V.delegateTarget||V.currentTarget,$e=this.action(Y)||"copy",Wt=qe({action:$e,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Wt?"success":"error",{action:$e,text:Wt,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(V){return vr("action",V)}},{key:"defaultTarget",value:function(V){var Y=vr("target",V);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(V){return vr("text",V)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(V){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(V,Y)}},{key:"cut",value:function(V){return y(V)}},{key:"isSupported",value:function(){var V=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof V=="string"?[V]:V,$e=!!document.queryCommandSupported;return Y.forEach(function(Wt){$e=$e&&!!document.queryCommandSupported(Wt)}),$e}}]),M}(s()),Ui=Fi},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,d,y){var L=c.apply(this,arguments);return l.addEventListener(u,L,y),{destroy:function(){l.removeEventListener(u,L,y)}}}function p(l,f,u,d,y){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(L){return s(L,f,u,d,y)}))}function c(l,f,u,d){return function(y){y.delegateTarget=a(y.target,f),y.delegateTarget&&d.call(l,y)}}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,d,y){if(!u&&!d&&!y)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(y))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,d,y);if(a.nodeList(u))return l(u,d,y);if(a.string(u))return f(u,d,y);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,d,y){return u.addEventListener(d,y),{destroy:function(){u.removeEventListener(d,y)}}}function l(u,d,y){return Array.prototype.forEach.call(u,function(L){L.addEventListener(d,y)}),{destroy:function(){Array.prototype.forEach.call(u,function(L){L.removeEventListener(d,y)})}}}function f(u,d,y){return s(document.body,u,d,y)}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;p0&&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 z(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||p(d,L)})},y&&(n[d]=y(n[d])))}function p(d,y){try{c(o[d](y))}catch(L){u(i[0][3],L)}}function c(d){d.value instanceof nt?Promise.resolve(d.value.v).then(l,f):u(i[0][2],d)}function l(d){p("next",d)}function f(d){p("throw",d)}function u(d,y){d(y),i.shift(),i.length&&p(i[0][0],i[0][1])}}function uo(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 he=="function"?he(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 H(e){return typeof e=="function"}function ut(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=ut(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 Ue=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=he(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(L){t={error:L}}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(H(l))try{l()}catch(L){i=L instanceof zt?L.errors:[L]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=he(f),d=u.next();!d.done;d=u.next()){var y=d.value;try{ho(y)}catch(L){i=i!=null?i:[],L instanceof zt?i=q(q([],z(i)),z(L.errors)):i.push(L)}}}catch(L){o={error:L}}finally{try{d&&!d.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)ho(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=Ue.EMPTY;function Nt(e){return e instanceof Ue||e&&"closed"in e&&H(e.remove)&&H(e.add)&&H(e.unsubscribe)}function ho(e){H(e)?e():e.unsubscribe()}var Pe={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var dt={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 Ue(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 j;return r.source=this,r},t.create=function(r,o){return new To(r,o)},t}(j);var To=function(e){oe(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){oe(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 _t={now:function(){return(_t.delegate||Date).now()},delegate:void 0};var At=function(e){oe(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=_t);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}(gt);var Lo=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(yt);var kr=new Lo(Oo);var Mo=function(e){oe(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=vt.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&&o===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(vt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(gt);var _o=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o;r?o=r.id:(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}(yt);var me=new _o(Mo);var S=new j(function(e){return e.complete()});function Kt(e){return e&&H(e.schedule)}function Hr(e){return e[e.length-1]}function Xe(e){return H(Hr(e))?e.pop():void 0}function ke(e){return Kt(Hr(e))?e.pop():void 0}function Yt(e,t){return typeof Hr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Bt(e){return H(e==null?void 0:e.then)}function Gt(e){return H(e[bt])}function Jt(e){return Symbol.asyncIterator&&H(e==null?void 0:e[Symbol.asyncIterator])}function Xt(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 Zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Zt=Zi();function er(e){return H(e==null?void 0:e[Zt])}function tr(e){return fo(this,arguments,function(){var r,o,n,i;return Dt(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 rr(e){return H(e==null?void 0:e.getReader)}function U(e){if(e instanceof j)return e;if(e!=null){if(Gt(e))return ea(e);if(xt(e))return ta(e);if(Bt(e))return ra(e);if(Jt(e))return Ao(e);if(er(e))return oa(e);if(rr(e))return na(e)}throw Xt(e)}function ea(e){return new j(function(t){var r=e[bt]();if(H(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function ta(e){return new j(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?Ve(t):Qo(function(){return new nr}))}}function jr(e){return e<=0?function(){return S}:E(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,d=0,y=!1,L=!1,X=function(){f==null||f.unsubscribe(),f=void 0},ee=function(){X(),l=u=void 0,y=L=!1},J=function(){var k=l;ee(),k==null||k.unsubscribe()};return E(function(k,ft){d++,!L&&!y&&X();var qe=u=u!=null?u:r();ft.add(function(){d--,d===0&&!L&&!y&&(f=Ur(J,p))}),qe.subscribe(ft),!l&&d>0&&(l=new at({next:function(Fe){return qe.next(Fe)},error:function(Fe){L=!0,X(),f=Ur(ee,n,Fe),qe.error(Fe)},complete:function(){y=!0,X(),f=Ur(ee,a),qe.complete()}}),U(k).subscribe(l))})(c)}}function Ur(e,t){for(var r=[],o=2;oe.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function R(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 Ie(){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 wa=O(h(document.body,"focusin"),h(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Ie()||document.body),G(1));function et(e){return wa.pipe(m(t=>e.contains(t)),K())}function Ht(e,t){return C(()=>O(h(e,"mouseenter").pipe(m(()=>!0)),h(e,"mouseleave").pipe(m(()=>!1))).pipe(t?kt(r=>Le(+!r*t)):le,Q(e.matches(":hover"))))}function Jo(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)Jo(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)Jo(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),O(h(t,"load"),h(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),_(()=>document.head.removeChild(t)),Te(1))))}var Xo=new g,Ta=C(()=>typeof ResizeObserver=="undefined"?wt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Xo.next(t)))),v(e=>O(Ye,I(e)).pipe(_(()=>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 Ta.pipe(w(r=>r.observe(t)),v(r=>Xo.pipe(b(o=>o.target===t),_(()=>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 Zo(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 De(e){return{x:e.offsetLeft,y:e.offsetTop}}function en(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function tn(e){return O(h(window,"load"),h(window,"resize")).pipe(Me(0,me),m(()=>De(e)),Q(De(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function ze(e){return O(h(e,"scroll"),h(window,"scroll"),h(window,"resize")).pipe(Me(0,me),m(()=>pr(e)),Q(pr(e)))}var rn=new g,Sa=C(()=>I(new IntersectionObserver(e=>{for(let t of e)rn.next(t)},{threshold:0}))).pipe(v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function tt(e){return Sa.pipe(w(t=>t.observe(e)),v(t=>rn.pipe(b(({target:r})=>r===e),_(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function on(e,t=16){return ze(e).pipe(m(({y:r})=>{let o=ce(e),n=Tt(e);return r>=n.height-o.height-t}),K())}var lr={drawer:R("[data-md-toggle=drawer]"),search:R("[data-md-toggle=search]")};function nn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function Ne(e){let t=lr[e];return h(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function Oa(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 La(){return O(h(window,"compositionstart").pipe(m(()=>!0)),h(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function an(){let e=h(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:nn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Ie();if(typeof o!="undefined")return!Oa(o,r)}return!0}),pe());return La().pipe(v(t=>t?S:e))}function ye(){return new URL(location.href)}function lt(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 sn(){return new g}function cn(){return location.hash.slice(1)}function pn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Ma(e){return O(h(window,"hashchange"),e).pipe(m(cn),Q(cn()),b(t=>t.length>0),G(1))}function ln(e){return Ma(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function $t(e){let t=matchMedia(e);return ir(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function mn(){let e=matchMedia("print");return O(h(window,"beforeprint").pipe(m(()=>!0)),h(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function zr(e,t){return e.pipe(v(r=>r?t():S))}function Nr(e,t){return new j(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 je(e,t){return Nr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),G(1))}function fn(e,t){let r=new DOMParser;return Nr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),G(1))}function un(e,t){let r=new DOMParser;return Nr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),G(1))}function dn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function hn(){return O(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(m(dn),Q(dn()))}function bn(){return{width:innerWidth,height:innerHeight}}function vn(){return h(window,"resize",{passive:!0}).pipe(m(bn),Q(bn()))}function gn(){return N([hn(),vn()]).pipe(m(([e,t])=>({offset:e,size:t})),G(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(te("size")),n=N([o,r]).pipe(m(()=>De(e)));return N([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 _a(e){return h(e,"message",t=>t.data)}function Aa(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function yn(e,t=new Worker(e)){let r=_a(t),o=Aa(t),n=new g;n.subscribe(o);let i=o.pipe(Z(),ie(!0));return n.pipe(Z(),Re(r.pipe(W(i))),pe())}var Ca=R("#__config"),St=JSON.parse(Ca.textContent);St.base=`${new URL(St.base,ye())}`;function xe(){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 R(`[data-md-component=${e}]`,t)}function ae(e,t=document){return P(`[data-md-component=${e}]`,t)}function ka(e){let t=R(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(m(()=>R(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function xn(e){if(!B("announce.dismiss")||!e.childElementCount)return S;if(!e.hidden){let t=R(".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)}),ka(e).pipe(w(r=>t.next(r)),_(()=>t.complete()),m(r=>$({ref:e},r)))})}function Ha(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function En(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Ha(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({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 wn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function Tn(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 Sn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}var Ln=Lt(qr());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,(0,Ln.default)(c))," "],[]).slice(0,-1),i=xe(),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}=xe();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&&x("nav",{class:"md-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 Mn(e){let t=e[0].score,r=[...e],o=xe(),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 _n(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 Kr(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 An(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ra(e){var o;let t=xe(),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 Cn(e,t){var o;let r=xe();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(Ra)))}var Ia=0;function ja(e){let t=N([et(e),Ht(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Zo(e)).pipe(ne(ze),pt(1),He(t),m(()=>en(e)));return t.pipe(Ae(o=>o),v(()=>N([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function Fa(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ia++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(Z(),ie(!1)).subscribe(a);let s=a.pipe(kt(c=>Le(+!c*250,kr)),K(),v(c=>c?r:S),w(c=>c.id=n),pe());N([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>Ht(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),re(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),d=u.width/2;if(l.role==="tooltip")return{x:d,y:8+u.height};if(u.y>=f.height/2){let{height:y}=ce(l);return{x:d,y:-16-y}}else return{x:d,y:16+u.height}}));return N([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),re(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(R(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),ve(me),re(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),N([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")}),ja(e).pipe(w(c=>i.next(c)),_(()=>i.complete()),m(c=>$({ref:e},c)))})}function mt(e,{viewport$:t},r=document.body){return Fa(e,{content$:new j(o=>{let n=e.title,i=wn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Ua(e,t){let r=C(()=>N([tn(e),ze(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 kn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(Z(),ie(!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(W(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),O(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(Me(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(pt(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")}}),h(n,"click").pipe(W(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),h(n,"mousedown").pipe(W(a),re(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=Ie())==null||c.blur()}}),r.pipe(W(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Ua(e,t).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function Wa(e){return e.tagName==="CODE"?P(".c, .c1, .cm",e):[e]}function Va(e){let t=[];for(let r of Wa(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 Hn(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 Va(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,Tn(p,i)),s.replaceWith(a.get(p)))}return a.size===0?S:C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=[];for(let[l,f]of a)c.push([R(".md-typeset",f),R(`:scope > li:nth-child(${l})`,e)]);return o.pipe(W(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?Hn(f,u):Hn(u,f)}),O(...[...a].map(([,l])=>kn(l,t,{target$:r}))).pipe(_(()=>s.complete()),pe())})}function $n(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return $n(t)}}function Pn(e,t){return C(()=>{let r=$n(e);return typeof r!="undefined"?fr(r,e,t):S})}var Rn=Lt(Br());var Da=0;function In(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return In(t)}}function za(e){return ge(e).pipe(m(({width:t})=>({scrollable:Tt(e).width>t})),te("scrollable"))}function jn(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(jr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Rn.default.isSupported()&&(e.closest(".copy")||B("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Da++}`;let l=Sn(c.id);c.insertBefore(l,e),B("content.tooltips")&&a.push(mt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=In(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||B("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(W(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:S)))}}return P(":scope > span[id]",e).length&&e.classList.add("md-code__content"),za(e).pipe(w(c=>n.next(c)),_(()=>n.complete()),m(c=>$({ref:e},c)),Re(...a))});return B("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function Na(e,{target$:t,print$:r}){let o=!0;return O(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),w(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Fn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),Na(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}var Un=".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}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.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 p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{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}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}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 marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}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)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-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 Gr,Qa=0;function Ka(){return typeof mermaid=="undefined"||mermaid instanceof Element?wt("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):I(void 0)}function Wn(e){return e.classList.remove("mermaid"),Gr||(Gr=Ka().pipe(w(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Un,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),G(1))),Gr.subscribe(()=>co(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Qa++}`,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)})),Gr.pipe(m(()=>({ref:e})))}var Vn=x("table");function Dn(e){return e.replaceWith(Vn),Vn.replaceWith(An(e)),I({ref:e})}function Ya(e){let t=e.find(r=>r.checked)||e[0];return O(...e.map(r=>h(r,"change").pipe(m(()=>R(`label[for="${r.id}"]`))))).pipe(Q(R(`label[for="${t.id}"]`)),m(r=>({active:r})))}function zn(e,{viewport$:t,target$:r}){let o=R(".tabbed-labels",e),n=P(":scope > input",e),i=Kr("prev");e.append(i);let a=Kr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(Z(),ie(!0));N([s,ge(e),tt(e)]).pipe(W(p),Me(1,me)).subscribe({next([{active:c},l]){let f=De(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let d=pr(o);(f.xd.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")}}),N([ze(o),ge(o)]).pipe(W(p)).subscribe(([c,l])=>{let f=Tt(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),O(h(i,"click").pipe(m(()=>-1)),h(a,"click").pipe(m(()=>1))).pipe(W(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(W(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=R(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),h(l.firstElementChild,"click").pipe(W(p),b(f=>!(f.metaKey||f.ctrlKey)),w(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return B("content.tabs.link")&&s.pipe(Ce(1),re(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 y of P("[data-tabs]"))for(let L of P(":scope > input",y)){let X=R(`label[for="${L.id}"]`);if(X!==c&&X.innerText.trim()===f){X.setAttribute("data-md-switching",""),L.click();break}}window.scrollTo({top:e.offsetTop-u});let d=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...d])])}}),s.pipe(W(p)).subscribe(()=>{for(let c of P("audio, video",e))c.pause()}),Ya(n).pipe(w(c=>s.next(c)),_(()=>s.complete()),m(c=>$({ref:e},c)))}).pipe(Ke(se))}function Nn(e,{viewport$:t,target$:r,print$:o}){return O(...P(".annotate:not(.highlight)",e).map(n=>Pn(n,{target$:r,print$:o})),...P("pre:not(.mermaid) > code",e).map(n=>jn(n,{target$:r,print$:o})),...P("pre.mermaid",e).map(n=>Wn(n)),...P("table:not([class])",e).map(n=>Dn(n)),...P("details",e).map(n=>Fn(n,{target$:r,print$:o})),...P("[data-tabs]",e).map(n=>zn(n,{viewport$:t,target$:r})),...P("[title]",e).filter(()=>B("content.tooltips")).map(n=>mt(n,{viewport$:t})))}function Ba(e,{alert$:t}){return t.pipe(v(r=>O(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function qn(e,t){let r=R(".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}),Ba(e,t).pipe(w(n=>o.next(n)),_(()=>o.complete()),m(n=>$({ref:e},n)))})}var Ga=0;function Ja(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"?ze(o):I({x:0,y:0}),i=O(et(t),Ht(t)).pipe(K());return N([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=De(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 Qn(e){let t=e.title;if(!t.length)return S;let r=`__tooltip_${Ga++}`,o=Pt(r,"inline"),n=R(".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")}}),O(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(Me(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(pt(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")}}),Ja(o,e).pipe(w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))}).pipe(Ke(se))}function Xa({viewport$:e}){if(!B("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Be(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=Ne("search");return N([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Kn(e,t){return C(()=>N([ge(e),Xa(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),G(1))}function Yn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(Z(),ie(!0));o.pipe(te("active"),He(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue(P("[title]",e)).pipe(b(()=>B("content.tooltips")),ne(a=>Qn(a)));return r.subscribe(o),t.pipe(W(n),m(a=>$({ref:e},a)),Re(i.pipe(W(n))))})}function Za(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}}),te("active"))}function Bn(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"?S:Za(o,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))})}function Gn(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})),te("bottom"))));return N([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 es(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(ne(o=>h(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 Jn(e){let t=P("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"),re(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(ve(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),es(t).pipe(W(n.pipe(Ce(1))),ct(),w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))})}function Xn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(w(o=>r.next({value:o})),_(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Jr=Lt(Br());function ts(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 Zn({alert$:e}){Jr.default.isSupported()&&new j(t=>{new Jr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||ts(R(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(w(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function ei(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function rs(e,t){let r=new Map;for(let o of P("url",e)){let n=R("loc",o),i=[ei(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(ei(new URL(s),t))}}return r}function ur(e){return un(new URL("sitemap.xml",e)).pipe(m(t=>rs(t,new URL(e))),de(()=>I(new Map)))}function os(e,t){if(!(e.target instanceof Element))return S;let r=e.target.closest("a");if(r===null)return S;if(r.target||e.metaKey||e.ctrlKey)return S;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):S}function ti(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function ri(e){for(let t of P("[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 ns(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=ti(document);for(let[o,n]of ti(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 We(P("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 j(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),S}),Z(),ie(document))}function oi({location$:e,viewport$:t,progress$:r}){let o=xe();if(location.protocol==="file:")return S;let n=ur(o.base);I(document).subscribe(ri);let i=h(document.body,"click").pipe(He(n),v(([p,c])=>os(p,c)),pe()),a=h(window,"popstate").pipe(m(ye),pe());i.pipe(re(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),O(i,a).subscribe(e);let s=e.pipe(te("pathname"),v(p=>fn(p,{progress$:r}).pipe(de(()=>(lt(p,!0),S)))),v(ri),v(ns),pe());return O(s.pipe(re(e,(p,c)=>c)),s.pipe(v(()=>e),te("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),w(()=>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",pn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(te("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ni=Lt(qr());function ii(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,ni.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 ai(e,t){let r=yn(e);return O(I(location.protocol!=="file:"),Ne("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 si(e){var l;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:o,currentBaseURL:n}=e,i=(l=Xr(n))==null?void 0:l.pathname;if(i===void 0)return;let a=ss(o.pathname,i);if(a===void 0)return;let s=ps(t.keys());if(!t.has(s))return;let p=Xr(a,s);if(!p||!t.has(p.href))return;let c=Xr(a,r);if(c)return c.hash=o.hash,c.search=o.search,c}function Xr(e,t){try{return new URL(e,t)}catch(r){return}}function ss(e,t){if(e.startsWith(t))return e.slice(t.length)}function cs(e,t){let r=Math.min(e.length,t.length),o;for(o=0;oS)),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=>h(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),re(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?S:(i.preventDefault(),I(new URL(p)))}}return S}),v(i=>ur(i).pipe(m(a=>{var s;return(s=si({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(n=>lt(n,!0)),N([r,o]).subscribe(([n,i])=>{R(".md-header__topic").appendChild(Cn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let p=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(p)||(p=[p]);e:for(let c of p)for(let l of n.aliases.concat(n.version))if(new RegExp(c,"i").test(l)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let p of ae("outdated"))p.hidden=!1})}function ls(e,{worker$:t}){let{searchParams:r}=ye();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),Ne("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=ye();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=O(t.pipe(Ae(It)),h(e,"keyup"),o).pipe(m(()=>e.value),K());return N([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),G(1))}function pi(e,{worker$:t}){let r=new g,o=r.pipe(Z(),ie(!0));N([t.pipe(Ae(It)),r],(i,a)=>a).pipe(te("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(te("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),h(e.form,"reset").pipe(W(o)).subscribe(()=>e.focus());let n=R("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ls(e,{worker$:t}).pipe(w(i=>r.next(i)),_(()=>r.complete()),m(i=>$({ref:e},i)),G(1))}function li(e,{worker$:t,query$:r}){let o=new g,n=on(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=R(":scope > :first-child",e),s=R(":scope > :last-child",e);Ne("search").subscribe(l=>{s.setAttribute("role",l?"list":"presentation"),s.hidden=!l}),o.pipe(re(r),Wr(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(w(()=>s.innerHTML=""),v(({items:l})=>O(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Be(4),Dr(n),v(([f])=>f)))),m(Mn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(ne(l=>{let f=fe("details",l);return typeof f=="undefined"?S:h(f,"toggle").pipe(W(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(w(l=>o.next(l)),_(()=>o.complete()),m(l=>$({ref:e},l)))}function ms(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=ye();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function mi(e,t){let r=new g,o=r.pipe(Z(),ie(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(W(o)).subscribe(n=>n.preventDefault()),ms(e,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))}function fi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=O(h(n,"keydown"),h(n,"focus")).pipe(ve(se),m(()=>n.value),K());return o.pipe(He(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(w(s=>o.next(s)),_(()=>o.complete()),m(()=>({ref:e})))}function ui(e,{index$:t,keyboard$:r}){let o=xe();try{let n=ai(o.search,t),i=Se("search-query",e),a=Se("search-result",e);h(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=Ie();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of P(":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],[,d])=>d-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,...P(":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!==Ie()&&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=pi(i,{worker$:n});return O(s,li(a,{worker$:n,query$:s})).pipe(Re(...ae("search-share",e).map(p=>mi(p,{query$:s})),...ae("search-suggest",e).map(p=>fi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ye}}function di(e,{index$:t,location$:r}){return N([t,r.pipe(Q(ye()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>ii(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 fs(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return N([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 Zr(e,o){var n=o,{header$:t}=n,r=so(n,["header$"]);let i=R(".md-sidebar__scrollwrap",e),{y:a}=De(i);return C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=s.pipe(Me(0,me));return c.pipe(re(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 P(".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:d}=ce(f);f.scrollTo({top:u-d/2})}}}),ue(P("label[tabindex]",e)).pipe(ne(l=>h(l,"click").pipe(ve(se),m(()=>l),W(p)))).subscribe(l=>{let f=R(`[id="${l.htmlFor}"]`);R(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),fs(e,r).pipe(w(l=>s.next(l)),_(()=>s.complete()),m(l=>$({ref:e},l)))})}function hi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return st(je(`${r}/releases/latest`).pipe(de(()=>S),m(o=>({version:o.tag_name})),Ve({})),je(r).pipe(de(()=>S),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Ve({}))).pipe(m(([o,n])=>$($({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return je(r).pipe(m(o=>({repositories:o.public_repos})),Ve({}))}}function bi(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return st(je(`${r}/releases/permalink/latest`).pipe(de(()=>S),m(({tag_name:o})=>({version:o})),Ve({})),je(r).pipe(de(()=>S),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Ve({}))).pipe(m(([o,n])=>$($({},o),n)))}function vi(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return hi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return bi(r,o)}return S}var us;function ds(e){return us||(us=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 S}return vi(e.href).pipe(w(o=>__md_set("__source",o,sessionStorage)))}).pipe(de(()=>S),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),G(1)))}function gi(e){let t=R(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(_n(o)),t.classList.add("md-source__repository--active")}),ds(e).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function hs(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),te("hidden"))}function yi(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}):hs(e,t)).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function bs(e,{viewport$:t,header$:r}){let o=new Map,n=P(".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(te("height"),m(({height:s})=>{let p=Se("main"),c=R(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(te("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 d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),He(i),v(([p,c])=>t.pipe(Fr(([l,f],{offset:{y:u},size:d})=>{let y=u+d.height>=Math.floor(s.height);for(;f.length;){let[,L]=f[0];if(L-c=u&&!y)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:[]}),Be(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(Z(),ie(!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=O(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),He(o.pipe(ve(se))),re(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:d}=ce(f);f.scrollTo({top:u-d/2,behavior:c})}}})}return B("navigation.tracking")&&t.pipe(W(a),te("offset"),_e(250),Ce(1),W(n.pipe(Ce(1))),ct({delay:250}),re(i)).subscribe(([,{prev:s}])=>{let p=ye(),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}`)}),bs(e,{viewport$:t,header$:r}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function vs(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Be(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return N([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),W(o.pipe(Ce(1))),ie(!0),ct({delay:250}),m(a=>({hidden:a})))}function Ei(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(Z(),ie(!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(W(a),te("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),h(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),vs(e,{viewport$:t,main$:o,target$:n}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))}function wi({document$:e,viewport$:t}){e.pipe(v(()=>P(".md-ellipsis")),ne(r=>tt(r).pipe(W(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")?mt(n,{viewport$:t}).pipe(W(e.pipe(Ce(1))),_(()=>n.removeAttribute("title"))):S})).subscribe(),B("content.tooltips")&&e.pipe(v(()=>P(".md-status")),ne(r=>mt(r,{viewport$:t}))).subscribe()}function Ti({document$:e,tablet$:t}){e.pipe(v(()=>P(".md-toggle--indeterminate")),w(r=>{r.indeterminate=!0,r.checked=!1}),ne(r=>h(r,"change").pipe(Vr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),re(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function gs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Si({document$:e}){e.pipe(v(()=>P("[data-md-scrollfix]")),w(t=>t.removeAttribute("data-md-scrollfix")),b(gs),ne(t=>h(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 Oi({viewport$:e,tablet$:t}){N([Ne("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),re(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 ys(){return location.protocol==="file:"?wt(`${new URL("search/search_index.js",eo.base)}`).pipe(m(()=>__index),G(1)):je(new URL("search/search_index.json",eo.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Go(),Ft=sn(),Ot=ln(Ft),to=an(),Oe=gn(),hr=$t("(min-width: 60em)"),Mi=$t("(min-width: 76.25em)"),_i=mn(),eo=xe(),Ai=document.forms.namedItem("search")?ys():Ye,ro=new g;Zn({alert$:ro});var oo=new g;B("navigation.instant")&&oi({location$:Ft,viewport$:Oe,progress$:oo}).subscribe(ot);var Li;((Li=eo.version)==null?void 0:Li.provider)==="mike"&&ci({document$:ot});O(Ft,Ot).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});to.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&<(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&<(r);break;case"Enter":let o=Ie();o instanceof HTMLLabelElement&&o.click()}});wi({viewport$:Oe,document$:ot});Ti({document$:ot,tablet$:hr});Si({document$:ot});Oi({viewport$:Oe,tablet$:hr});var rt=Kn(Se("header"),{viewport$:Oe}),jt=ot.pipe(m(()=>Se("main")),v(e=>Gn(e,{viewport$:Oe,header$:rt})),G(1)),xs=O(...ae("consent").map(e=>En(e,{target$:Ot})),...ae("dialog").map(e=>qn(e,{alert$:ro})),...ae("palette").map(e=>Jn(e)),...ae("progress").map(e=>Xn(e,{progress$:oo})),...ae("search").map(e=>ui(e,{index$:Ai,keyboard$:to})),...ae("source").map(e=>gi(e))),Es=C(()=>O(...ae("announce").map(e=>xn(e)),...ae("content").map(e=>Nn(e,{viewport$:Oe,target$:Ot,print$:_i})),...ae("content").map(e=>B("search.highlight")?di(e,{index$:Ai,location$:Ft}):S),...ae("header").map(e=>Yn(e,{viewport$:Oe,header$:rt,main$:jt})),...ae("header-title").map(e=>Bn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?zr(Mi,()=>Zr(e,{viewport$:Oe,header$:rt,main$:jt})):zr(hr,()=>Zr(e,{viewport$:Oe,header$:rt,main$:jt}))),...ae("tabs").map(e=>yi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>xi(e,{viewport$:Oe,header$:rt,main$:jt,target$:Ot})),...ae("top").map(e=>Ei(e,{viewport$:Oe,header$:rt,main$:jt,target$:Ot})))),Ci=ot.pipe(v(()=>Es),Re(xs),G(1));Ci.subscribe();window.document$=ot;window.location$=Ft;window.target$=Ot;window.keyboard$=to;window.viewport$=Oe;window.tablet$=hr;window.screen$=Mi;window.print$=_i;window.alert$=ro;window.progress$=oo;window.component$=Ci;})(); +//# sourceMappingURL=bundle.50899def.min.js.map + diff --git a/mkdocs-site/assets/javascripts/bundle.50899def.min.js.map b/mkdocs-site/assets/javascripts/bundle.50899def.min.js.map new file mode 100644 index 0000000..6130f72 --- /dev/null +++ b/mkdocs-site/assets/javascripts/bundle.50899def.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/escape-html/index.js", "node_modules/clipboard/dist/clipboard.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/tslib/tslib.es6.mjs", "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/findurl/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 * 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 * 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 * Copyright (c) 2016-2025 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: 60em)\")\nconst screen$ = watchMedia(\"(min-width: 76.25em)\")\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 /* 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 */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\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", "/******************************************************************************\nCopyright (c) Microsoft Corporation.\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n***************************************************************************** */\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nexport function __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\nexport var __assign = function() {\n __assign = Object.assign || function __assign(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n }\n return __assign.apply(this, arguments);\n}\n\nexport function __rest(s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\n t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\n t[p[i]] = s[p[i]];\n }\n return t;\n}\n\nexport function __decorate(decorators, target, key, desc) {\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\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;\n return c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\nexport function __param(paramIndex, decorator) {\n return function (target, key) { decorator(target, key, paramIndex); }\n}\n\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\n var _, done = false;\n for (var i = decorators.length - 1; i >= 0; i--) {\n var context = {};\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\n if (kind === \"accessor\") {\n if (result === void 0) continue;\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\n if (_ = accept(result.get)) descriptor.get = _;\n if (_ = accept(result.set)) descriptor.set = _;\n if (_ = accept(result.init)) initializers.unshift(_);\n }\n else if (_ = accept(result)) {\n if (kind === \"field\") initializers.unshift(_);\n else descriptor[key] = _;\n }\n }\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\n done = true;\n};\n\nexport function __runInitializers(thisArg, initializers, value) {\n var useValue = arguments.length > 2;\n for (var i = 0; i < initializers.length; i++) {\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\n }\n return useValue ? value : void 0;\n};\n\nexport function __propKey(x) {\n return typeof x === \"symbol\" ? x : \"\".concat(x);\n};\n\nexport function __setFunctionName(f, name, prefix) {\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\n};\n\nexport function __metadata(metadataKey, metadataValue) {\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\n}\n\nexport function __awaiter(thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n}\n\nexport function __generator(thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\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;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n}\n\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n});\n\nexport function __exportStar(m, o) {\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\n}\n\nexport function __values(o) {\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n if (m) return m.call(o);\n if (o && typeof o.length === \"number\") return {\n next: function () {\n if (o && i >= o.length) o = void 0;\n return { value: o && o[i++], done: !o };\n }\n };\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n}\n\nexport function __read(o, n) {\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\n if (!m) return o;\n var i = m.call(o), r, ar = [], e;\n try {\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\n }\n catch (error) { e = { error: error }; }\n finally {\n try {\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\n }\n finally { if (e) throw e.error; }\n }\n return ar;\n}\n\n/** @deprecated */\nexport function __spread() {\n for (var ar = [], i = 0; i < arguments.length; i++)\n ar = ar.concat(__read(arguments[i]));\n return ar;\n}\n\n/** @deprecated */\nexport function __spreadArrays() {\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\n r[k] = a[j];\n return r;\n}\n\nexport function __spreadArray(to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n}\n\nexport function __await(v) {\n return this instanceof __await ? (this.v = v, this) : new __await(v);\n}\n\nexport function __asyncGenerator(thisArg, _arguments, generator) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\n function fulfill(value) { resume(\"next\", value); }\n function reject(value) { resume(\"throw\", value); }\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\n}\n\nexport function __asyncDelegator(o) {\n var i, p;\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\n}\n\nexport function __asyncValues(o) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var m = o[Symbol.asyncIterator], i;\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);\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); }); }; }\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\n}\n\nexport function __makeTemplateObject(cooked, raw) {\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\n return cooked;\n};\n\nvar __setModuleDefault = Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n};\n\nexport function __importStar(mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n __setModuleDefault(result, mod);\n return result;\n}\n\nexport function __importDefault(mod) {\n return (mod && mod.__esModule) ? mod : { default: mod };\n}\n\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\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\");\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n}\n\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\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\");\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n}\n\nexport function __classPrivateFieldIn(state, receiver) {\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\n}\n\nexport function __addDisposableResource(env, value, async) {\n if (value !== null && value !== void 0) {\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\n var dispose, inner;\n if (async) {\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\n dispose = value[Symbol.asyncDispose];\n }\n if (dispose === void 0) {\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\n dispose = value[Symbol.dispose];\n if (async) inner = dispose;\n }\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\n env.stack.push({ value: value, dispose: dispose, async: async });\n }\n else if (async) {\n env.stack.push({ async: true });\n }\n return value;\n}\n\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\n var e = new Error(message);\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\n};\n\nexport function __disposeResources(env) {\n function fail(e) {\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\n env.hasError = true;\n }\n var r, s = 0;\n function next() {\n while (r = env.stack.pop()) {\n try {\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\n if (r.dispose) {\n var result = r.dispose.call(r.value);\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\n }\n else s |= 1;\n }\n catch (e) {\n fail(e);\n }\n }\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\n if (env.hasError) throw env.error;\n }\n return next();\n}\n\nexport default {\n __extends,\n __assign,\n __rest,\n __decorate,\n __param,\n __metadata,\n __awaiter,\n __generator,\n __createBinding,\n __exportStar,\n __values,\n __read,\n __spread,\n __spreadArrays,\n __spreadArray,\n __await,\n __asyncGenerator,\n __asyncDelegator,\n __asyncValues,\n __makeTemplateObject,\n __importStar,\n __importDefault,\n __classPrivateFieldGet,\n __classPrivateFieldSet,\n __classPrivateFieldIn,\n __addDisposableResource,\n __disposeResources,\n};\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 */\nexport class Subscription implements SubscriptionLike {\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 */\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 */\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 * @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 value The `next` value.\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 err The `error` exception.\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 */\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 */\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 * @param 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 * @param subscribe the subscriber function to be passed to the Observable constructor\n * @return A new observable.\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 * @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 observerOrNext Either an {@link Observer} with some or all callback methods,\n * or the `next` handler that is called for each value emitted from the subscribed Observable.\n * @param error 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 complete A handler for a terminal event resulting from successful completion.\n * @return A subscription reference to the registered handlers.\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 * @return 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 *\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 * @return The Observable result of all the operators having been called\n * in the order they were passed in.\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 * @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 * @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 that this Subject casts to.\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\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 */\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 */\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 state Some contextual data that the `work` function uses when called by the\n * Scheduler.\n * @param delay Time to wait before executing the work, where the time unit is implicit\n * and defined by the Scheduler.\n * @return A subscription in order to be able to unsubscribe the scheduled work.\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 */\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 * @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 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 work A function representing a task, or some unit of work to be\n * executed by the Scheduler.\n * @param delay Time to wait before executing the work, where the time unit is\n * implicit and defined by the Scheduler itself.\n * @param state Some contextual data that the `work` function uses when called\n * by the Scheduler.\n * @return A subscription in order to be able to unsubscribe 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 * @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 * @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 && id === scheduler._scheduled && 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 let flushId;\n if (action) {\n flushId = action.id;\n } else {\n flushId = this._scheduled;\n this._scheduled = undefined;\n }\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