Repository: redhat-scholars/kubernetes-tutorial Branch: master Commit: 8e93b43aec6a Files: 205 Total size: 350.0 KB Directory structure: gitextract_62qzlr9h/ ├── .devcontainer/ │ ├── Dockerfile │ ├── assets/ │ │ ├── .zshrc.example │ │ ├── copy-kube-config.sh │ │ ├── fedora.repo │ │ └── post-start.sh │ ├── devcontainer.json │ └── workspace-setup/ │ ├── asciidoc.json.code-snippets │ └── launch.json ├── .editorconfig ├── .github/ │ └── workflows/ │ ├── docs.yml │ ├── helloworld-go.yml │ ├── helloworld-quarkus.yml │ └── helloworld-spring-boot.yml ├── .gitignore ├── LICENSE ├── README.adoc ├── apps/ │ ├── config/ │ │ ├── other.properties │ │ └── some.properties │ ├── helloworld/ │ │ ├── go/ │ │ │ ├── Dockerfile │ │ │ ├── myrest │ │ │ ├── myrest.go │ │ │ └── readme.txt │ │ ├── nodejs/ │ │ │ ├── .devcontainer/ │ │ │ │ ├── Dockerfile │ │ │ │ └── devcontainer.json │ │ │ ├── Dockerfile │ │ │ ├── hello-http.js │ │ │ └── readme.txt │ │ ├── python/ │ │ │ ├── Dockerfile │ │ │ ├── app.py │ │ │ ├── readme.txt │ │ │ └── requirements.txt │ │ ├── quarkus/ │ │ │ ├── .dockerignore │ │ │ ├── buildNativeLinux.sh │ │ │ ├── buildNativeMac.sh │ │ │ ├── build_push_docker.sh │ │ │ ├── build_push_quay.sh │ │ │ ├── dockerbuild.sh │ │ │ ├── dockerbuild_openshift.sh │ │ │ ├── dockerpush_docker.sh │ │ │ ├── dockerpush_quay.sh │ │ │ ├── kubefiles/ │ │ │ │ ├── Deployment.yml │ │ │ │ ├── Deployment_quay.yml │ │ │ │ ├── Dockerfile │ │ │ │ ├── Dockerfile.openshift │ │ │ │ └── Service.yml │ │ │ ├── poller.sh │ │ │ ├── pom.xml │ │ │ ├── readme.txt │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── redhat/ │ │ │ └── developer/ │ │ │ └── demo/ │ │ │ └── GreetingEndpoint.java │ │ └── springboot/ │ │ ├── .devcontainer/ │ │ │ ├── Dockerfile │ │ │ └── devcontainer.json │ │ ├── Dockerfile │ │ ├── Dockerfile.openshift │ │ ├── Dockerfile_Java11 │ │ ├── Dockerfile_Memory │ │ ├── Dockerfile_Memory2 │ │ ├── build_push_docker.sh │ │ ├── build_push_quay.sh │ │ ├── pom.xml │ │ ├── readme.txt │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── burrsutter/ │ │ ├── HellobootApplication.java │ │ └── MyRESTController.java │ ├── kubefiles/ │ │ ├── demo-dynamic-persistent.yaml │ │ ├── demo-ingress-2.yaml │ │ ├── demo-ingress.yaml │ │ ├── demo-persistent-volume-hostpath.yaml │ │ ├── demo-persistent-volume-local.yaml │ │ ├── myboot-deployment-bad-image.yml │ │ ├── myboot-deployment-configuration-secret.yml │ │ ├── myboot-deployment-configuration.yml │ │ ├── myboot-deployment-live-ready-aggressive.yml │ │ ├── myboot-deployment-live-ready.yml │ │ ├── myboot-deployment-resources-limits-v2.yml │ │ ├── myboot-deployment-resources-limits.yml │ │ ├── myboot-deployment-resources.yml │ │ ├── myboot-deployment-startup-live-ready.yml │ │ ├── myboot-deployment.yml │ │ ├── myboot-node-affinity.yml │ │ ├── myboot-persistent-volume-claim.yaml │ │ ├── myboot-pod-affinity.yml │ │ ├── myboot-pod-antiaffinity.yaml │ │ ├── myboot-pod-volume-hostpath.yaml │ │ ├── myboot-pod-volume-pvc.yaml │ │ ├── myboot-pod-volume.yml │ │ ├── myboot-pods-volume.yml │ │ ├── myboot-service.yml │ │ ├── myboot-toleration.yaml │ │ ├── mykafka.yml │ │ ├── quarkus-daemonset.yaml │ │ ├── quarkus-statefulset-external-svc.yaml │ │ ├── quarkus-statefulset.yaml │ │ ├── whalesay-cronjob.yaml │ │ └── whalesay-job.yaml │ ├── pizza-operator/ │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ ├── MavenWrapperDownloader.java │ │ │ ├── maven-wrapper.jar │ │ │ └── maven-wrapper.properties │ │ ├── README.md │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── docker/ │ │ │ │ ├── Dockerfile.jvm │ │ │ │ └── Dockerfile.native │ │ │ ├── java/ │ │ │ │ └── org/ │ │ │ │ └── acme/ │ │ │ │ ├── ExampleResource.java │ │ │ │ ├── KubernetesClientProducer.java │ │ │ │ ├── PizzaResource.java │ │ │ │ ├── PizzaResourceDoneable.java │ │ │ │ ├── PizzaResourceList.java │ │ │ │ ├── PizzaResourceSpec.java │ │ │ │ ├── PizzaResourceStatus.java │ │ │ │ └── PizzaResourceWatcher.java │ │ │ └── resources/ │ │ │ ├── META-INF/ │ │ │ │ └── resources/ │ │ │ │ └── index.html │ │ │ └── application.properties │ │ └── test/ │ │ └── java/ │ │ └── org/ │ │ └── acme/ │ │ ├── ExampleResourceTest.java │ │ └── NativeExampleResourceIT.java │ └── pizzas/ │ ├── cheese-pizza.yaml │ ├── meat-pizza.yaml │ ├── pizza-crd.yaml │ ├── pizza-deployment.yaml │ └── veggie-lovers.yaml ├── bin/ │ └── build-site.sh ├── documentation/ │ ├── antora.yml │ └── modules/ │ ├── ROOT/ │ │ ├── examples/ │ │ │ ├── PizzaResourceWatcher.java │ │ │ ├── cheese-pizza.yaml │ │ │ ├── meat-pizza.yaml │ │ │ ├── myboot-deployment-configuration-secret.yml │ │ │ ├── myboot-deployment-live-ready-aggressive.yml │ │ │ ├── myboot-deployment-live-ready.yml │ │ │ ├── myboot-deployment-startup-live-ready.yml │ │ │ ├── myboot-pod-volume-hostpath.yaml │ │ │ ├── myboot-pod-volume.yml │ │ │ ├── myboot-pods-volume.yml │ │ │ ├── pizza-crd.yaml │ │ │ ├── quarkus-statefulset-external-svc.yaml │ │ │ ├── whalesay-cronjob.yaml │ │ │ └── whalesay-job.yaml │ │ ├── nav.adoc │ │ ├── pages/ │ │ │ ├── _attributes.adoc │ │ │ ├── _partials/ │ │ │ │ ├── affinity_label.adoc │ │ │ │ ├── find_node_for_pod.adoc │ │ │ │ ├── invoke-service.adoc │ │ │ │ ├── set-env-vars.adoc │ │ │ │ ├── verify-setup.adoc │ │ │ │ └── watching-logs.adoc │ │ │ ├── blue-green.adoc │ │ │ ├── building-images.adoc │ │ │ ├── configmap.adoc │ │ │ ├── crds.adoc │ │ │ ├── daemonset.adoc │ │ │ ├── exec.adoc │ │ │ ├── index.adoc │ │ │ ├── ingress.adoc │ │ │ ├── installation.adoc │ │ │ ├── jobs-cronjobs.adoc │ │ │ ├── kubectl.adoc │ │ │ ├── live-ready.adoc │ │ │ ├── logs.adoc │ │ │ ├── pod-rs-deployment.adoc │ │ │ ├── resources.adoc │ │ │ ├── rolling-updates.adoc │ │ │ ├── secrets.adoc │ │ │ ├── service-magic.adoc │ │ │ ├── service.adoc │ │ │ ├── statefulset.adoc │ │ │ ├── taints-affinity.adoc │ │ │ └── volumes-persistentvolumes.adoc │ │ └── partials/ │ │ ├── create-greeting-file.adoc │ │ ├── describe-deployment.adoc │ │ ├── describe.adoc │ │ ├── env-curl.adoc │ │ ├── file-watch-command.adoc │ │ ├── loop.adoc │ │ ├── namespace-setup-tip.adoc │ │ ├── open-terminal-in-editor-inset.adoc │ │ ├── optional-requisites.adoc │ │ ├── prerequisites-kubernetes.adoc │ │ ├── set-context.adoc │ │ ├── stern-watch.adoc │ │ ├── taint-remove-taint.adoc │ │ ├── terminal-cleanup.adoc │ │ ├── tip_vscode_kube_editor.adoc │ │ ├── tip_vscode_quick_open.adoc │ │ ├── watch-node-directory.adoc │ │ ├── watching-pods-with-nodes.adoc │ │ ├── watching-pods.adoc │ │ └── watching-services.adoc │ └── _attributes.adoc ├── github-pages-stage.yml ├── github-pages.yml ├── gulpfile.babel.js ├── lib/ │ ├── copy-to-clipboard.js │ ├── remote-include-processor.js │ └── tab-block.js ├── package.json ├── scripts/ │ ├── create-kubeconfig.sh │ ├── github-pages-publish.sh │ ├── minikube-server-setup.sh │ ├── pod-node-columns-template.txt │ └── shell-setup.sh ├── supplemental-ui/ │ └── partials/ │ ├── header-content.hbs │ ├── nav-explore.hbs │ ├── nav-menu.hbs │ └── nav.hbs └── vscode-asciidoc-extra.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/Dockerfile ================================================ # syntax = docker/dockerfile:1.0-experimental # # This is the base dockerfile to be used with the BUILDKIT to build the # image that the .devcontainer docker image is based on # FROM registry.access.redhat.com/ubi8/openjdk-11:latest USER root # add a reference to fedora repo to install packages not part of the # ubi8 repos COPY assets/fedora.repo /etc/yum.repos.d/fedora.repo RUN microdnf install dnf \ # install a smattering of useful packages (some of which are used later in dockerfile such as wget, zsh, and git) && dnf install -y skopeo wget jq iputils vi procps git \ # Install packages from fedora (outside unsubscribed ubi8) && dnf -y install --enablerepo fedora zsh tree \ # Install necessary tools to run antora && dnf -y install npm && npm i -g @antora/cli@2.3 @antora/site-generator-default@2.3 && npm rm --global npx && npm install --global npx && npm install --global gulp \ # Install yum so that docker can be installed in the container && dnf -y install yum && yum install -y yum-utils \ # install docker repo && yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo \ # install docker client && yum install -y docker-ce-cli \ # make sure jboss user has rights to run docker && usermod -aG docker jboss \ # cleanup packages and yum && yum remove -y yum-utils && yum clean all && dnf clean all && rm -r /var/cache/dnf # install specific version of yq (2.4.1) RUN wget https://github.com/mikefarah/yq/releases/download/2.4.1/yq_linux_amd64 -O /usr/bin/yq && \ chmod +x /usr/bin/yq # install stern RUN cd /usr/local/bin && \ wget https://github.com/wercker/stern/releases/download/1.11.0/stern_linux_amd64 -O /usr/local/bin/stern && \ chmod 755 /usr/local/bin/stern && \ # install hey wget https://mwh-demo-assets.s3-ap-southeast-2.amazonaws.com/hey_linux_amd64 -O /usr/local/bin/hey && \ chmod 755 /usr/local/bin/hey # overwrite existing oc with the absolute newest version of the openshift client RUN curl -L https://mirror.openshift.com/pub/openshift-v4/clients/ocp/latest/openshift-client-linux.tar.gz | \ tar -xvzf - -C /usr/local/bin/ oc && chmod 755 /usr/local/bin/oc && ln -s /usr/local/bin/oc /usr/local/bin/kubectl USER jboss # install and configure ohmyzsh for jboss user RUN wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | zsh # needed for krew commands ENV PATH="$HOME/.krew/bin:$PATH" # install kube ctx and kube ns via krew RUN ( 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/')" && \ curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/krew.tar.gz" && \ tar zxvf krew.tar.gz && \ KREW=./krew-"${OS}_${ARCH}" && \ "$KREW" install krew ) &&\ kubectl krew install ctx && kubectl krew install ns # Subdirectory where local-config files should reside (matched to gitignore to ensure no secrets are checked in) ENV CONFIG_SUBDIR "local-config" ENV DEMO_HOME "/workspaces/kubernetes-tutorial/" # Use VSCode with kubectl edit commands ENV KUBE_EDITOR="code -w" # this is done in the base image already (to support the demo shell images too), but for those that make # local changes to .zshrc they should not have to rebuild the base COPY assets/.zshrc.example $HOME/.zshrc ================================================ FILE: .devcontainer/assets/.zshrc.example ================================================ # If you come from bash you might have to change your $PATH. # export PATH=$HOME/bin:/usr/local/bin:$PATH # Path to your oh-my-zsh installation. export ZSH="$HOME/.oh-my-zsh" # Set name of the theme to load --- if set to "random", it will # load a random theme each time oh-my-zsh is loaded, in which case, # to know which specific one was loaded, run: echo $RANDOM_THEME # See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes ZSH_THEME="robbyrussell" # Set list of themes to pick from when loading at random # Setting this variable when ZSH_THEME=random will cause zsh to load # a theme from this variable instead of looking in ~/.oh-my-zsh/themes/ # If set to an empty array, this variable will have no effect. # ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" ) # Uncomment the following line to use case-sensitive completion. # CASE_SENSITIVE="true" # Uncomment the following line to use hyphen-insensitive completion. # Case-sensitive completion must be off. _ and - will be interchangeable. # HYPHEN_INSENSITIVE="true" # Uncomment the following line to disable bi-weekly auto-update checks. # DISABLE_AUTO_UPDATE="true" # Uncomment the following line to automatically update without prompting. # DISABLE_UPDATE_PROMPT="true" # Uncomment the following line to change how often to auto-update (in days). # export UPDATE_ZSH_DAYS=13 # Uncomment the following line if pasting URLs and other text is messed up. # DISABLE_MAGIC_FUNCTIONS=true # Uncomment the following line to disable colors in ls. # DISABLE_LS_COLORS="true" # Uncomment the following line to disable auto-setting terminal title. # DISABLE_AUTO_TITLE="true" # Uncomment the following line to enable command auto-correction. # ENABLE_CORRECTION="true" # Uncomment the following line to display red dots whilst waiting for completion. # COMPLETION_WAITING_DOTS="true" # Uncomment the following line if you want to disable marking untracked files # under VCS as dirty. This makes repository status check for large repositories # much, much faster. # DISABLE_UNTRACKED_FILES_DIRTY="true" # Uncomment the following line if you want to change the command execution time # stamp shown in the history command output. # You can set one of the optional three formats: # "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" # or set a custom format using the strftime function format specifications, # see 'man strftime' for details. # HIST_STAMPS="mm/dd/yyyy" # Would you like to use another custom folder than $ZSH/custom? # ZSH_CUSTOM=/path/to/new-custom-folder # Which plugins would you like to load? # Standard plugins can be found in ~/.oh-my-zsh/plugins/* # Custom plugins may be added to ~/.oh-my-zsh/custom/plugins/ # Example format: plugins=(rails git textmate ruby lighthouse) # Add wisely, as too many plugins slow down shell startup. plugins=(git) source $ZSH/oh-my-zsh.sh # User configuration # export MANPATH="/usr/local/man:$MANPATH" # You may need to manually set your language environment # export LANG=en_US.UTF-8 # Preferred editor for local and remote sessions # if [[ -n $SSH_CONNECTION ]]; then # export EDITOR='vim' # else # export EDITOR='mvim' # fi # Compilation flags # export ARCHFLAGS="-arch x86_64" # Set personal aliases, overriding those provided by oh-my-zsh libs, # plugins, and themes. Aliases can be placed here, though oh-my-zsh # users are encouraged to define aliases within the ZSH_CUSTOM folder. # For a full list of active aliases, run `alias`. # # Example aliases # alias zshconfig="mate ~/.zshrc" # alias ohmyzsh="mate ~/.oh-my-zsh" # source $DEMO_HOME/scripts/shell-setup.sh export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH" ================================================ FILE: .devcontainer/assets/copy-kube-config.sh ================================================ #!/bin/bash -i # set -euo pipefail # Copies localhost's ~/.kube/config file into the container and swap out localhost # for host.docker.internal whenever a new shell starts to keep them in sync. if [ "$SYNC_LOCALHOST_KUBECONFIG" = "true" ] && [ -d "/usr/local/share/kube-localhost" ]; then mkdir -p $HOME/.kube cp -r /usr/local/share/kube-localhost/config $HOME/.kube/config sed -i -e "s/localhost/host.docker.internal/g" $HOME/.kube/config sed -i -e "s/127.0.0.1/host.docker.internal/g" $HOME/.kube/config # If .minikube was mounted, set up client cert/key if [ -d "/usr/local/share/minikube-localhost" ]; then mkdir -p $HOME/.minikube cp -r /usr/local/share/minikube-localhost/ca.crt $HOME/.minikube # Location varies between versions of minikube if [ -f "/usr/local/share/minikube-localhost/client.crt" ]; then cp -r /usr/local/share/minikube-localhost/client.crt $HOME/.minikube cp -r /usr/local/share/minikube-localhost/client.key $HOME/.minikube elif [ -f "/usr/local/share/minikube-localhost/profiles/${SYNC_MINIKUBE_PROFILE}/client.crt" ]; then cp -r /usr/local/share/minikube-localhost/profiles/${SYNC_MINIKUBE_PROFILE}/client.crt $HOME/.minikube cp -r /usr/local/share/minikube-localhost/profiles/${SYNC_MINIKUBE_PROFILE}/client.key $HOME/.minikube fi # Point .kube/config to the correct locaiton of the certs sed -i -r "s|(\s*certificate-authority:\s).*|\\1$HOME\/.minikube\/ca.crt|g" $HOME/.kube/config sed -i -r "s|(\s*client-certificate:\s).*|\\1$HOME\/.minikube\/client.crt|g" $HOME/.kube/config sed -i -r "s|(\s*client-key:\s).*|\\1$HOME\/.minikube\/client.key|g" $HOME/.kube/config fi fi ================================================ FILE: .devcontainer/assets/fedora.repo ================================================ [fedora] name = Fedora baseurl = https://mirror.aarnet.edu.au/pub/fedora/linux/releases/34/Everything/x86_64/os/ gpgcheck=0 enabled=0 ================================================ FILE: .devcontainer/assets/post-start.sh ================================================ #!/bin/bash WORKSPACE_FOLDER=$1 rsync -a ${WORKSPACE_FOLDER}/.devcontainer/workspace-setup/ ${WORKSPACE_FOLDER}/.vscode/ --ignore-existing ${WORKSPACE_FOLDER}/.devcontainer/assets/copy-kube-config.sh ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "DevNation Kubernetes Tutorial", "dockerFile": "Dockerfile", "runArgs": [ "-v", "/var/run/docker.sock.raw:/var/run/docker.sock", "-v", "${env:HOME}/.vs-kubernetes:/home/jboss/.vs-kubernetes", // use local .oh-my-zsh configuration if it exists (overwriting one in container). // comment the following line out if you want to use local installation on container instead "-v", "${env:HOME}/.oh-my-zsh:/home/jboss/.oh-my-zsh", "-v", "${env:HOME}/.helm:/home/jboss/.helm", "-v", "${env:HOME}/.ssh:/home/jboss/.ssh", // mount the maven cache locally "-v", "${env:HOME}/.m2/:/home/jboss/.m2", // mount npm cache locally "-v", "${env:HOME}/.npm:/home/jboss/.npm", // This allows us to reach the minikube instance from within the docker container "--network", "host", // override dockerfile DEMO_HOME to whatever folder vscode considers the root folder in the container "-e", "DEMO_HOME=${containerWorkspaceFolder}", ], "mounts":[ "source=${env:HOME}${env:USERPROFILE}/.kube,target=/usr/local/share/kube-localhost,type=bind", "source=${env:HOME}${env:USERPROFILE}/.minikube,target=/usr/local/share/minikube-localhost,type=bind" ], "remoteEnv": { "SYNC_LOCALHOST_KUBECONFIG": "true", "SYNC_MINIKUBE_PROFILE": "devnation", "HOST_USER": "${env:USER}" }, "extensions": [ "vscjava.vscode-java-pack", "redhat.vscode-xml", "redhat.vscode-quarkus", "ggrebert.quarkus-snippets", "humao.rest-client", "asciidoctor.asciidoctor-vscode", "madhavd1.javadoc-tools" ], "postStartCommand": "${containerWorkspaceFolder}/.devcontainer/assets/post-start.sh ${containerWorkspaceFolder}", "settings":{ "terminal.integrated.shell.linux": "/bin/zsh", "editor.tabCompletion": "on", "java.home": "/usr/lib/jvm/java-11-openjdk", "workbench.colorTheme": "Solarized Light", "http.proxyStrictSSL": false, "workbench.tips.enabled": false, "xml.format.enabled": true, // don't pull in the .m2 cache "files.exclude": { "**/.classpath": true, "**/.project": true, "**/.settings": true, "**/.factorypath": true, "**/.m2": true, }, // Don't import the example-operator project // these exclusions don't work entirely as advertised. // See: https://github.com/redhat-developer/vscode-java/issues/1698 "java.import.exclusions": [ //"**/example-operator", //"example-operator/**", "**/.m2/**", "**/node_modules/**", "**/.metadata/**", "**/archetype-resources/**", "**/META-INF/maven/**" ] } } ================================================ FILE: .devcontainer/workspace-setup/asciidoc.json.code-snippets ================================================ { "Add Tabs": { "prefix": "tabs", "body": [ "[tabs]", "====", "${1:tab1}::", "+", "--", "--", "${2:tab2}::", "+", "--", "--", "====" ], "description": "Add Tabs macro" }, "Add Navigation": { "prefix": "nav", "body": [ "${1|*,**,***|} xref:${2:page.adoc}[${3:Nav Title}]" ], "description": "Add new navigation" }, "Console Input": { "prefix": "input", "body": [ "[.console-input]", "[source,${1:bash},subs=\"${2:+macros,+attributes}\"]", "----", "${3:echo \"Hello World\"}", "----" ], "description": "Adds Console Input source fragment" }, "Console Output": { "prefix": "output", "body": [ "[.console-output]", "[source,${1:bash},subs=\"${2:+macros,+attributes}\"]", "----", "${3:\"Hello World\"}", "----" ], "description": "Adds Console Output source fragment" }, "Asciidoc Tag": { "prefix": "atag", "body": [ "// tag::${1:tag_name}[]", "${2:body}", "// end::${1:tag_name}[]" ] }, "Partial Tag Include": { "prefix": "tinclude", "body": [ "include::partial$${1:include_name}.adoc[tags=**;!*;${2:tags_to_include}]" ], "description": "Include a partial with tags" }, "Add Console Tab": { "prefix": "tconsole", "body": [ "[tabs]", "====", "${1:tab1}::", "+", "--", "[.console-${2:input}]", "[source,${3:bash},subs=\"${4:+macros,+attributes}\"]", "----", "${5:echo \"Hello World\"}", "----", "--", "====" ], "description": "Add Tabs macro" }, } ================================================ FILE: .devcontainer/workspace-setup/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "java", "name": "Debug (Attach)", "request": "attach", "hostName": "localhost", "port": 5005, } ] } ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space charset = utf-8 trim_trailing_whitespace = false insert_final_newline = false [*.java] indent_style = space indent_size = 4 [*.xml] indent_style = space indent_size = 2 ================================================ FILE: .github/workflows/docs.yml ================================================ name: docs on: push: branches: - v1.29 - v1.34 paths: - .github/workflows/docs.yml - github-pages.yml - 'documentation/**' jobs: build-and-publish: runs-on: ubuntu-22.04 steps: - name: Checkout project uses: actions/checkout@v4 with: fetch-depth: 0 - name: Run antora uses: docker://antora/antora:2.3.1 with: args: github-pages.yml - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@releases/v4 with: token: "${{github.token}}" FOLDER: gh-pages BRANCH: gh-pages commit-message: "[docs] Publishing the docs for commit(s) ${{github.sha}}" ================================================ FILE: .github/workflows/helloworld-go.yml ================================================ name: helloworld-go on: push: branches: - master paths: - '.github/workflows/helloworld-go.yml' - 'apps/helloworld/go/**' jobs: build: runs-on: ubuntu-18.04 steps: - name: Checkout project uses: actions/checkout@v2 - name: Setup Go uses: actions/setup-go@v2.0.3 with: go-version: '1.14.2' - name: Build Go app working-directory: apps/helloworld/go run: go build myrest.go ================================================ FILE: .github/workflows/helloworld-quarkus.yml ================================================ name: helloworld-quarkus on: push: branches: - master paths: - '.github/workflows/helloworld-quarkus.yml' - 'apps/helloworld/quarkus/**' jobs: build: runs-on: ubuntu-18.04 steps: - name: Checkout project uses: actions/checkout@v2 - name: Setup Java JDK uses: actions/setup-java@v2 with: distribution: "temurin" java-version: 11 - name: Maven build working-directory: apps/helloworld/quarkus run: mvn package ================================================ FILE: .github/workflows/helloworld-spring-boot.yml ================================================ name: helloworld-spring-boot on: push: branches: - master paths: - '.github/workflows/helloworld-spring-boot.yml' - 'apps/helloworld/springboot/**' jobs: build: runs-on: ubuntu-18.04 steps: - name: Checkout project uses: actions/checkout@v2 - name: Setup Java JDK uses: actions/setup-java@v2 with: distribution: "temurin" java-version: 11 - name: Maven build working-directory: apps/helloworld/springboot run: mvn package ================================================ FILE: .gitignore ================================================ .DS_Store target *.iml .idea *.class *.log .cache /gh-pages /.cache *.swp node_modules .classpath .project .settings .kube .minikube .DS_Store .vscode firebase* node_modules .firebaserc .firebase .vscode/ # local kubernetes cluster info local-config/ # once gulp is run, this file is generated. Some debate whether this should be checked in or not package-lock.json yarn.lock ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2020 Red Hat Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.adoc ================================================ # Kubernetes Tutorial image:https://github.com/redhat-developer-demos/kubernetes-tutorial/workflows/docs/badge.svg[] image:https://github.com/redhat-developer-demos/kubernetes-tutorial/workflows/helloworld-go/badge.svg[] image:https://github.com/redhat-developer-demos/kubernetes-tutorial/workflows/helloworld-spring-boot/badge.svg[] image:https://github.com/redhat-developer-demos/kubernetes-tutorial/workflows/helloworld-quarkus/badge.svg[] You can access the HTML version of this tutorial here: https://redhat-scholars.github.io/kubernetes-tutorial/ ## Visual Studio Code Remote Development If you are using link:https://code.visualstudio.com/[Visual Studio Code] with the link:https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers[Remote Containers Extension], you don't need to install anything locally to be able to contribute or run through this tutorial. Simply follow these instructions: 1. (Only if running with podman) Set the environment variable `DEVCONTAINER_TARGET_PREFIX=podman` 2. Open VS Code from the root of the `kubernetes-tutorial` repository and when prompted indicate that you want to "open the folder in a container". Once the devcontainer is initialized, from the Visual Studio Code terminal you will be able to run all the commands outlined for creating documentation. ### Execute Tutorial with VSCode Remote You can also run through the tutorial with VSCode Remote. The only trick is that you will need to be able to access minikube from within your docker container. ## Building the HTML locally In the root of your git repository, run: ``` bin/build-site.sh ``` And then open your `gh-pages/index.html` file: ``` open gh-pages/index.html ``` ## Iterative local development You can develop the tutorial docs locally using a rapid iterative cycle. First, install the `yarn` dependencies: [source,bash] ---- yarn install ---- And now start `gulp`. It will create the website and open your browser connected with `browser-sync`. Everytime it detects a change, it will automatically refresh your browser page. [source,bash] ---- gulp ---- You can clean the local cache using: [source,bash] ---- gulp clean ---- ================================================ FILE: apps/config/other.properties ================================================ DBCONN=jdbc:sqlserver://123.123.123.123:1443;user=MyUserName;password=*****; MSGBROKER=tcp://localhost:61616?jms.useAsyncSend=true ================================================ FILE: apps/config/some.properties ================================================ GREETING=jambo LOVE=Amour ================================================ FILE: apps/helloworld/go/Dockerfile ================================================ FROM registry.access.redhat.com/ubi8/ubi-minimal EXPOSE 8000 COPY myrest /usr/bin CMD /bin/sh -c '/usr/bin/myrest' ================================================ FILE: apps/helloworld/go/myrest.go ================================================ package main import ( "fmt" "net/http" "os" // "time" ) func main() { //api := mux.NewRouter() http.HandleFunc("/", HelloHandler) //http.Handle("/hello", api) fmt.Println("Listening on localhost:8000") http.ListenAndServe(":8000", nil) } func HelloHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) hostname, err := os.Hostname() if err != nil { fmt.Println("unable to get hostname") } // fmt.Fprintf(w, "Hello from Go! %s on %s\n", time.Now(), hostname) fmt.Fprintf(w, "Go Hello on %s\n", hostname) } ================================================ FILE: apps/helloworld/go/readme.txt ================================================ Download and install go https://golang.org/dl/ go build myrest.go then run the compiled executable ./myrest curl localhost:8000/hello ctrl-c Note: go compiles to native and if you have been using a Mac/Windows you likely need to recompile the binary env GOOS=linux GOARCH=amd64 go build myrest.go docker build -t burr/mygo:v1 . docker run -it -p 8000:8000 burr/mygo:v1 Thank you to Jesus R who figured this out for me! https://github.com/jmrodri/go-demo ================================================ FILE: apps/helloworld/nodejs/.devcontainer/Dockerfile ================================================ FROM nodeshift/centos7-s2i-nodejs:10.x LABEL maintainer="Burr Sutter \"burrsutter@gmail.com\"" EXPOSE 8000 WORKDIR /opt/app-root/src CMD ["npm", "start"] ================================================ FILE: apps/helloworld/nodejs/.devcontainer/devcontainer.json ================================================ { "name": "Node Sample", "dockerFile": "Dockerfile", "appPort": "8000", "extensions": [ // "afractal.node-essentials", "visualstudioexptteam.vscodeintellicode", "ms-vscode.node-debug2" ] } ================================================ FILE: apps/helloworld/nodejs/Dockerfile ================================================ FROM nodeshift/centos7-s2i-nodejs:10.x LABEL maintainer="Burr Sutter \"burrsutter@gmail.com\"" EXPOSE 8000 WORKDIR /opt/app-root/src COPY hello-http.js . COPY package.json . CMD ["npm", "start"] ================================================ FILE: apps/helloworld/nodejs/hello-http.js ================================================ const os = require('os'); const http = require('http'); let cnt = 0; http.createServer((req, res) => { // don't increment the counter if the favicon.ico is being requested if (req.url.toLowerCase() === '/favicon.ico') { res.writeHead(200, { 'Content-Type': 'image/x-icon' }); res.end(); console.log('favicon requested'); return; } res.end(`Node Bonjour on ${os.hostname()} ${cnt++} \n`); } ).listen(8000); console.log(`Server running at http://localhost:8000/`); ================================================ FILE: apps/helloworld/nodejs/readme.txt ================================================ Test it plain node -v v8.11.3 npm -v v8.11.3 npm start curl localhost:8000 Test it in minishift or minikube's Docker minishift docker-env minikube docker-env docker build -f Dockerfile -t dev.local/burrsutter/mynode:v1 . or docker build -f Dockerfile.openshift -t dev.local/burrsutter/mynode:v1 . docker login docker.io docker images | grep mynode docker tag $1 docker.io/burrsutter/mynode:v1 docker push docker.io/burrsutter/mynode:v1 or docker login quay.io docker images | grep mynode docker tag $1 quay.io/burrsutter/mynode:v1 docker push quay.io/burrsutter/mynode:v1 to test via Docker: docker run --rm -d -p 8000:8000 dev.local/burrsutter/mynode:v1 docker ps | grep mynode docker stop 08efa083696b ================================================ FILE: apps/helloworld/python/Dockerfile ================================================ FROM python:2 WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD [ "python", "./app.py" ] ================================================ FILE: apps/helloworld/python/app.py ================================================ import os from flask import Flask app = Flask(__name__) @app.route("/") def main(): return "Python Hello on " + os.getenv('HOSTNAME', "unknown") + "\n" if __name__ == "__main__": app.run(host='0.0.0.0',port='8000') ================================================ FILE: apps/helloworld/python/readme.txt ================================================ https://www.python.org/ftp/python/2.7.15/python-2.7.15-macosx10.9.pkg python --version Python 2.7.15 pip --version pip 19.0.3 pip install --no-cache-dir -r requirements.txt python app.py curl localhost:8000 ctrl-c docker build -t burrsutter/flask_web_app . docker run -it -p 8000:8000 --rm burrsutter/flask_web_app curl localhost:8000 ================================================ FILE: apps/helloworld/python/requirements.txt ================================================ Flask==1.0.2 ================================================ FILE: apps/helloworld/quarkus/.dockerignore ================================================ * !target/*-runner ================================================ FILE: apps/helloworld/quarkus/buildNativeLinux.sh ================================================ #!/bin/bash export GRAALVM_HOME=~/tools/graalvm-ce-19.1.1/Contents/Home/ mvn package -Pnative -Dnative-image.docker-build=true -DskipTests ================================================ FILE: apps/helloworld/quarkus/buildNativeMac.sh ================================================ #!/bin/bash export GRAALVM_HOME=~/tools/graalvm-ce-19.1.1/Contents/Home/ # Mac Native mvn package -Pnative ================================================ FILE: apps/helloworld/quarkus/build_push_docker.sh ================================================ #!/bin/bash IMAGE_VER=quarkus-demo:2.0.0 docker build -f Dockerfile -t dev.local/burrsutter/$IMAGE_VER . docker login docker.io docker tag dev.local/burrsutter/$IMAGE_VER docker.io/burrsutter/$IMAGE_VER docker push docker.io/burrsutter/$IMAGE_VER ================================================ FILE: apps/helloworld/quarkus/build_push_quay.sh ================================================ #!/bin/bash IMAGE_VER=quarkus-demo:2.0.0 docker build -f kubefiles/Dockerfile -t dev.local/burrsutter/$IMAGE_VER . docker login quay.io docker tag dev.local/burrsutter/$IMAGE_VER quay.io/burrsutter/$IMAGE_VER docker push quay.io/burrsutter/$IMAGE_VER ================================================ FILE: apps/helloworld/quarkus/dockerbuild.sh ================================================ #!/bin/bash docker build -f kubefiles/Dockerfile -t dev.local/rhdevelopers/quarkus-demo:v2 . ================================================ FILE: apps/helloworld/quarkus/dockerbuild_openshift.sh ================================================ #!/bin/bash docker build -f kubefiles/Dockerfile.openshift -t dev.local/rhdevelopers/quarkus-demo:v2 . ================================================ FILE: apps/helloworld/quarkus/dockerpush_docker.sh ================================================ #!/bin/bash # use docker images | grep quarkus to get the image ID for $1 docker login docker.io docker tag $1 docker.io/burrsutter/quarkus-demo:2.0.0 docker push docker.io/burrsutter/quarkus-demo:2.0.0 ================================================ FILE: apps/helloworld/quarkus/dockerpush_quay.sh ================================================ #!/bin/bash # use docker images | grep quarkus to get the image ID for $1 docker login quay.io docker tag $1 quay.io/rhdevelopers/quarkus-demo:v2 docker push quay.io/rhdevelopers/quarkus-demo:v2 echo 'quay.io marks repositories as private by default' echo 'to update https://screencast.com/t/uAooYnghlW' ================================================ FILE: apps/helloworld/quarkus/kubefiles/Deployment.yml ================================================ apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: myquarkus name: myquarkus spec: replicas: 1 selector: matchLabels: app: myquarkus template: metadata: labels: app: myquarkus spec: containers: - name: myquarkus image: quay.io/rhdevelopers/quarkus-demo:v2 ports: - containerPort: 8080 resources: requests: memory: "50Mi" cpu: "250m" # 1/4 core limits: memory: "50Mi" cpu: "250m" livenessProbe: httpGet: port: 8080 path: / initialDelaySeconds: 1 periodSeconds: 5 timeoutSeconds: 2 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 1 periodSeconds: 3 ================================================ FILE: apps/helloworld/quarkus/kubefiles/Deployment_quay.yml ================================================ apiVersion: extensions/v1beta1 kind: Deployment metadata: labels: app: myquarkus name: myquarkus spec: replicas: 1 selector: matchLabels: app: myquarkus template: metadata: labels: app: myquarkus spec: containers: - name: myquarkus image: quay.io/rhdevelopers/quarkus-demo:v2 ports: - containerPort: 8080 resources: requests: memory: "50Mi" cpu: "250m" # 1/4 core limits: memory: "50Mi" cpu: "250m" livenessProbe: httpGet: port: 8080 path: / initialDelaySeconds: 1 periodSeconds: 5 timeoutSeconds: 2 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 1 periodSeconds: 3 ================================================ FILE: apps/helloworld/quarkus/kubefiles/Dockerfile ================================================ FROM registry.access.redhat.com/ubi8/ubi-minimal WORKDIR /work/ COPY target/*-runner /work/application RUN chmod 775 /work EXPOSE 8080 CMD ["./application", "-Xmx8m", "-Xmn8m", "-Xms8m"] ================================================ FILE: apps/helloworld/quarkus/kubefiles/Dockerfile.openshift ================================================ FROM registry.access.redhat.com/ubi8/ubi-minimal WORKDIR /work/ RUN chgrp -R 0 /work && \ chmod -R g=u /work COPY target/*-runner /work/application EXPOSE 8080 USER 1001 ENTRYPOINT [ "./application", "-Xmx8m", "-Xmn8m", "-Xms8m" ] ================================================ FILE: apps/helloworld/quarkus/kubefiles/Service.yml ================================================ apiVersion: v1 kind: Service metadata: name: myquarkus labels: app: myquarkus spec: ports: - name: http port: 8080 selector: app: myquarkus type: LoadBalancer ================================================ FILE: apps/helloworld/quarkus/poller.sh ================================================ #!/bin/bash while true do curl $(minikube -p 9steps ip):$(kubectl get svc myapp -ojsonpath="{.spec.ports[?(@.port==8080)].nodePort}") sleep .2; done ================================================ FILE: apps/helloworld/quarkus/pom.xml ================================================ 4.0.0 com.redhat.developer.demo quarkus-demo 2.0.0 UTF-8 2.22.0 1.3.2.Final 1.8 UTF-8 1.8 io.quarkus quarkus-bom ${quarkus.version} pom import io.quarkus quarkus-resteasy io.quarkus quarkus-junit5 test io.rest-assured rest-assured test io.quarkus quarkus-maven-plugin ${quarkus.version} build maven-surefire-plugin ${surefire-plugin.version} org.jboss.logmanager.LogManager native native io.quarkus quarkus-maven-plugin ${quarkus.version} native-image true maven-failsafe-plugin ${surefire-plugin.version} integration-test verify ${project.build.directory}/${project.build.finalName}-runner ================================================ FILE: apps/helloworld/quarkus/readme.txt ================================================ mvn compile quarkus:dev curl localhost:8080 ctrl-c mvn clean package ./buildNativeLinux.sh ./dockerbuild.sh kubectl apply -f kubefiles/Deployment.yml OR kubectl apply -f kubefiles/Deployment_quay.yml kubectl apply -f kubefiles/Service.yml ./poller.sh ================================================ FILE: apps/helloworld/quarkus/src/main/java/com/redhat/developer/demo/GreetingEndpoint.java ================================================ package com.redhat.developer.demo; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/") public class GreetingEndpoint { private String prefix = "Supersonic Subatomic Java with Quarkus"; private String HOSTNAME = System.getenv().getOrDefault("HOSTNAME", "unknown"); private int count = 0; @GET @Produces(MediaType.TEXT_PLAIN) public String greet() { count++; return prefix + " " + HOSTNAME + ":" + count + "\n"; } @GET @Path("/healthz") @Produces(MediaType.TEXT_PLAIN) public String health() { return "OK"; } @GET @Path("/myresources") public String getSystemResources() { long memory = Runtime.getRuntime().maxMemory(); int cores = Runtime.getRuntime().availableProcessors(); System.out.println("/myresources " + HOSTNAME); return " Memory: " + (memory / 1024 / 1024) + " Cores: " + cores + "\n"; } @GET @Path("/consume") public String consumeSome() { System.out.println("/consume " + HOSTNAME); Runtime rt = Runtime.getRuntime(); StringBuilder sb = new StringBuilder(); long maxMemory = rt.maxMemory(); long usedMemory = 0; // while usedMemory is less than 80% of Max while (((float) usedMemory / maxMemory) < 0.80) { sb.append(System.nanoTime() + sb.toString()); usedMemory = rt.totalMemory(); } String msg = "Allocated about 80% (" + humanReadableByteCount(usedMemory, false) + ") of the max allowed JVM memory size (" + humanReadableByteCount(maxMemory, false) + ")"; System.out.println(msg); return msg + "\n"; } public static String humanReadableByteCount(long bytes, boolean si) { int unit = si ? 1000 : 1024; if (bytes < unit) return bytes + " B"; int exp = (int) (Math.log(bytes) / Math.log(unit)); String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } } ================================================ FILE: apps/helloworld/springboot/.devcontainer/Dockerfile ================================================ FROM openjdk:8u151 ENV JAVA_APP_JAR boot-demo-0.0.1.jar ## Ensure maven is installed RUN apt-get update -y && apt-get install maven -y WORKDIR /app/ EXPOSE 8080 CMD java -XX:+PrintFlagsFinal -XX:+PrintGCDetails $JAVA_OPTIONS -jar $JAVA_APP_JAR ================================================ FILE: apps/helloworld/springboot/.devcontainer/devcontainer.json ================================================ { "name": "Spring Boot Sample", "dockerFile": "Dockerfile", "appPort": "8080", "extensions": [ "vscjava.vscode-java-pack", "redhat.vscode-xml", ] } ================================================ FILE: apps/helloworld/springboot/Dockerfile ================================================ FROM openjdk:17.0-slim ENV JAVA_APP_JAR boot-demo-1.0.0.jar WORKDIR /app/ COPY target/$JAVA_APP_JAR . EXPOSE 8080 CMD java $JAVA_OPTIONS -jar $JAVA_APP_JAR ================================================ FILE: apps/helloworld/springboot/Dockerfile.openshift ================================================ FROM registry.access.redhat.com/ubi8/openjdk-8-runtime WORKDIR /work/ ENV JAVA_APP_JAR boot-demo-1.0.0.jar # the following is not needed on this Red Hat created image # RUN chgrp -R 0 /work && \ # chmod -R g=u /work COPY target/$JAVA_APP_JAR . EXPOSE 8080 USER 1001 CMD java $JAVA_OPTIONS -jar $JAVA_APP_JAR ================================================ FILE: apps/helloworld/springboot/Dockerfile_Java11 ================================================ FROM openjdk:11-jre ENV JAVA_APP_JAR boot-demo-1.0.0.jar WORKDIR /app/ COPY target/$JAVA_APP_JAR . EXPOSE 8080 CMD java -jar $JAVA_APP_JAR ================================================ FILE: apps/helloworld/springboot/Dockerfile_Memory ================================================ FROM openjdk:8u151-jre ENV JAVA_APP_JAR boot-demo-1.0.0.jar WORKDIR /app/ COPY target/$JAVA_APP_JAR . EXPOSE 8080 CMD java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:+PrintFlagsFinal -XX:+PrintGCDetails $JAVA_OPTIONS -jar $JAVA_APP_JAR ================================================ FILE: apps/helloworld/springboot/Dockerfile_Memory2 ================================================ FROM openjdk:8u131-jre ENV JAVA_APP_JAR boot-demo-1.0.0.jar WORKDIR /app/ COPY target/$JAVA_APP_JAR . EXPOSE 8080 CMD java -Xmx112M -XX:+PrintFlagsFinal -XX:+PrintGCDetails $JAVA_OPTIONS -jar $JAVA_APP_JAR ================================================ FILE: apps/helloworld/springboot/build_push_docker.sh ================================================ #!/bin/bash IMAGE_VER=boot-demo:1.0.0 docker build -f Dockerfile -t dev.local/burrsutter/$IMAGE_VER . docker login docker.io docker tag dev.local/burrsutter/$IMAGE_VER docker.io/burrsutter/$IMAGE_VER docker push docker.io/burrsutter/$IMAGE_VER ================================================ FILE: apps/helloworld/springboot/build_push_quay.sh ================================================ #!/bin/bash IMAGE_VER=boot-demo:1.0.0 docker build -f Dockerfile -t dev.local/burrsutter/$IMAGE_VER . docker login quay.io docker tag dev.local/burrsutter/$IMAGE_VER quay.io/burrsutter/$IMAGE_VER docker push quay.io/burrsutter/$IMAGE_VER ================================================ FILE: apps/helloworld/springboot/pom.xml ================================================ 4.0.0 com.burrsutter boot-demo 1.0.0 jar helloboot Demo project for Spring Boot org.springframework.boot spring-boot-starter-parent 3.3.4 UTF-8 UTF-8 17 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-jar-plugin 3.3.0 ================================================ FILE: apps/helloworld/springboot/readme.txt ================================================ Initial pom.xml created by start.spring.io mvn clean compile package java -jar target/boot-demo-1.0.0.jar or mvn spring-boot:run curl http://localhost:8080/ ctrl-c Manual Deployment export IMAGE_VER=boot-demo:1.0.0 docker build -f Dockerfile -t dev.local/burrsutter/$IMAGE_VER . docker login docker.io docker tag dev.local/burrsutter/$IMAGE_VER docker.io/burrsutter/$IMAGE_VER docker push docker.io/burrsutter/$IMAGE_VER or docker build -f Dockerfile -t dev.local/burrsutter/$IMAGE_VER . docker login quay.io docker tag dev.local/burrsutter/$IMAGE_VER quay.io/burrsutter/$IMAGE_VER docker push quay.io/burrsutter/$IMAGE_VER or docker build -f Dockerfile.openshift -t dev.local/burrsutter/$IMAGE_VER . ================================================ FILE: apps/helloworld/springboot/src/main/java/com/burrsutter/HellobootApplication.java ================================================ package com.burrsutter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class HellobootApplication { public static void main(String[] args) { SpringApplication.run(HellobootApplication.class, args); } } ================================================ FILE: apps/helloworld/springboot/src/main/java/com/burrsutter/MyRESTController.java ================================================ package com.burrsutter; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class MyRESTController { @Autowired private Environment environment; final String hostname = System.getenv().getOrDefault("HOSTNAME", "unknown"); String greeting; private int count = 0; // simple counter to see lifecycle boolean behave = true; boolean dead = false; RestTemplate restTemplate = new RestTemplate(); @GetMapping("/appendgreetingfile") public ResponseEntity appendGreetingToFile() throws IOException { try(final FileWriter fileWriter = new FileWriter("/tmp/demo/greeting.txt", true)) { fileWriter.append(environment.getProperty("GREETING","Jambo")); fileWriter.close(); } return ResponseEntity.status(HttpStatus.CREATED).build(); } @GetMapping("/readgreetingfile") public String readGreetingFile() throws IOException { return new String(Files.readAllBytes(Paths.get("/tmp/demo/greeting.txt"))); } @GetMapping("/") public String sayHello() { greeting = environment.getProperty("GREETING","Jambo"); count++; System.out.println(greeting + " from " + hostname + " " + count); return greeting + " from Spring Boot! " + count + " on " + hostname + "\n"; } @GetMapping("/sysresources") public String getSystemResources() { long memory = Runtime.getRuntime().maxMemory(); int cores = Runtime.getRuntime().availableProcessors(); System.out.println("/sysresources " + hostname); return " Memory: " + (memory / 1024 / 1024) + " Cores: " + cores + "\n"; } @GetMapping("/consume") public String consumeSome() { System.out.println("/consume " + hostname); Runtime rt = Runtime.getRuntime(); StringBuilder sb = new StringBuilder(); long maxMemory = rt.maxMemory(); long usedMemory = 0; // while usedMemory is less than 80% of Max while (((float) usedMemory / maxMemory) < 0.80) { sb.append(System.nanoTime() + sb.toString()); usedMemory = rt.totalMemory(); } String msg = "Allocated about 80% (" + humanReadableByteCount(usedMemory, false) + ") of the max allowed JVM memory size (" + humanReadableByteCount(maxMemory, false) + ")"; System.out.println(msg); return msg + "\n"; } @GetMapping("/health") public ResponseEntity health() { if (behave) { return ResponseEntity.status(HttpStatus.OK) .body("I am fine, thank you\n"); } else { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("Bad"); } } @GetMapping("/misbehave") public ResponseEntity misbehave() { behave = false; return ResponseEntity.status(HttpStatus.OK).body("Misbehaving"); } @GetMapping("/behave") public ResponseEntity behave() { behave = true; return ResponseEntity.status(HttpStatus.OK).body("Ain't Misbehaving"); } @GetMapping("/shot") public ResponseEntity shot() { dead = true; return ResponseEntity.status(HttpStatus.OK).body("I have been shot in the head"); // https://www.quora.com/Why-can-zombies-only-die-by-being-shot-in-the-head-Why-can-they-survive-all-the-blood-loss-and-still-live-If-zombies-were-real-anyway } @GetMapping("/reborn") public ResponseEntity reborn() { dead = false; return ResponseEntity.status(HttpStatus.OK).body("I have been reborn"); // https://www.quora.com/Why-can-zombies-only-die-by-being-shot-in-the-head-Why-can-they-survive-all-the-blood-loss-and-still-live-If-zombies-were-real-anyway } @GetMapping("/alive") public ResponseEntity alive() { if (!dead) { return ResponseEntity.status(HttpStatus.OK) .body("It's Alive! (Frankenstein)\n"); } else { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("All dead, not mostly dead (Princess Bride)"); } } @GetMapping("/configure") public String configure() { String databaseConn = environment.getProperty("DBCONN","Default"); String msgBroker = environment.getProperty("MSGBROKER","Default"); greeting = environment.getProperty("GREETING","Default"); String love = environment.getProperty("LOVE","Default"); return "Configuration for : " + hostname + "\n" + "databaseConn=" + databaseConn + "\n" + "msgBroker=" + msgBroker + "\n" + "greeting=" + greeting + "\n" + "love=" + love + "\n"; } @GetMapping("/callinganother") public String callinganother() { // ..svc.cluster.local String url = "http://mynode.yourspace.svc.cluster.local:8000/"; ResponseEntity response = restTemplate.getForEntity(url, String.class); String responseBody = response.getBody(); System.out.println(responseBody); return responseBody; } public static String humanReadableByteCount(long bytes, boolean si) { int unit = si ? 1000 : 1024; if (bytes < unit) return bytes + " B"; int exp = (int) (Math.log(bytes) / Math.log(unit)); String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } } ================================================ FILE: apps/kubefiles/demo-dynamic-persistent.yaml ================================================ kind: PersistentVolumeClaim apiVersion: v1 metadata: name: myboot-volumeclaim spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Mi ================================================ FILE: apps/kubefiles/demo-ingress-2.yaml ================================================ apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: example-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: /$1 spec: rules: - host: kube-devnation.info http: paths: - path: / backend: serviceName: quarkus-demo-deployment servicePort: 8080 - path: /v2 backend: serviceName: mynode-deployment servicePort: 8000 ================================================ FILE: apps/kubefiles/demo-ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: /$1 spec: rules: - host: kube-devnation.info http: paths: - pathType: Prefix path: / backend: service: name: quarkus-demo-deployment port: number: 8080 ================================================ FILE: apps/kubefiles/demo-persistent-volume-hostpath.yaml ================================================ kind: PersistentVolume apiVersion: v1 metadata: name: my-persistent-volume labels: type: local spec: storageClassName: pv-demo capacity: storage: 100Mi accessModes: - ReadWriteOnce hostPath: path: "/mnt/persistent-volume" ================================================ FILE: apps/kubefiles/demo-persistent-volume-local.yaml ================================================ apiVersion: v1 kind: PersistentVolume metadata: name: my-persistent-volume spec: capacity: storage: 10Mi volumeMode: Filesystem accessModes: - ReadWriteOnce storageClassName: pv-demo local: path: "/tmp" nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - ip-10-0-138-222 ================================================ FILE: apps/kubefiles/myboot-deployment-bad-image.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot spec: containers: - name: myboot image: quay.io/rhdevelopers/myboo:v1 ports: - containerPort: 8080 ================================================ FILE: apps/kubefiles/myboot-deployment-configuration-secret.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ports: - containerPort: 8080 volumeMounts: - name: mysecretvolume #<.> mountPath: /mystuff/secretstuff readOnly: true resources: requests: memory: "300Mi" cpu: "250m" # 1/4 core limits: memory: "400Mi" cpu: "1000m" # 1 core volumes: - name: mysecretvolume #<.> secret: secretName: mysecret ================================================ FILE: apps/kubefiles/myboot-deployment-configuration.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ports: - containerPort: 8080 envFrom: - configMapRef: name: my-config resources: requests: memory: "300Mi" cpu: "250m" # 1/4 core limits: memory: "400Mi" cpu: "1000m" # 1 core ================================================ FILE: apps/kubefiles/myboot-deployment-live-ready-aggressive.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot env: dev spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 imagePullPolicy: Always ports: - containerPort: 8080 resources: requests: memory: "300Mi" cpu: "250m" # 1/4 core limits: memory: "400Mi" cpu: "1000m" # 1 core livenessProbe: httpGet: port: 8080 path: /alive periodSeconds: 2 timeoutSeconds: 2 failureThreshold: 2 readinessProbe: httpGet: path: /health port: 8080 periodSeconds: 3 ================================================ FILE: apps/kubefiles/myboot-deployment-live-ready.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot env: dev spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 imagePullPolicy: Always ports: - containerPort: 8080 resources: requests: memory: "300Mi" cpu: "250m" # 1/4 core limits: memory: "400Mi" cpu: "1000m" # 1 core livenessProbe: httpGet: port: 8080 path: /alive initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 3 ================================================ FILE: apps/kubefiles/myboot-deployment-resources-limits-v2.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot-next name: myboot-next spec: replicas: 1 selector: matchLabels: app: myboot-next template: metadata: labels: app: myboot-next spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v3 ports: - containerPort: 8080 resources: requests: memory: "300Mi" cpu: "250m" # 1/4 core limits: memory: "900Mi" cpu: "1000m" # 1 core ================================================ FILE: apps/kubefiles/myboot-deployment-resources-limits.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ports: - containerPort: 8080 resources: requests: memory: "400Mi" cpu: "250m" # 1/4 core # NOTE: These are the same limits we tested our Docker Container with earlier # -m matches limits.memory and --cpus matches limits.cpu limits: memory: "600Mi" cpu: "1000m" # 1 core ================================================ FILE: apps/kubefiles/myboot-deployment-resources.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ports: - containerPort: 8080 resources: requests: memory: "300Mi" cpu: "10000m" # 10 cores ================================================ FILE: apps/kubefiles/myboot-deployment-startup-live-ready.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot env: dev spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 imagePullPolicy: Always ports: - containerPort: 8080 resources: requests: memory: "300Mi" cpu: "250m" # 1/4 core limits: memory: "400Mi" cpu: "1000m" # 1 core livenessProbe: httpGet: port: 8080 path: /alive periodSeconds: 2 timeoutSeconds: 2 failureThreshold: 2 readinessProbe: httpGet: path: /health port: 8080 periodSeconds: 3 startupProbe: httpGet: path: /alive port: 8080 failureThreshold: 6 periodSeconds: 5 timeoutSeconds: 1 ================================================ FILE: apps/kubefiles/myboot-deployment.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ports: - containerPort: 8080 ================================================ FILE: apps/kubefiles/myboot-node-affinity.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: color operator: In values: - blue containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ports: - containerPort: 8080 ================================================ FILE: apps/kubefiles/myboot-persistent-volume-claim.yaml ================================================ kind: PersistentVolumeClaim apiVersion: v1 metadata: name: myboot-volumeclaim spec: storageClassName: pv-demo accessModes: - ReadWriteOnce resources: requests: storage: 10Mi ================================================ FILE: apps/kubefiles/myboot-pod-affinity.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot2 name: myboot2 spec: replicas: 1 selector: matchLabels: app: myboot2 template: metadata: labels: app: myboot2 spec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - topologyKey: kubernetes.io/hostname labelSelector: matchExpressions: - key: app operator: In values: - myboot containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ports: - containerPort: 8080 ================================================ FILE: apps/kubefiles/myboot-pod-antiaffinity.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot3 name: myboot3 spec: replicas: 1 selector: matchLabels: app: myboot3 template: metadata: labels: app: myboot3 spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - topologyKey: kubernetes.io/hostname labelSelector: matchExpressions: - key: app operator: In values: - myboot containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ports: - containerPort: 8080 ================================================ FILE: apps/kubefiles/myboot-pod-volume-hostpath.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: myboot-demo spec: containers: - name: myboot-demo image: quay.io/rhdevelopers/myboot:v4 volumeMounts: - mountPath: /tmp/demo name: demo-volume volumes: - name: demo-volume hostPath: #<.> path: "/mnt/data" #<.> ================================================ FILE: apps/kubefiles/myboot-pod-volume-pvc.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: myboot-demo spec: containers: - name: myboot-demo image: quay.io/rhdevelopers/myboot:v4 volumeMounts: - mountPath: /tmp/demo name: demo-volume volumes: - name: demo-volume persistentVolumeClaim: claimName: myboot-volumeclaim ================================================ FILE: apps/kubefiles/myboot-pod-volume.yml ================================================ apiVersion: v1 kind: Pod #<.> metadata: name: myboot-demo spec: containers: - name: myboot-demo image: quay.io/rhdevelopers/myboot:v4 volumeMounts: - mountPath: /tmp/demo #<.> name: demo-volume #<.> volumes: - name: demo-volume emptyDir: {} ================================================ FILE: apps/kubefiles/myboot-pods-volume.yml ================================================ apiVersion: v1 kind: Pod metadata: name: myboot-demo spec: containers: - name: myboot-demo-1 #<.> image: quay.io/rhdevelopers/myboot:v4 volumeMounts: - mountPath: /tmp/demo name: demo-volume - name: myboot-demo-2 #<.> image: quay.io/rhdevelopers/myboot:v4 #<.> env: - name: SERVER_PORT #<.> value: "8090" volumeMounts: - mountPath: /tmp/demo name: demo-volume volumes: - name: demo-volume #<.> emptyDir: {} ================================================ FILE: apps/kubefiles/myboot-service.yml ================================================ apiVersion: v1 kind: Service metadata: name: myboot labels: app: myboot spec: ports: - name: http port: 8080 selector: app: myboot type: LoadBalancer ================================================ FILE: apps/kubefiles/myboot-toleration.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot spec: tolerations: - key: "color" operator: "Equal" value: "blue" effect: "NoSchedule" containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ports: - containerPort: 8080 ================================================ FILE: apps/kubefiles/mykafka.yml ================================================ apiVersion: kafka.strimzi.io/v1alpha1 kind: Kafka metadata: name: my-cluster spec: kafka: replicas: 3 listeners: external: type: nodeport storage: type: ephemeral zookeeper: replicas: 3 storage: type: ephemeral entityOperator: topicOperator: {} ================================================ FILE: apps/kubefiles/quarkus-daemonset.yaml ================================================ apiVersion: apps/v1 kind: DaemonSet metadata: name: quarkus-daemonset labels: app: quarkus-daemonset spec: selector: matchLabels: app: quarkus-daemonset template: metadata: labels: app: quarkus-daemonset spec: containers: - name: quarkus-daemonset image: quay.io/rhdevelopers/quarkus-demo:v1 ================================================ FILE: apps/kubefiles/quarkus-statefulset-external-svc.yaml ================================================ apiVersion: v1 kind: Service metadata: name: quarkus-statefulset-2 spec: type: LoadBalancer #<.> externalTrafficPolicy: Local #<.> selector: statefulset.kubernetes.io/pod-name: quarkus-statefulset-2 #<.> ports: - port: 8080 name: web ================================================ FILE: apps/kubefiles/quarkus-statefulset.yaml ================================================ apiVersion: apps/v1 kind: StatefulSet metadata: name: quarkus-statefulset labels: app: quarkus-statefulset spec: selector: matchLabels: app: quarkus-statefulset serviceName: "quarkus" replicas: 1 template: metadata: labels: app: quarkus-statefulset spec: containers: - name: quarkus-statefulset image: quay.io/rhdevelopers/quarkus-demo:v1 ports: - containerPort: 8080 name: web --- apiVersion: v1 kind: Service metadata: name: quarkus labels: app: quarkus-statefulset spec: ports: - port: 8080 name: web clusterIP: None selector: app: quarkus-statefulset --- ================================================ FILE: apps/kubefiles/whalesay-cronjob.yaml ================================================ apiVersion: batch/v1 kind: CronJob metadata: name: whale-say-cronjob spec: schedule: "* * * * *" #<.> jobTemplate: spec: template: metadata: labels: job-type: whale-say #<.> spec: containers: - name: whale-say-container image: docker/whalesay command: ["cowsay","Hello DevNation"] restartPolicy: Never ================================================ FILE: apps/kubefiles/whalesay-job.yaml ================================================ apiVersion: batch/v1 kind: Job metadata: name: whale-say-job #<.> spec: template: spec: containers: - name: whale-say-container image: docker/whalesay command: ["cowsay","Hello DevNation"] restartPolicy: Never ================================================ FILE: apps/pizza-operator/.dockerignore ================================================ * !target/*-runner !target/*-runner.jar !target/lib/* ================================================ FILE: apps/pizza-operator/.gitignore ================================================ # Eclipse .project .classpath .settings/ bin/ # IntelliJ .idea *.ipr *.iml *.iws # NetBeans nb-configuration.xml # Visual Studio Code .vscode .factorypath # OSX .DS_Store # Vim *.swp *.swo # patch *.orig *.rej # Maven target/ pom.xml.tag pom.xml.releaseBackup pom.xml.versionsBackup release.properties ================================================ FILE: apps/pizza-operator/.mvn/wrapper/MavenWrapperDownloader.java ================================================ /* * Copyright 2007-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: apps/pizza-operator/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar ================================================ FILE: apps/pizza-operator/README.md ================================================ # pizza-operator project This project uses Quarkus, the Supersonic Subatomic Java Framework. If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . ## Running the application in dev mode You can run your application in dev mode that enables live coding using: ``` ./mvnw quarkus:dev ``` ## Packaging and running the application The application can be packaged using `./mvnw package`. It produces the `pizza-operator-1.0.0-SNAPSHOT-runner.jar` file in the `/target` directory. Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/lib` directory. The application is now runnable using `java -jar target/pizza-operator-1.0.0-SNAPSHOT-runner.jar`. ## Creating a native executable You can create a native executable using: `./mvnw package -Pnative`. Or, if you don't have GraalVM installed, you can run the native executable build in a container using: `./mvnw package -Pnative -Dquarkus.native.container-build=true`. You can then execute your native executable with: `./target/pizza-operator-1.0.0-SNAPSHOT-runner` If you want to learn more about building native executables, please consult https://quarkus.io/guides/building-native-image. ================================================ FILE: apps/pizza-operator/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" else jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: apps/pizza-operator/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: apps/pizza-operator/pom.xml ================================================ 4.0.0 org.acme pizza-operator 1.0.0-SNAPSHOT 3.8.1 true 11 11 UTF-8 UTF-8 1.5.0.Final quarkus-universe-bom io.quarkus 1.5.0.Final 2.22.1 ${quarkus.platform.group-id} ${quarkus.platform.artifact-id} ${quarkus.platform.version} pom import io.quarkus quarkus-resteasy io.quarkus quarkus-junit5 test io.rest-assured rest-assured test io.quarkus quarkus-kubernetes-client io.quarkus quarkus-maven-plugin ${quarkus-plugin.version} build maven-compiler-plugin ${compiler-plugin.version} maven-surefire-plugin ${surefire-plugin.version} org.jboss.logmanager.LogManager native native maven-failsafe-plugin ${surefire-plugin.version} integration-test verify ${project.build.directory}/${project.build.finalName}-runner native ================================================ FILE: apps/pizza-operator/src/main/docker/Dockerfile.jvm ================================================ #### # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode # # Before building the docker image run: # # mvn package # # Then, build the image with: # # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/pizza-operator-jvm . # # Then run the container using: # # docker run -i --rm -p 8080:8080 quarkus/pizza-operator-jvm # # If you want to include the debug port into your docker image # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5050 # # Then run the container using : # # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/pizza-operator-jvm # ### FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' # Install java and the run-java script # Also set up permissions for user `1001` RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ && microdnf update \ && microdnf clean all \ && mkdir /deployments \ && chown 1001 /deployments \ && chmod "g+rwX" /deployments \ && chown 1001:root /deployments \ && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ && chown 1001 /deployments/run-java.sh \ && chmod 540 /deployments/run-java.sh \ && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" COPY target/lib/* /deployments/lib/ COPY target/*-runner.jar /deployments/app.jar EXPOSE 8080 USER 1001 ENTRYPOINT [ "/deployments/run-java.sh" ] ================================================ FILE: apps/pizza-operator/src/main/docker/Dockerfile.native ================================================ #### # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode # # Before building the docker image run: # # mvn package -Pnative -Dquarkus.native.container-build=true # # Then, build the image with: # # docker build -f src/main/docker/Dockerfile.native -t quarkus/pizza-operator . # # Then run the container using: # # docker run -i --rm -p 8080:8080 quarkus/pizza-operator # ### FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 WORKDIR /work/ COPY --chown=1001:root target/*-runner /work/application EXPOSE 8080 USER 1001 CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] ================================================ FILE: apps/pizza-operator/src/main/java/org/acme/ExampleResource.java ================================================ package org.acme; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/hello") public class ExampleResource { @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { return "hello"; } } ================================================ FILE: apps/pizza-operator/src/main/java/org/acme/KubernetesClientProducer.java ================================================ package org.acme; import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.internal.KubernetesDeserializer; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import javax.enterprise.inject.Produces; import javax.inject.Named; import javax.inject.Singleton; public class KubernetesClientProducer { @Produces @Singleton @Named("namespace") String findMyCurrentNamespace() throws IOException { return new String(Files.readAllBytes(Paths.get("/var/run/secrets/kubernetes.io/serviceaccount/namespace"))); } @Produces @Singleton KubernetesClient makeDefaultClient(@Named("namespace") String namespace) { return new DefaultKubernetesClient().inNamespace(namespace); } @Produces @Singleton NonNamespaceOperation> makeCustomHelloResourceClient(KubernetesClient defaultClient, @Named("namespace") String namespace) { KubernetesDeserializer.registerCustomKind("mykubernetes.acme.org/v1beta2", "Pizza", PizzaResource.class); CustomResourceDefinition crd = defaultClient.customResourceDefinitions() .list() .getItems() .stream() .filter(d -> "pizzas.mykubernetes.acme.org".equals(d.getMetadata().getName())) .findAny() .orElseThrow(() -> new RuntimeException("Deployment error: Custom resource definition mykubernetes.acme.org/v1beta2 not found.")); return defaultClient.customResources(crd, PizzaResource.class, PizzaResourceList.class, PizzaResourceDoneable.class).inNamespace(namespace); } } ================================================ FILE: apps/pizza-operator/src/main/java/org/acme/PizzaResource.java ================================================ package org.acme; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.fabric8.kubernetes.client.CustomResource; @JsonDeserialize public class PizzaResource extends CustomResource { private PizzaResourceSpec spec; private PizzaResourceStatus status; // getters/setters public PizzaResourceSpec getSpec() { return spec; } public void setSpec(PizzaResourceSpec spec) { this.spec = spec; } public PizzaResourceStatus getStatus() { return status; } public void setStatus(PizzaResourceStatus status) { this.status = status; } @Override public String toString() { String name = getMetadata() != null ? getMetadata().getName() : "unknown"; String version = getMetadata() != null ? getMetadata().getResourceVersion() : "unknown"; return "name=" + name + " version=" + version + " value=" + spec; } } ================================================ FILE: apps/pizza-operator/src/main/java/org/acme/PizzaResourceDoneable.java ================================================ package org.acme; import io.fabric8.kubernetes.api.builder.Function; import io.fabric8.kubernetes.client.CustomResourceDoneable; public class PizzaResourceDoneable extends CustomResourceDoneable { public PizzaResourceDoneable(PizzaResource resource, Function function) { super(resource, function); } } ================================================ FILE: apps/pizza-operator/src/main/java/org/acme/PizzaResourceList.java ================================================ package org.acme; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.fabric8.kubernetes.client.CustomResourceList; @JsonSerialize public class PizzaResourceList extends CustomResourceList { } ================================================ FILE: apps/pizza-operator/src/main/java/org/acme/PizzaResourceSpec.java ================================================ package org.acme; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.quarkus.runtime.annotations.RegisterForReflection; import java.util.ArrayList; import java.util.List; @JsonDeserialize @RegisterForReflection public class PizzaResourceSpec { @JsonProperty("toppings") private List toppings = new ArrayList<>(); @JsonProperty("sauce") private String sauce; // getters/setters public List getToppings() { return toppings; } public void setToppings(List toppings) { this.toppings = toppings; } public String getSauce() { return sauce; } public void setSauce(String sauce) { this.sauce = sauce; } } ================================================ FILE: apps/pizza-operator/src/main/java/org/acme/PizzaResourceStatus.java ================================================ package org.acme; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @JsonDeserialize public class PizzaResourceStatus { } ================================================ FILE: apps/pizza-operator/src/main/java/org/acme/PizzaResourceWatcher.java ================================================ package org.acme; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.api.model.PodSpecBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.quarkus.runtime.StartupEvent; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.enterprise.event.Observes; import javax.inject.Inject; public class PizzaResourceWatcher { @Inject KubernetesClient defaultClient; @Inject NonNamespaceOperation> crClient; void onStartup(@Observes StartupEvent event) { System.out.println("Startup"); crClient.watch(new Watcher() { //<.> @Override public void eventReceived(Action action, PizzaResource resource) { System.out.println("Event " + action.name()); if (action == Action.ADDED) { final String app = resource.getMetadata().getName(); final String sauce = resource.getSpec().getSauce(); final List toppings = resource.getSpec().getToppings(); final Map labels = new HashMap<>(); labels.put("app", app); final ObjectMetaBuilder objectMetaBuilder = new ObjectMetaBuilder().withName(app + "-pod") .withNamespace(resource.getMetadata().getNamespace()).withLabels(labels); final ContainerBuilder containerBuilder = new ContainerBuilder().withName("pizza-maker") .withImage("quay.io/lordofthejars/pizza-maker:1.0.0").withCommand("/work/application") .withArgs("--sauce=" + sauce, "--toppings=" + String.join(",", toppings)); final PodSpecBuilder podSpecBuilder = new PodSpecBuilder().withContainers(containerBuilder.build()) .withRestartPolicy("Never"); final PodBuilder podBuilder = new PodBuilder().withMetadata(objectMetaBuilder.build()) .withSpec(podSpecBuilder.build()); final Pod pod = podBuilder.build(); defaultClient.resource(pod).createOrReplace(); } } @Override public void onClose(KubernetesClientException e) { } }); } } ================================================ FILE: apps/pizza-operator/src/main/resources/META-INF/resources/index.html ================================================ pizza-operator - 1.0.0-SNAPSHOT

Congratulations, you have created a new Quarkus application.

Why do you see this?

This page is served by Quarkus. The source is in src/main/resources/META-INF/resources/index.html.

What can I do from here?

If not already done, run the application in dev mode using: mvn compile quarkus:dev.

  • Add REST resources, Servlets, functions and other services in src/main/java.
  • Your static assets are located in src/main/resources/META-INF/resources.
  • Configure your application in src/main/resources/application.properties.

Do you like Quarkus?

Go give it a star on GitHub.

How do I get rid of this page?

Just delete the src/main/resources/META-INF/resources/index.html file.

Application

  • GroupId: org.acme
  • ArtifactId: pizza-operator
  • Version: 1.0.0-SNAPSHOT
  • Quarkus Version: 1.5.0.Final
================================================ FILE: apps/pizza-operator/src/main/resources/application.properties ================================================ ================================================ FILE: apps/pizza-operator/src/test/java/org/acme/ExampleResourceTest.java ================================================ package org.acme; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; @QuarkusTest public class ExampleResourceTest { @Test public void testHelloEndpoint() { given() .when().get("/hello") .then() .statusCode(200) .body(is("hello")); } } ================================================ FILE: apps/pizza-operator/src/test/java/org/acme/NativeExampleResourceIT.java ================================================ package org.acme; import io.quarkus.test.junit.NativeImageTest; @NativeImageTest public class NativeExampleResourceIT extends ExampleResourceTest { // Execute the same tests but in native mode. } ================================================ FILE: apps/pizzas/cheese-pizza.yaml ================================================ apiVersion: mykubernetes.acme.org/v1 kind: Pizza metadata: name: cheesep spec: toppings: - mozzarella sauce: regular ================================================ FILE: apps/pizzas/meat-pizza.yaml ================================================ apiVersion: mykubernetes.acme.org/v1 kind: Pizza metadata: name: meatsp spec: toppings: - mozzarella - pepperoni - sausage - bacon sauce: extra ================================================ FILE: apps/pizzas/pizza-crd.yaml ================================================ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: pizzas.mykubernetes.acme.org labels: app: pizzamaker mylabel: stuff spec: group: mykubernetes.acme.org scope: Namespaced versions: - name: v1 served: true storage: true schema: openAPIV3Schema: description: "A custom resource for making yummy pizzas" #<.> type: object properties: spec: type: object description: "Information about our pizza" properties: toppings: #<.> type: array items: type: string description: "List of toppings for our pizza" sauce: #<.> type: string description: "The name of the sauce to use on our pizza" names: kind: Pizza #<.> listKind: PizzaList plural: pizzas singular: pizza shortNames: - pz ================================================ FILE: apps/pizzas/pizza-deployment.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: quarkus-operator-example rules: - apiGroups: - '' resources: - pods verbs: - get - list - watch - create - update - delete - patch - apiGroups: - apiextensions.k8s.io resources: - customresourcedefinitions verbs: - list - apiGroups: - mykubernetes.acme.org resources: - pizzas verbs: - list - watch --- apiVersion: v1 kind: ServiceAccount metadata: name: quarkus-operator-example --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: quarkus-operator-example subjects: - kind: ServiceAccount name: quarkus-operator-example namespace: pizzahat roleRef: kind: ClusterRole name: quarkus-operator-example apiGroup: rbac.authorization.k8s.io --- apiVersion: apps/v1 kind: Deployment metadata: name: quarkus-operator-example spec: selector: matchLabels: app: quarkus-operator-example replicas: 1 template: metadata: labels: app: quarkus-operator-example spec: serviceAccountName: quarkus-operator-example containers: - image: quay.io/rhdevelopers/pizza-operator:1.0.1 name: quarkus-operator-example imagePullPolicy: IfNotPresent ================================================ FILE: apps/pizzas/veggie-lovers.yaml ================================================ apiVersion: mykubernetes.acme.org/v1 kind: Pizza metadata: name: veggiep spec: toppings: - mozzarella - black olives sauce: extra ================================================ FILE: bin/build-site.sh ================================================ #!/bin/sh docker run -u $(id -u) -v $PWD:/antora:Z --rm -t antora/antora:2.3.1 --cache-dir=./.cache/antora github-pages.yml ================================================ FILE: documentation/antora.yml ================================================ name: kubernetes-tutorial version: v1.34 display_version: v1.34 prerelease: false nav: - modules/ROOT/nav.adoc start_page: ROOT:index.adoc ================================================ FILE: documentation/modules/ROOT/examples/PizzaResourceWatcher.java ================================================ package org.acme; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.api.model.PodSpecBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.quarkus.runtime.StartupEvent; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.enterprise.event.Observes; import javax.inject.Inject; public class PizzaResourceWatcher { @Inject KubernetesClient defaultClient; @Inject NonNamespaceOperation> crClient; void onStartup(@Observes StartupEvent event) { System.out.println("Startup"); crClient.watch(new Watcher() { //<.> @Override public void eventReceived(Action action, PizzaResource resource) { System.out.println("Event " + action.name()); if (action == Action.ADDED) { final String app = resource.getMetadata().getName(); final String sauce = resource.getSpec().getSauce(); final List toppings = resource.getSpec().getToppings(); final Map labels = new HashMap<>(); labels.put("app", app); final ObjectMetaBuilder objectMetaBuilder = new ObjectMetaBuilder().withName(app + "-pod") .withNamespace(resource.getMetadata().getNamespace()).withLabels(labels); final ContainerBuilder containerBuilder = new ContainerBuilder().withName("pizza-maker") .withImage("quay.io/lordofthejars/pizza-maker:1.0.0").withCommand("/work/application") .withArgs("--sauce=" + sauce, "--toppings=" + String.join(",", toppings)); final PodSpecBuilder podSpecBuilder = new PodSpecBuilder().withContainers(containerBuilder.build()) .withRestartPolicy("Never"); final PodBuilder podBuilder = new PodBuilder().withMetadata(objectMetaBuilder.build()) .withSpec(podSpecBuilder.build()); final Pod pod = podBuilder.build(); defaultClient.resource(pod).createOrReplace(); } } @Override public void onClose(KubernetesClientException e) { } }); } } ================================================ FILE: documentation/modules/ROOT/examples/cheese-pizza.yaml ================================================ ================================================ FILE: documentation/modules/ROOT/examples/meat-pizza.yaml ================================================ apiVersion: mykubernetes.acme.org/v1 kind: Pizza metadata: name: meatsp spec: toppings: - mozzarella - pepperoni - sausage - bacon sauce: extra ================================================ FILE: documentation/modules/ROOT/examples/myboot-deployment-configuration-secret.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app: myboot name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ports: - containerPort: 8080 volumeMounts: - name: mysecretvolume #<.> mountPath: /mystuff/secretstuff readOnly: true resources: requests: memory: "300Mi" cpu: "250m" # 1/4 core limits: memory: "400Mi" cpu: "1000m" # 1 core volumes: - name: mysecretvolume #<.> secret: secretName: mysecret ================================================ FILE: documentation/modules/ROOT/examples/myboot-deployment-live-ready-aggressive.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot env: dev spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 imagePullPolicy: Always ports: - containerPort: 8080 resources: requests: memory: "300Mi" cpu: "250m" # 1/4 core limits: memory: "400Mi" cpu: "1000m" # 1 core livenessProbe: httpGet: port: 8080 path: /alive periodSeconds: 2 timeoutSeconds: 2 failureThreshold: 2 readinessProbe: httpGet: path: /health port: 8080 periodSeconds: 3 ================================================ FILE: documentation/modules/ROOT/examples/myboot-deployment-live-ready.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot env: dev spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 imagePullPolicy: Always ports: - containerPort: 8080 resources: requests: memory: "300Mi" cpu: "250m" # 1/4 core limits: memory: "400Mi" cpu: "1000m" # 1 core livenessProbe: httpGet: port: 8080 path: /alive initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 3 ================================================ FILE: documentation/modules/ROOT/examples/myboot-deployment-startup-live-ready.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: myboot spec: replicas: 1 selector: matchLabels: app: myboot template: metadata: labels: app: myboot env: dev spec: containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 imagePullPolicy: Always ports: - containerPort: 8080 resources: requests: memory: "300Mi" cpu: "250m" # 1/4 core limits: memory: "400Mi" cpu: "1000m" # 1 core livenessProbe: httpGet: port: 8080 path: /alive periodSeconds: 2 timeoutSeconds: 2 failureThreshold: 2 readinessProbe: httpGet: path: /health port: 8080 periodSeconds: 3 startupProbe: httpGet: path: /alive port: 8080 failureThreshold: 6 periodSeconds: 5 timeoutSeconds: 1 ================================================ FILE: documentation/modules/ROOT/examples/myboot-pod-volume-hostpath.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: myboot-demo spec: containers: - name: myboot-demo image: quay.io/rhdevelopers/myboot:v4 volumeMounts: - mountPath: /tmp/demo name: demo-volume volumes: - name: demo-volume hostPath: #<.> path: "/mnt/data" #<.> ================================================ FILE: documentation/modules/ROOT/examples/myboot-pod-volume.yml ================================================ apiVersion: v1 kind: Pod #<.> metadata: name: myboot-demo spec: containers: - name: myboot-demo image: quay.io/rhdevelopers/myboot:v4 volumeMounts: - mountPath: /tmp/demo #<.> name: demo-volume #<.> volumes: - name: demo-volume emptyDir: {} ================================================ FILE: documentation/modules/ROOT/examples/myboot-pods-volume.yml ================================================ apiVersion: v1 kind: Pod metadata: name: myboot-demo spec: containers: - name: myboot-demo-1 #<.> image: quay.io/rhdevelopers/myboot:v4 volumeMounts: - mountPath: /tmp/demo name: demo-volume - name: myboot-demo-2 #<.> image: quay.io/rhdevelopers/myboot:v4 #<.> env: - name: SERVER_PORT #<.> value: "8090" volumeMounts: - mountPath: /tmp/demo name: demo-volume volumes: - name: demo-volume #<.> emptyDir: {} ================================================ FILE: documentation/modules/ROOT/examples/pizza-crd.yaml ================================================ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: pizzas.mykubernetes.acme.org labels: app: pizzamaker mylabel: stuff spec: group: mykubernetes.acme.org scope: Namespaced versions: - name: v1 served: true storage: true schema: openAPIV3Schema: description: "A custom resource for making yummy pizzas" #<.> type: object properties: spec: type: object description: "Information about our pizza" properties: toppings: #<.> type: array items: type: string description: "List of toppings for our pizza" sauce: #<.> type: string description: "The name of the sauce to use on our pizza" names: kind: Pizza #<.> listKind: PizzaList plural: pizzas singular: pizza shortNames: - pz ================================================ FILE: documentation/modules/ROOT/examples/quarkus-statefulset-external-svc.yaml ================================================ apiVersion: v1 kind: Service metadata: name: quarkus-statefulset-2 spec: type: LoadBalancer #<.> externalTrafficPolicy: Local #<.> selector: statefulset.kubernetes.io/pod-name: quarkus-statefulset-2 #<.> ports: - port: 8080 name: web ================================================ FILE: documentation/modules/ROOT/examples/whalesay-cronjob.yaml ================================================ apiVersion: batch/v1 kind: CronJob metadata: name: whale-say-cronjob spec: schedule: "* * * * *" #<.> jobTemplate: spec: template: metadata: labels: job-type: whale-say #<.> spec: containers: - name: whale-say-container image: docker/whalesay command: ["cowsay","Hello DevNation"] restartPolicy: Never ================================================ FILE: documentation/modules/ROOT/examples/whalesay-job.yaml ================================================ apiVersion: batch/v1 kind: Job metadata: name: whale-say-job #<.> spec: template: spec: containers: - name: whale-say-container image: docker/whalesay command: ["cowsay","Hello DevNation"] restartPolicy: Never ================================================ FILE: documentation/modules/ROOT/nav.adoc ================================================ * 1. Requirements ** xref:installation.adoc[Installation] *** xref:installation.adoc#tutorial-all-local[CLI] *** xref:installation.adoc#install-minikube[Install Minikube] *** xref:installation.adoc#start-kubernetes[Start Kubernetes] * 2. Beginner ** xref:kubectl.adoc[kubectl] ** xref:pod-rs-deployment.adoc[Pod, ReplicaSet, Deployment] ** xref:service.adoc[Service] ** xref:logs.adoc[Logs] ** xref:service-magic.adoc[Service Magic] ** xref:blue-green.adoc[Blue/Green Deployments] * 3. Elementary ** xref:building-images.adoc[Building Images] ** xref:resources.adoc[Resources and Limits] ** xref:rolling-updates.adoc[Rolling updates] ** xref:live-ready.adoc[Liveness, Readiness & Startup] ** xref:configmap.adoc[ConfigMap] * 4. Intermediate ** xref:secrets.adoc[Secrets] ** xref:crds.adoc[Operators] ** xref:volumes-persistentvolumes.adoc[Volumes] ** xref:taints-affinity.adoc[Taints & Affinity] ** xref::jobs-cronjobs.adoc[Jobs & CronJobs] ** xref::daemonset.adoc[DaemonSet] ** xref::statefulset.adoc[StatefulSet] * 5. Advanced ** xref:ingress.adoc[Ingress]- ================================================ FILE: documentation/modules/ROOT/pages/_attributes.adoc ================================================ :moduledir: .. :branch: master :github-repo: https://github.com/redhat-scholars/kubernetes-tutorial :openshift-version: 4.3 :vm-driver: virtualbox :profile: devnation :curl-loop-sleep-time: .3 ================================================ FILE: documentation/modules/ROOT/pages/_partials/affinity_label.adoc ================================================ // tag::openshift[] :chosen-node: ip-10-0-175-64.eu-central-1.compute.internal // end::openshift[] // tag::minikube[] :chosen-node: devnation-m02 // end::minikube[] Get a list of nodes: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get nodes ---- [.console-output] [source,bash,subs="+attributes,+quotes"] ---- NAME STATUS ROLES AGE VERSION # tag::openshift[] ip-10-0-136-107.eu-central-1.compute.internal Ready master 26h v1.16.2 ip-10-0-140-186.eu-central-1.compute.internal Ready worker 26h v1.16.2 ip-10-0-141-128.eu-central-1.compute.internal Ready worker 25h v1.16.2 ip-10-0-146-109.eu-central-1.compute.internal Ready worker 25h v1.16.2 ip-10-0-150-226.eu-central-1.compute.internal Ready worker 26h v1.16.2 ip-10-0-155-122.eu-central-1.compute.internal Ready master 26h v1.16.2 ip-10-0-162-206.eu-central-1.compute.internal Ready worker 26h v1.16.2 ip-10-0-168-102.eu-central-1.compute.internal Ready master 26h v1.16.2 #{chosen-node}# Ready worker 25h v1.16.2 # end::openshift[] # tag::minikube[] devnation Ready control-plane,master 3d v1.21.2 #{chosen-node}# Ready 42h v1.21.2 # end::minikube[] ---- Then pick a node in the list to label (such as the one highlighted) [.console-input] [source,bash,subs="+macros,+attributes,+quotes"] ---- kubectl label nodes {chosen-node} #color=blue# #<.> ---- <.> Notice that this matches the affinity in the pod [.console-output] [source,bash,subs="+attributes"] ---- node/{chosen-node} labeled ---- ================================================ FILE: documentation/modules/ROOT/pages/_partials/find_node_for_pod.adoc ================================================ [.console-input] [source,bash,subs="+macros"] ---- NODE=$(kubectl get pod -o jsonpath='{.items[0].spec.nodeName}') #<.> echo ${NODE} ---- <.> the `.items[0]` is because we're asking for all pods, but we know our list will contain only one element ================================================ FILE: documentation/modules/ROOT/pages/_partials/invoke-service.adoc ================================================ [k8s-env=''] [k8s-cli=''] [doc-sec=''] ================================================ FILE: documentation/modules/ROOT/pages/_partials/set-env-vars.adoc ================================================ .Environment Variables [cols="4*^,4*."] |=== |**Variable** |**Description** |**Default Value** | **e.g.** |REGISTRY_USERNAME |The Container Registry User Id that will be used to authenticate against the container registry `$REGISTRY_URL` | |demo |REGISTRY_PASSWORD |The Container Registry User Password that will be used to authenticate against the container registry `$REGISTRY_URL` | |demopassword |REGISTRY_URL |The Container Registry URL, defaults to https://index.docker.io |https://index.docker.io |https://quay.io/v2 |DESTINATION_IMAGE_NAME |The fully qualified image name that will be built | | quay.io/foo/bar:v1.0 |=== ================================================ FILE: documentation/modules/ROOT/pages/_partials/verify-setup.adoc ================================================ The following checks ensure that each chapter exercises are done with the right environment settings. [#minikube-config-view] [.console-input] [source,bash,subs="+macros,+attributes"] ---- minikube config view ---- The command should return an output as shown: [.console-output] [source,bash,subs="+macros,+attributes"] ---- - profile: devnation - vm-driver: virtualbox - cpus: 2 - kubernetes-version: {kubernetes-version} - memory: 6144 ---- [#k8s-cluster-info] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl cluster-info ---- The command should return an output as shown: [.console-output] [source,bash,subs="+macros,+attributes"] ---- Kubernetes master is running at https://192.168.99.100:8443 KubeDNS is running at https://192.168.99.100:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy ---- [NOTE] ==== To further debug and diagnose cluster problems, use `kubectl cluster-info dump`. ==== * Set your local docker to use the minikube docker daemon [#minikube-set-env] [.console-input] [source,bash,subs="+macros,+attributes"] ---- eval $(minikube docker-env) ---- * Kubernetes should be {kubernetes-version} [#kubectl-version] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl version ---- -- ================================================ FILE: documentation/modules/ROOT/pages/_partials/watching-logs.adoc ================================================ [kube-ns='kubernetestutorial'] [kube-svc=''] Since a Cron job source is used in this section of the tutorial, it would emit events every minute. We can watch the logs of the service to see the messages delivered. The logs could be watched using the command: [tabs] ==== kubectl:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl logs -n {kube-ns} -f -c user-container ---- -- oc:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- oc logs -n {kube-ns} -f -c user-container ---- -- ==== [TIP] ==== * Using stern with the command `stern -n {kube-ns} {kube-svc}`, to filter the logs further add `-c user-container` to the stern command. [.console-input] [source,bash,subs="+macros,+attributes"] ---- stern -n {kube-ns} -c user-container {kube-svc} ---- ==== ================================================ FILE: documentation/modules/ROOT/pages/blue-green.adoc ================================================ = Blue/Green https://martinfowler.com/bliki/BlueGreenDeployment.html[Here] you can find a description and history of Blue/Green Deployment. Make sure you are in the correct namespace :section-k8s: bluegreen :set-namespace: myspace include::partial$set-context.adoc[] Make sure nothing else is deployed: [#no-resources-blue-green] [.console-input] [source, bash] ---- kubectl get all ---- [.console-output] [source,bash] ---- No resources found in myspace namespace. ---- Deploy V1 of `myboot`: [#deploy-v1-blue-green] [.console-input] [source, bash] ---- kubectl apply -f apps/kubefiles/myboot-deployment-resources-limits.yml ---- Scale to 2 replicas: [#scale-v1-blue-green] [.console-input] [source, bash] ---- kubectl scale deployment/myboot --replicas=2 ---- Watch and `show-labels`: [#labels-v1-blue-green] [.console-input] [source, bash] ---- kubectl get pods -w --show-labels ---- Deploy the service: [#deploy-service-blue-green] [.console-input] [source, bash] ---- kubectl apply -f apps/kubefiles/myboot-service.yml ---- :section-k8s: bluegreen :service-exposed: myboot include::partial$env-curl.adoc[] And run loop script: include::partial$loop.adoc[] Deploy V2 of `myboot`: [#deploy-v2-blue-green] [.console-input] [source, bash] ---- kubectl apply -f apps/kubefiles/myboot-deployment-resources-limits-v2.yml ---- Verify that the new pod/deployment carries the new code: [#exec-v2-blue-green] [.console-input] [source, bash] ---- PODNAME=$(kubectl get pod -l app=myboot-next -o name) kubectl exec -it $PODNAME -- curl localhost:8080 ---- [.console-output] [source,bash] ---- Jambo from Spring Boot! 1 on myboot-next-66b68c6659-ftcjr ---- Now update the single Service to point to the new pod and go GREEN: [#patch-service-green] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl patch svc/myboot -p '{"spec":{"selector":{"app":"myboot-next"}}}' ---- [.console-output] [source,bash] ---- Aloha from Spring Boot! 240 on myboot-d78fb6d58-929wn Jambo from Spring Boot! 2 on myboot-next-66b68c6659-ftcjr Jambo from Spring Boot! 3 on myboot-next-66b68c6659-ftcjr Jambo from Spring Boot! 4 on myboot-next-66b68c6659-ftcjr ---- Determine that you prefer Hawaiian (blue) to French (green) and fallback: Now update the single Service to point to the new pod and go BLUE: [#patch-service-blue] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl patch svc/myboot -p '{"spec":{"selector":{"app":"myboot"}}}' ---- [.console-output] [source,bash] ---- Jambo from Spring Boot! 17 on myboot-next-66b68c6659-ftcjr Aloha from Spring Boot! 257 on myboot-d78fb6d58-vqvlb Aloha from Spring Boot! 258 on myboot-d78fb6d58-vqvlb ---- == Clean Up [#clean] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete service myboot kubectl delete deployment myboot kubectl delete deployment myboot-next ---- ================================================ FILE: documentation/modules/ROOT/pages/building-images.adoc ================================================ = Building Images // See antora yaml (such as github-pages.yml) to change what attribute docker-host is set to == Prerequisite In this section, we are assuming you are running Docker in your local machine (either using Docker Tools or native Docker). IMPORTANT: To make it work correctly, you need to run this section in a new terminal window to avoid using the Kubernetes (`minikube`) environment used in previous sections. == Build your application artifact First let's take a quick look at the application we're looking to build :quick-open-file: MyRESTController.java include::partial$tip_vscode_quick_open.adoc[] .MyRESTController.java image::hello-world-app.png[] Compile, build and test the Spring Boot Java project: [#build-building-images] [.console-input] [source, bash, subs="+attributes"] ---- cd apps/helloworld/springboot mvn clean package java -jar target/boot-demo-1.0.0.jar ---- Then `curl` it in a separate terminal: [.console-input] [source, bash] ---- curl localhost:8080 ---- [.console-output] [source,bash] ---- Aloha from Spring Boot! 1 on unknown ---- `unknown` because the environment variable is not currently set, it will be inside of a Docker container and inside of Kubernetes. == Build container image NOTE: Change `quay.io` for your registry (e.g. `docker.io`) and `{myrepo}` to your organization. This next step does assume you have a working installation of Docker for Mac/Windows/Linux. [#build-container--building-images] [.console-input] [source, bash, subs="+attributes"] ---- docker build -t quay.io/{myrepo}/myapp:v1 . ---- Results: [.console-output] [source,bash, subs="+attributes"] ---- Sending build context to Docker daemon 14.47MB Step 1/6 : FROM openjdk:8u151 ---> a30a1e547e6d Step 2/6 : ENV JAVA_APP_JAR boot-demo-1.0.0.jar ---> Using cache ---> 62b714308856 Step 3/6 : WORKDIR /app/ ---> Using cache ---> aefc5bf44b15 Step 4/6 : COPY target/$JAVA_APP_JAR . ---> f881c5f5815b Step 5/6 : EXPOSE 8080 ---> Running in 4e9adc135345 Removing intermediate container 4e9adc135345 ---> 2909459c83f6 Step 6/6 : CMD java $JAVA_OPTIONS -jar $JAVA_APP_JAR ---> Running in 46bcab555de7 Removing intermediate container 46bcab555de7 ---> 85b78b9b70b1 Successfully built 85b78b9b70b1 Successfully tagged quay.io/{myrepo}/myapp:v1 ---- == Run the container image Run and Test your newly created Docker container: [#run-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- docker run --rm -it -p 8080:8080 --name myapp quay.io/{myrepo}/myapp:v1 ---- [#curl-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- curl {docker-host}:8080 ---- [.console-output] [source,bash] ---- Aloha from Spring Boot! 1 on 76851270a3e7 ---- [#curl-sys-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- curl {docker-host}:8080/sysresources ---- [.console-output] [source,bash] ---- Memory: 1268 Cores: 3 ---- These numbers are based on the memory and CPUs allocated to the Docker daemon as seen in the image below: .Docker settings image::docker-settings.png[Docker Settings] [#curl-consume-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- curl {docker-host}:8080/consume ---- [.console-output] [source,bash] ---- Allocated about 80% (1.2 GiB) of the max allowed JVM memory size (1.2 GiB) ---- Stop & remove the Docker container: ---- control-c ---- == Run your container with constrained resources Now, constrain the resources associated with this Linux container [#run-container-constrained-building-images] [.console-input] [source, bash, subs="+attributes"] ---- docker run --rm -it -p 8080:8080 -m 400m --cpus="1" --name myapp quay.io/{myrepo}/myapp:v1 ---- Ask for the container's resources: [#curl-sys-constrained-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- curl {docker-host}:8080/sysresources ---- [.console-output] [source,bash] ---- Memory: 1268 Cores: 3 ---- Crash it: [#curl-consume-crash-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- curl {docker-host}:8080/consume ---- == Fix memory problems To correct this behavior use a different Dockerfile: [#build-mem-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- docker build -t quay.io/{myrepo}/myapp:v1 -f Dockerfile_Memory . ---- Now docker run it: [#run-sys-constrained-fix-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- docker run --rm -it -p 8080:8080 -m 400m --cpus="1" --name myapp quay.io/{myrepo}/myapp:v1 ---- And `curl` it: [#curl-sys-constrained-fix-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- curl {docker-host}:8080/sysresources ---- [.console-output] [source,bash] ---- Memory: 112 Cores: 3 ---- And try to crash it: [#curl-consume-fix-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- curl {docker-host}:8080/consume ---- [.console-output] [source,bash] ---- Allocated about 80% (98.0 MiB) of the max allowed JVM memory size (112.0 MiB) ---- Once you are happy with your container image, push it up to your favorite registry: [#push-container-building-images] [.console-input] [source, bash, subs="+attributes"] ---- docker login quay.io docker push quay.io/{myrepo}/myapp:v1 ---- [.console-output] [source,bash] ---- . . . 20c527f217db: Pushed 61c06e07759a: Pushed bcbe43405751: Pushed e1df5dc88d2c: Pushed v1: digest: sha256:d22d4af6e297a024b061dbaae05be76c771fdb1db51643dc2dd8b8e047f79647 size: 2630 ---- ================================================ FILE: documentation/modules/ROOT/pages/configmap.adoc ================================================ = ConfigMap ConfigMap is the Kubernetes resource that allows you to externalize your application's configuration. *_An app’s config is everything that is likely to vary between deploys (staging, production, developer environments, etc)._* https://12factor.net/config[12 Factor Apps] == Environment Variables MyRESTController.java includes a small chunk of code that looks to the environment [source,java] ---- @RequestMapping("/configure") public String configure() { String databaseConn = environment.getProperty("DBCONN","Default"); String msgBroker = environment.getProperty("MSGBROKER","Default"); String hello = environment.getProperty("GREETING","Default"); String love = environment.getProperty("LOVE","Default"); return "Configuration: \n" + "databaseConn=" + databaseConn + "\n" + "msgBroker=" + msgBroker + "\n" + "hello=" + hello + "\n" + "love=" + love + "\n"; } ---- Environment variables can be manipulated at the Deployment level. Changes cause Pod redeployment. Deploy `myboot`: [#deploy-myboot-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-deployment.yml ---- Deploy `myboot` Service: [#deploy-myboot-service-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-service.yml ---- And watch the pods status: :section-k8s: configmap include::partial$watching-pods.adoc[] Ask the application for its configuration: :section-k8s: configmaps :service-exposed: myboot include::partial$env-curl.adoc[] [#get-config-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl $IP:$PORT/configure ---- [.console-output] [source,bash] ---- Configuration for : myboot-66d7d57687-jsbz7 databaseConn=Default msgBroker=Default greeting=Default love=Default ---- == Set Environment Variables [#set-env-vars] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl set env deployment/myboot GREETING="namaste" \ LOVE="Aloha" \ DBCONN="jdbc:sqlserver://45.91.12.123:1443;user=MyUserName;password=*****;" ---- Watch the pods being reborn: [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE myboot-66d7d57687-jsbz7 1/1 Terminating 0 5m myboot-785ff6bddc-ghwpc 1/1 Running 0 13s ---- [#get-config2-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl $IP:$PORT/configure ---- [.console-output] [source,bash] ---- Configuration for : myboot-5fd9dd9c59-58xbh databaseConn=jdbc:sqlserver://45.91.12.123:1443;user=MyUserName;password=*****; msgBroker=Default greeting=namaste love=Aloha ---- Describe the deployment: :section-k8s: configmaps :describe-deployment-name: myboot include::partial$describe-deployment.adoc[] [.console-output] [source,bash] ---- ... Containers: myboot: Image: quay.io/burrsutter/myboot:v1 Port: 8080/TCP Host Port: 0/TCP Environment: GREETING: namaste LOVE: Aloha DBCONN: jdbc:sqlserver://45.91.12.123:1443;user=MyUserName;password=*****; Mounts: Volumes: ... ---- Remove environment variables: [#remove-env-vars-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl set env deployment/myboot GREETING- \ LOVE- \ DBCONN- ---- And verify that they have been removed: [#get-config3-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl $IP:$PORT/configure ---- [.console-output] [source,bash] ---- Configuration for : myboot-66d7d57687-xkgw6 databaseConn=Default msgBroker=Default greeting=Default love=Default ---- == Create a ConfigMap [#create-configmap-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl create cm my-config --from-env-file=apps/config/some.properties ---- [#get-configmap-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get cm kubectl get cm my-config kubectl get cm my-config -o json ---- [.console-output] [source,bash] ---- ... "data": { "GREETING": "jambo", "LOVE": "Amour" }, "kind": "ConfigMap", ... ---- Or you can describe the `ConfigMap` object: [#describe-configmap-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe cm my-config ---- [.console-output] [source,bash] ---- Name: my-config Namespace: myspace Labels: Annotations: Data ==== GREETING: ==== jambo LOVE: ==== Amour Events: ---- .Using `kubectl edit` to view resources **** For large files you might find using `kubectl edit` is more convenient for viewing resources on the cluster. In our case, we can view the config map by running the following (and aborting any changes!): include::partial$tip_vscode_kube_editor.adoc[] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl edit cm my-config ---- **** Now deploy the app with its request for the `ConfigMap`: [#deploy-myboot-configmap-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-deployment-configuration.yml ---- And get its configure endpoint: [#get-config4-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl $IP:$PORT/configure ---- [.console-output] [source,bash] ---- Configuration for : myboot-84bfcff474-x6xnt databaseConn=Default msgBroker=Default greeting=jambo love=Amour ---- And switch to the other properties file by recreating the `ConfigMap`: [#delete-pod-configmap-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete cm my-config kubectl create cm my-config --from-env-file=apps/config/other.properties kubectl delete pod -l app=myboot --wait=false ---- [#get-config5-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl $IP:$PORT/configure ---- [.console-output] [source,bash] ---- Configuration for : myboot-694954fc6d-nzdvx databaseConn=jdbc:sqlserver://123.123.123.123:1443;user=MyUserName;password=*****; msgBroker=tcp://localhost:61616?jms.useAsyncSend=true hello=Default love=Default ---- There are a lot more ways to have fun with ConfigMaps. The core documentation has you manipulate a Pod specification instead of a Deployment, but the results are basically the same: https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap == Clean Up [#clean-configmaps] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete deployment myboot kubectl delete cm my-config kubectl delete service myboot ---- ================================================ FILE: documentation/modules/ROOT/pages/crds.adoc ================================================ = Operators include::_attributes.adoc[] :watch-terminal: Terminal 2 :log-terminal: Terminal 3 :section-namespace: pizzahat Operators are a way of extending the functionality of our Kubernetes cluster by installing automated controllers to manage extensions we provide to the underlying Kubernetes API. In this section we'll take a deeper look at how operators interact with the Kubernetes API to do this .Operators in the Real World **** When demonstrating this tutorial in a master class, it can be good to show the Kafka Operator in Openshift (as roughly outlined <>). Key Points when showing on an OpenShift cluster: . Use OperatorHub to show how many different Operators there are. . Install the `AMQStreams` or `Strimzi` Operator to add Kafka support (i.e. CRDs as we'll see) to the cluster . Once the operator is installed, pick a namespace to install a `Kafka` CR in . Show in the Developer Perspective the Kafka being created by the operator In this section of the tutorial we'll be demonstrating these aspects of operators with a home grown toy "Pizza Operator" **** == Preparation === Namespace We'll need a namespace where we're house our operator deployment and our `CustomResources` upon which the operator will operate [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl create namespace {section-namespace} kubectl config set-context --current --namespace={section-namespace} ---- === Watch If it's not open already, you'll want to have a terminal open (call it *{watch-terminal}*) to watch what's going on with the pods in our current namespace :section-k8s: crd include::partial$watching-pods-with-nodes.adoc[] === Logs We'll want to open a third terminal (call it *{log-terminal}*) where we'll use a tool called `stern` to watch the output of certain pods include::partial$open-terminal-in-editor-inset.adoc[] :stern-namespace: {section-namespace} :stern-pattern: p-pod :section-k8s: crd include::partial$stern-watch.adoc[] == CRDs Custom Resources extend the API Custom Controllers provide the functionality - continually maintains the desired state - to monitor its state and reconcile the resource to match with the configuration https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/ Custom Resource Definitions (CRDs) in version 1.7 CRDs extend the Kubernetes API. We can see these api resources readily: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl api-resources ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME SHORTNAMES APIVERSION NAMESPACED KIND bindings v1 true Binding componentstatuses cs v1 false ComponentStatus configmaps cm v1 true ConfigMap endpoints ep v1 true Endpoint ... #<.> ---- <.> This list is truncated In the list you will find some of the resources we've already learned about, like `Deployments` [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl api-resources | grep Deployment ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- deployments deploy apps/v1 true Deployment ---- `CustomResourceDefinition` s are a sub-set of the Kubernetes `api-resources`. Let's see if there are any CRDs already installed in our cluster [#get-crds] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get crds --all-namespaces ---- [tabs] ==== Minikube:: + -- If you are using something like minikube, you will find that there are no CRDs installed yet [.console-output] [source,bash,subs="+macros,+attributes"] ---- No resources found ---- -- OpenShift:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME CREATED AT alertmanagerconfigs.monitoring.coreos.com 2021-07-12T01:37:49Z alertmanagers.monitoring.coreos.com 2021-07-12T01:37:53Z apiservers.config.openshift.io 2021-07-12T01:37:06Z authentications.config.openshift.io 2021-07-12T01:37:06Z authentications.operator.openshift.io 2021-07-12T01:37:53Z baremetalhosts.metal3.io 2021-07-12T01:38:25Z builds.config.openshift.io 2021-07-12T01:37:06Z catalogsources.operators.coreos.com 2021-07-12T01:37:49Z cloudcredentials.operator.openshift.io 2021-07-12T01:37:10Z ... #<.> ---- <.> This list has been truncated OpenShift is at its heart Kubernetes. One of the main ways OpenShift extends Kubernetes is via CRDs, which explains why you find so many of them installed even on the back of a fresh installation. -- ==== === Example CRD :quick-open-file: pizza-crd.yaml Let's go ahead and create our own Custom Resource Definition. Later on, this Custom Resources created from this definition will be something that our operator will operate upon. Take a look at `{quick-open-file}` to see what the CRD we'll be creating looks like include::partial$tip_vscode_quick_open.adoc[] [source, yaml] .{quick-open-file} ---- include::example$pizza-crd.yaml[] ---- <1> This is a description that will be shown when somebody attempts to describe the CRD <2> This describes one of the values our `CustomResource` will have, namely, the (`array`) list of (`string`) toppings <3> This describes the second field our `CustomResource` can define in its spec, the (`string`) name of the sauce to use <4> This is the name that our CustomResources will have. Sort of like `Deployment` or `Pod` [IMPORTANT] ==== Many CRDs include metadata about the fields that are exposed so that the CR can be validated by the Kubernetes API. Prior to API version `v1` this was not enforced, after Kubernetes v1.22 all `CustomResources` will need to be `v1` and thus will need to define their object schema ==== Now let's go ahead ad add this CRD to our cluster so that we can create `Pizza` Custom Resources. [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/pizzas/pizza-crd.yaml ---- We should now be able to see that our CRD is part of our API [#get-pizzas-crds] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get crds | grep pizza ---- Results: [.console-output] [source,bash] ---- NAME CREATED AT pizzas.mykubernetes.acme.org 2020-07-01T08:12:00Z ---- And since CRDs are a subset of all `api-resources`, we should now see `pizzas` as extending our cluster's api-resources: [#get-api-pizzas-crds] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl api-resources | grep pizzas ---- Yields: [.console-output] [source,bash] ---- pizzas pz mykubernetes.acme.org true Pizza ---- Finally, since we defined the schema for our `CustomResourceDefinition` we've made it easier for people to consume our api. CRDs hook into the `kubectl describe` functionality [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl explain pizza ---- Gives us this helpful output [.console-output] [source,bash,subs="+macros,+attributes"] ---- KIND: Pizza VERSION: mykubernetes.acme.org/v1 DESCRIPTION: A custom resource for making yummy pizzas #<.> FIELDS: apiVersion 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 kind 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 metadata Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata sauce #<.> The name of the sauce to use on our pizza toppings <[]string> #<.> List of toppings for our pizza' ---- <.> Notice that this matches our overall description of the pizza <.> This is from the Schema section of the CRD for sauce. It says that it's a string. The description comes from the description field <.> This is from the Schema section of the CRD for toppings. It says that it's an array of strings. The description comes from the description field === Deploying the Operator Our CRD is not limited to a particular namespace, but we do need a namespace to put our operator that is going to operate on our `pizza` CRs. At its heart, an operator is just an application, like the `myboot` application that we deployed previously. The difference is that the operator knows to to interact with the Kubernetes API and watch for resources that it cares about. :quick-open-file: PizzaResourceWatcher.java The Pizza operator that we're about to deploy was written in link:https://quarkus.io/[Quarkus^] using the link:https://github.com/java-operator-sdk/java-operator-sdk[java operator sdk^]. The code for this operator is present in this repo. See the `{quick-open-file}` which is one of the key classes in the operator controller: include::partial$tip_vscode_quick_open.adoc[] [.console-output] [source,java,subs="+macros,+attributes"] .{quick-open-file} ---- package org.acme; import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.api.model.PodSpecBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.quarkus.runtime.StartupEvent; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.enterprise.event.Observes; import javax.inject.Inject; public class PizzaResourceWatcher { @Inject KubernetesClient defaultClient; @Inject NonNamespaceOperation> crClient; void onStartup(@Observes StartupEvent event) { System.out.println("Startup"); crClient.watch(new Watcher() { //<.> @Override public void eventReceived(Action action, PizzaResource resource) { System.out.println("Event " + action.name()); if (action == Action.ADDED) { final String app = resource.getMetadata().getName(); final String sauce = resource.getSpec().getSauce(); final List toppings = resource.getSpec().getToppings(); final Map labels = new HashMap<>(); labels.put("app", app); final ObjectMetaBuilder objectMetaBuilder = new ObjectMetaBuilder().withName(app + "-pod") .withNamespace(resource.getMetadata().getNamespace()).withLabels(labels); final ContainerBuilder containerBuilder = new ContainerBuilder().withName("pizza-maker") .withImage("quay.io/lordofthejars/pizza-maker:1.0.0").withCommand("/work/application") .withArgs("--sauce=" + sauce, "--toppings=" + String.join(",", toppings)); final PodSpecBuilder podSpecBuilder = new PodSpecBuilder().withContainers(containerBuilder.build()) .withRestartPolicy("Never"); final PodBuilder podBuilder = new PodBuilder().withMetadata(objectMetaBuilder.build()) .withSpec(podSpecBuilder.build()); final Pod pod = podBuilder.build(); defaultClient.resource(pod).createOrReplace(); } } @Override public void onClose(KubernetesClientException e) { } }); } } ---- <.> Notice that it's watching for our custom resource of `Pizza` [TIP] ==== The creation of an operator controller is outside the scope of this tutorial. If you'd like to learn more about creating operators with Quarkus, watch link:https://bit.ly/3kwJmcd[this 20 minute tutorial^] ==== [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/pizzas/pizza-deployment.yaml ---- Soon in your watch window (*{watch-terminal}*) you should see something like this [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME READY STATUS RESTARTS AGE quarkus-operator-example-5f5bf777bc-glfg9 1/1 Running 0 58s ---- -- ==== [IMPORTANT] ==== Wait until the deployment `STATUS` of the operator is `Running` before moving on to the next section ==== === Make some Pizzas Once our operator is running, it will be on the lookout for information in our `Pizza` Custom Resources and use it to (pretend to) make some pizzas by spinning up a pod configurated with information from the Custom Resource instance. For example, consider this instance of the Pizza `CustomResourceDefinition`: [.console-output] [source,yaml,subs="+macros,+attributes"] ---- include::example$cheese-pizza.yaml[] ---- Pay special attention to: * *Sauce*: `regular` * *Toppings*: `mozzarella` Now let's create this `CustomResource`: [#create-pizzas-crds] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/pizzas/cheese-pizza.yaml kubectl get pizzas ---- [.console-output] [source,bash] ---- NAME AGE cheesep 4s ---- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe pizza cheesep ---- [.console-output] [source,bash,subs="+attributes"] ---- Name: cheesep Namespace: {section-namespace} Labels: Annotations: kubectl.kubernetes.io/last-applied-configuration: {"apiVersion":"mykubernetes.acme.org/v1beta2","kind":"Pizza","metadata":{"annotations":{},"name":"cheesep","namespace":"{section-namespace}"},"spec":... API Version: mykubernetes.acme.org/v1beta2 Kind: Pizza ... ---- And in our *{watch-terminal}* we should see how the Operator responds... [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME READY STATUS RESTARTS AGE cheesep-pod 0/1 Completed 0 3s quarkus-operator-example-5f5bf777bc-glfg9 1/1 Running 0 44m ---- -- ==== And once the `cheesep-pod` completes we should see the following in *{log-terminal}* [tabs] ==== {log-terminal}:: + -- [.console-output] [source,bash,subs="+quotes,+macros"] ---- + cheesep-pod › pizza-maker pass:[cheesep-pod pizza-maker __ ____ __ _____ ___ __ ____ ______ ] pass:[cheesep-pod pizza-maker --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ ] pass:[cheesep-pod pizza-maker -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ ] pass:[cheesep-pod pizza-maker --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ ] cheesep-pod pizza-maker 2021-07-19 08:16:26,113 INFO [io.quarkus] (main) pizza-maker 1.0-SNAPSHOT (powered by Quarkus 1.4.0.CR1) started in 1.063s. cheesep-pod pizza-maker 2021-07-19 08:16:26,114 INFO [io.quarkus] (main) Profile prod activated. cheesep-pod pizza-maker 2021-07-19 08:16:26,114 INFO [io.quarkus] (main) Installed features: [cdi] cheesep-pod pizza-maker Doing The Base cheesep-pod pizza-maker Adding Sauce #regular# cheesep-pod pizza-maker Adding Toppings #[mozzarella]# cheesep-pod pizza-maker Baking cheesep-pod pizza-maker Baked cheesep-pod pizza-maker Ready For Delivery cheesep-pod pizza-maker 2021-07-19 08:16:26,615 INFO [io.quarkus] (main) pizza-maker stopped in 0.000s ---- -- ==== Notice that *Sauce* and *Toppings* matches what was specified in the `pizza` CustomResource === Make more Pizzas :quick-open-file: meat-pizza.yaml Take a look at `{quick-open-file}` and `veggie-lovers.yaml` to show the sauce and toppings options there include::partial$tip_vscode_quick_open.adoc[] [.console-output] [source,yaml,subs="+macros,+attributes"] .{quick-open-file} ---- include::example$meat-pizza.yaml[] ---- Now make the pizzas [#create-more-pizzas-crds] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/pizzas/meat-pizza.yaml kubectl apply -f apps/pizzas/veggie-lovers.yaml kubectl get pizzas --all-namespaces ---- Pod watch in the *{watch-terminal}* should show [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME READY STATUS AGE NODE cheesep-pod 0/1 Completed 8m46s devnation meatsp-pod 0/1 ContainerCreating 8s devnation quarkus-operator-example-fdb76c946-cwmnq 1/1 Running 14m devnation veggiep-pod 0/1 ContainerCreating 6s devnation ---- -- ==== And this notice in our log terminal *{log-terminal}* [tabs] ==== {log-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- + meatsp-pod › pizza-maker pass:[meatsp-pod pizza-maker __ ____ __ _____ ___ __ ____ ______ ] pass:[meatsp-pod pizza-maker --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ ] pass:[meatsp-pod pizza-maker -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ ] pass:[meatsp-pod pizza-maker --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ ] meatsp-pod pizza-maker 2021-07-19 08:24:48,015 INFO [io.quarkus] (main) pizza-maker 1.0-SNAPSHOT (powered by Quarkus 1.4.0.CR1) started in 0.817s. meatsp-pod pizza-maker 2021-07-19 08:24:48,016 INFO [io.quarkus] (main) Profile prod activated. meatsp-pod pizza-maker 2021-07-19 08:24:48,016 INFO [io.quarkus] (main) Installed features: [cdi] meatsp-pod pizza-maker Doing The Base meatsp-pod pizza-maker Adding Sauce #extra# #<.> meatsp-pod pizza-maker Adding Toppings #[mozzarella,pepperoni,sausage,bacon]# meatsp-pod pizza-maker Baking meatsp-pod pizza-maker Baked meatsp-pod pizza-maker Ready For Delivery meatsp-pod pizza-maker 2021-07-19 08:24:48,517 INFO [io.quarkus] (main) pizza-maker stopped in 0.000s + veggiep-pod › pizza-maker pass:[veggiep-pod pizza-maker __ ____ __ _____ ___ __ ____ ______ ] pass:[veggiep-pod pizza-maker --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ ] pass:[veggiep-pod pizza-maker -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ ] pass:[veggiep-pod pizza-maker --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ ] veggiep-pod pizza-maker 2021-07-19 08:24:55,289 INFO [io.quarkus] (main) pizza-maker 1.0-SNAPSHOT (powered by Quarkus 1.4.0.CR1) started in 0.869s. veggiep-pod pizza-maker 2021-07-19 08:24:55,289 INFO [io.quarkus] (main) Profile prod activated. veggiep-pod pizza-maker 2021-07-19 08:24:55,289 INFO [io.quarkus] (main) Installed features: [cdi] veggiep-pod pizza-maker Doing The Base veggiep-pod pizza-maker Adding Sauce #extra# #<.> veggiep-pod pizza-maker Adding Toppings #[mozzarella,black olives]# veggiep-pod pizza-maker Baking veggiep-pod pizza-maker Baked veggiep-pod pizza-maker Ready For Delivery veggiep-pod pizza-maker 2021-07-19 08:24:55,790 INFO [io.quarkus] (main) pizza-maker stopped in 0.000s ---- <.> Matches `sauce` and `toppings` on the meat-pizza CR <.> Matches `sauce` and `toppings` on the veggie-lovers CR -- ==== === Cleanup Let's cleanup everything in our namespace [#delete-pizzas-crds] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete all --all #<.> kubectl delete ns {section-namespace} ---- <.> Whilst namespaces do tend to automatically cleanup the resources within them, it's usually good practice to empty them out first to ensure you don't have any `finalizer` issues [.console-output] [source,bash] ---- pizza.mykubernetes.acme.org "cheesep" deleted pizza.mykubernetes.acme.org "meatsp" deleted pizza.mykubernetes.acme.org "veggiep" deleted pod "cheesep-pod" deleted pod "meatsp-pod" deleted pod "quarkus-operator-example-fdb76c946-cwmnq" deleted pod "veggiep-pod" deleted deployment.apps "quarkus-operator-example" deleted namespace "pizzahat" deleted ---- And finally, let's remove our CRD (which was not bound to a specific namespace like `section-namespace`) [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete crd pizzas.mykubernetes.acme.org #<.> ---- <.> When deleting a crd we need to refer to it by its fully qualified name [.console-output] [source,bash,subs="+macros,+attributes"] ---- customresourcedefinition.apiextensions.k8s.io "pizzas.mykubernetes.acme.org" deleted ---- == Create some Kafka https://github.com/strimzi/strimzi-kafka-operator/blob/master/install/cluster-operator/040-Crd-kafka.yaml[Example CRD] === Kafka for Minikube Create a new namespace for this experiment: [#create-namespace-franz] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl create namespace franz kubectl config set-context --current --namespace=franz ---- For minikube, the instructions for installation can be found here: https://operatorhub.io/operator/strimzi-kafka-operator[Click Install] What follows were the instructions from a moment in time: [#minikube-install] [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl -sL https://github.com/operator-framework/operator-lifecycle-manager/releases/download/0.14.1/install.sh | bash -s 0.14.1 kubectl create -f https://operatorhub.io/install/strimzi-kafka-operator.yaml ---- === Kafka for OpenShift image:operator-hub-openshift.png[OperatorHub in OpenShift] === Verify Install [#verify-install] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get csv -n operators kubectl get crds | grep kafka ---- Start a watch in another terminal: [#watch-pods] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get pods -w ---- Then deploy the resource requesting a Kafka cluster: [#deploy-cluster] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/mykafka.yml ---- [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE my-cluster-entity-operator-66676cb9fb-fzckz 2/2 Running 0 29s my-cluster-kafka-0 2/2 Running 0 60s my-cluster-kafka-1 2/2 Running 0 60s my-cluster-kafka-2 2/2 Running 0 60s my-cluster-zookeeper-0 2/2 Running 0 92s my-cluster-zookeeper-1 2/2 Running 0 92s my-cluster-zookeeper-2 2/2 Running 0 92s ---- And you can get all information from Kafka: [#get-kafkas-crd] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get kafkas ---- [.console-output] [source,bash] ---- NAME DESIRED KAFKA REPLICAS DESIRED ZK REPLICAS my-cluster 3 3 ---- === Clean up [#clean-up] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete namespace {section-namespace} kubectl delete -f apps/pizzas/pizza-crd.yaml kubectl delete kafka my-cluster kubectl delete namespace franz ---- ================================================ FILE: documentation/modules/ROOT/pages/daemonset.adoc ================================================ = DaemonSets include::_attributes.adoc[] A DaemonSet ensures that all nodes run a copy of a Pod. As nodes are added to the cluster, Pods are added to them automatically. When the nodes are deleted, they are not rescheduled but deleted. So DaemonSet allows you to deploy a Pod across all nodes. == Preparation include::https://raw.githubusercontent.com/redhat-developer-demos/rhd-tutorial-common/master/minikube-multinode.adoc[] == DaemonSet DaemonSet is created using the Kubernetes `DaemonSet` resource: [source, yaml] ---- apiVersion: apps/v1 kind: DaemonSet metadata: name: quarkus-daemonset labels: app: quarkus-daemonset spec: selector: matchLabels: app: quarkus-daemonset template: metadata: labels: app: quarkus-daemonset spec: containers: - name: quarkus-daemonset image: quay.io/rhdevelopers/quarkus-demo:v1 ---- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/quarkus-daemonset.yaml kubectl get pods -o wide ---- [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES quarkus-daemonset-jl2t5 1/1 Running 0 23s 10.244.0.2 multinode quarkus-daemonset-r64ql 1/1 Running 0 23s 10.244.1.2 multinode-m02 ---- Notice that an instance of the Quarkus Pod is deployed to every node. === Clean Up [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete -f apps/kubefiles/quarkus-daemonset.yaml ---- ================================================ FILE: documentation/modules/ROOT/pages/exec.adoc ================================================ = Kubectl exec The exec command allows you to "shell into" your pod and execute commands inside of that tiny linux machine that is running your application. You can execute it this way: [.console-input] [source,bash] ---- kubectl exec -it {podname} -- /bin/bash ---- Or this way: [.console-input] [source,bash] ---- kubectl exec {podname} -- /somecommand ---- In this section, we will be debugging an OOMKilled that is often seen when running Java inside of a container, inside of Kubernetes. Make sure the Spring Boot pod from the Resources chapter is still running: [#get-pods-exec] [.console-input] [source, bash] ---- kubectl get pods ---- [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE myboot-d78fb6d58-69kl7 1/1 Running 2 32m ---- Then let's move inside the container by running `exec` into that running Pod: [#exec-pod-exec] [.console-input] [source, bash] ---- PODNAME=$(kubectl get pod -l app=myboot -o name) kubectl exec -it $PODNAME -- /bin/bash ---- Run `ps` command to see the current running processes: [#exec-ps-exec] [.console-input] [source, bash] ---- ps -ef ---- [.console-output] [source,bash] ---- UID PID PPID C STIME TTY TIME CMD 1000610+ 1 0 0 19:20 ? 00:00:00 /bin/sh -c java -XX:+PrintFlagsFinal -XX:+PrintGCDetails $JAVA 1000610+ 7 1 2 19:20 ? 00:00:14 java -XX:+PrintFlagsFinal -XX:+PrintGCDetails -jar boot-demo-0 1000610+ 43 0 0 19:27 pts/0 00:00:00 /bin/bash 1000610+ 49 43 0 19:29 pts/0 00:00:00 ps -ef ---- Execute a `top` to get an overview of the memory: [#exec-top-exec] [.console-input] [source, bash] ---- top ---- // The .no-query-replace tells the course ui to not attempt to replace tokens between % % [.no-query-replace] [.console-output] [source,bash] ---- top - 19:29:34 up 2 days, 7:02, 0 users, load average: 0.16, 0.13, 0.14 Tasks: 4 total, 1 running, 3 sleeping, 0 stopped, 0 zombie %Cpu(s): 2.8 us, 3.4 sy, 0.0 ni, 93.1 id, 0.1 wa, 0.3 hi, 0.3 si, 0.0 st KiB Mem : 15389256 total, 6438576 free, 2289352 used, 6661328 buff/cache KiB Swap: 0 total, 0 free, 0 used. 13142476 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1 1000610+ 20 0 4292 708 632 S 0.0 0.0 0:00.02 sh 7 1000610+ 20 0 7511676 328704 16988 S 0.0 2.1 0:14.02 java 43 1000610+ 20 0 19960 3644 3080 S 0.0 0.0 0:00.00 bash 50 1000610+ 20 0 42672 3516 3080 R 0.0 0.0 0:00.00 top ---- Get the distro: [#exec-cat-release-exec] [.console-input] [source, bash] ---- cat /etc/os-release ---- [.console-output] [source,bash] ---- PRETTY_NAME="Debian GNU/Linux 9 (stretch)" NAME="Debian GNU/Linux" VERSION_ID="9" VERSION="9 (stretch)" ID=debian HOME_URL="https://www.debian.org/" SUPPORT_URL="https://www.debian.org/support" BUG_REPORT_URL="https://bugs.debian.org/" ---- Check the free memory: [#exec-free-exec] [.console-input] [source, bash] ---- free -h ---- [.console-output] [source,bash] ---- total used free shared buff/cache available Mem: 14G 2.2G 6.1G 17M 6.4G 12G Swap: 0B 0B 0B ---- And now you might see part of the problem. "free" is not `cgroups` aware, it thinks it has access to the whole VMs memory. No wonder the JVM reports a larger than accurate Max memory: [#curl-sysresources-exec] [.console-input] [source, bash] ---- curl localhost:8080/sysresources ---- [.console-output] [source,bash] ---- Memory: 1324 Cores: 4 ---- [NOTE] ==== If using Minikube, the cores are the core count provided by `minikube --profile devnation config set cpus 4` and the memory is a subset of the memory provided by `minikube --profile devnation config set memory 6144` ==== Check your Java version: [#java-version-181-exec] [.console-input] [source, bash] ---- java -version ---- [.console-output] [source,bash] ---- openjdk version "1.8.0_181" OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13) OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode) ---- Ask the JVM about its resource availability: [#java-version-181-settings-exec] [.console-input] [source, bash] ---- java -XshowSettings:vm -version ---- [.console-output] [source,bash] ---- VM settings: Max. Heap Size (Estimated): 3.26G Ergonomics Machine Class: server Using VM: OpenJDK 64-Bit Server VM openjdk version "1.8.0_181" OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13) OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode) ---- Now check the actual `cgroups` settings: [#cat-cgroup-exec] [.console-input] [source, bash] ---- cd /sys/fs/cgroup/memory/ cat memory.limit_in_bytes ---- [.console-output] [source,bash] ---- 419430400 ---- And if you divide that 419430400 by 1024 and 1024, you end up with the 400 that was specified in the deployment YAML. If you have a JVM of 1.8.0_131 or higher then you can try the experimental options [#java-version-131-settings-exec] [.console-input] [source, bash] ---- java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XshowSettings:vm -version ---- [.console-output] [source,bash] ---- VM settings: Max. Heap Size (Estimated): 112.00M Ergonomics Machine Class: server Using VM: OpenJDK 64-Bit Server VM openjdk version "1.8.0_181" OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13) OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode) ---- To leave this pod, simply type `exit` and hit enter: [.console-input] [source, bash] ---- exit ---- == Clean Up [.console-input] [source,bash] ---- kubectl delete deployment myboot kubectl delete service myboot ---- ================================================ FILE: documentation/modules/ROOT/pages/index.adoc ================================================ = Kubernetes Tutorial Welcome to your Kubernetes Journey! Your journey contains four steps, each one of them in a different section: include::../nav.adoc[] ================================================ FILE: documentation/modules/ROOT/pages/ingress.adoc ================================================ = Ingress Make sure you are in the correct namespace. == Enable Ingress Controller In case of using `minikube` you need to enable NGNIX Ingress controller. [.console-input] [source,bash,subs="+macros,+attributes"] ---- minikube addons enable ingress -p devnation ---- Wait a minute or so and verify that it has been deployed correctly: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get pods -n ingress-nginx ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- ingress-nginx-admission-create-lqfh2 0/1 Completed 0 6m28s ingress-nginx-admission-patch-z2lzj 0/1 Completed 2 6m28s ingress-nginx-controller-69ccf5d9d8-95xgp 1/1 Running 0 6m28s ---- == Deploy Application [.console-input] [source,bash,subs="+macros,+attributes"] ---- cat < 8080:30408/TCP 11s ---- :section-k8s: ingress :service-exposed: quarkus-demo-deployment include::partial$env-curl.adoc[] == Configuring Ingress An Ingress resource is defined as: [source, yaml] ---- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: /$1 spec: rules: - host: kube-devnation.info http: paths: - pathType: Prefix path: / backend: service: name: quarkus-demo-deployment port: number: 8080 ---- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/demo-ingress.yaml ---- Get the information from the Ingress resource: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get ingress ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME CLASS HOSTS ADDRESS PORTS AGE example-ingress kube-devnation.info 192.168.99.115 80 68s ---- You need to wait until address field is set. It might take some minutes. Modify the `/etc/hosts` to point the hostname to the Ingress address. IMPORTANT: If you are using minikube, use the `minikube ip -p kube` as address because the Ingress IP is an internal IP. [.console-input] [source,bash,subs="+macros,+attributes"] ./etc/hosts ---- 172.17.0.15 kube-devnation.info ---- [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl kube-devnation.info ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- Supersonic Subatomic Java with Quarkus quarkus-demo-deployment-8cf45f5c8-qmzwl:1 ---- == Second Deployment Deploy a second version of the service: [.console-input] [source,bash,subs="+macros,+attributes"] ---- cat < The name of the job will be used as the value of a label `job-name` on any pods that are spawned by this job definition. [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/whalesay-job.yaml ---- -- ==== This should yield the following output (in successive refreshes) in *{watch-terminal}* [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash] ---- NAME READY STATUS AGE NODE whale-say-job-m8vxt 0/1 ContainerCreating 14s devnation-m02 ---- [.console-output] [source,bash] ---- NAME READY STATUS AGE NODE whale-say-job-m8vxt 1/1 Running 80s devnation-m02 ---- [.console-output] [source,bash] ---- NAME READY STATUS AGE NODE whale-say-job-m8vxt 0/1 Completed 85s devnation-m02 ---- -- ==== You can get `jobs` as any other Kubernetes resource: [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get jobs ---- [.console-output] [source,bash] ---- NAME COMPLETIONS DURATION AGE whale-say-job 1/1 20s 36s ---- -- ==== Since the job is run by a pod, to get the output of the `job` execution, we need only to get the output of the pod's logs: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl logs \ -l job-name=whale-say-job \#<.> --tail=-1 #<.> ---- <.> This is allowing us to look for any pod labeled with `job-name` (see above) set to `whale-say-job` <.> `--tail` tells the log command how many lines from the end of the (pod's) log to return. So that we can see all the whimsy in this job pod's message, we set this to `-1` to see all the linesfootnote:[Normally --tail is set to -1 by default, but that's only when requesting logs from a _single specific resource_. When there is the potential to return multiple resources' logs (as is the case here when we're asking for logs by label) the number of lines returned from each resource's logs are limited to 10 by default] [.console-output] [source,bash] ---- _________________ < Hello DevNation > ----------------- \ \ \ ## . ## ## ## == ## ## ## ## === /""""""""""""""""___/ === ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ \______ o __/ \ \ __/ \____\______/ ---- === Clean Up [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete -f apps/kubefiles/whalesay-job.yaml ---- -- ==== == CronJobs :quick-open-file: whalesay-cronjob.yaml A CronJob is defined using the Kubernetes `CronJob` resource. The name `cronjob` comes from Linux and represents some sort of batch process that is scheduled to run once or repeatedly. This concept has been translated into Kubernetes as we can see in the `{quick-open-file}` file: include::partial$tip_vscode_quick_open.adoc[] [source, yaml] .{quick-open-file} ---- include::example$whalesay-cronjob.yaml[] ---- <.> This string represents a job is executed every minute. <.> Here we specify our own additional label we'd like applied to `jobs` and `pods` created by the `cronjob`. Even though the `job-name` label will still exist, it will contain a guid on every indication meaning we can't predict what the value is a priori [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/whalesay-cronjob.yaml ---- -- ==== But then if we look to our watch window in *{watch-terminal}* [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE ---- -- ==== No Pod is running as CronJob is setting up (and is checked only once every 10 seconds or so, see warning below) While we're waiting for our cronjob to run, we can use *Terminal 1* to watch how the `cronjob` is changing: [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get cronjobs -w #<.> ---- <.> the `-w` flag says to watch the output (sort of like what we're doing in the *{watch-terminal}*) but only post back when the state of the observed resource's (in this case the `cronjob`) state changes. Here is some representative output after waiting almost 3 minutes (notice the job restarts) [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE whale-say-cronjob * * * * * False #1# 0s #20s# #<.> whale-say-cronjob * * * * * False 0 31s 51s whale-say-cronjob * * * * * False #1# 0s #80s# #<.> whale-say-cronjob * * * * * False 0 23s 103s whale-say-cronjob * * * * * False #1# 1s #2m21s# ---- <.> The first invocation took a while to start, this was not a function of the `cronjob` schedule <.> Notice that the next time the job is active is about 60s after the first job was active (by AGE). And the job after that has an age of ~60s after that -- ==== You'll notice that every time the cronjob moves to ACTIVE (see highlight above),you should see the following in *{watch-terminal}*; [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash] ---- NAME READY STATUS AGE NODE whale-say-cronjob-27108480-2ws6k 0/1 Completed 46s devnation-m02 ---- -- ==== [WARNING] ==== Per the link:https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/[official Kubernetes documentation]: A cron job creates a job object about once per execution time of its schedule. We say "about" because there are certain circumstances where two jobs might be created, or no job might be created. We attempt to make these rare, but do not completely prevent them. Therefore, jobs should be idempotent. ==== Let's examine our cronjob by using the `describe` subcommand. Use kbd:[CTRL+c] to cancel the `kubectl get cronjobs -w` command and replace with the following: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe cronjobs ---- You should then see something like this [.console-output] [source,bash,subs="+quotes"] ---- Name: whale-say-cronjob Namespace: myspace Labels: Annotations: Schedule: * * * * * Concurrency Policy: Allow Suspend: False #Successful Job History Limit: 3# #<.> Failed Job History Limit: 1 Starting Deadline Seconds: Selector: Parallelism: Completions: Pod Template: Labels: #job-type=whale-say# Containers: whale-say-container: Image: docker/whalesay Port: Host Port: Command: cowsay Hello DevNation Environment: Mounts: Volumes: #Last Schedule Time: Sat, 17 Jul 2021 08:06:00 +0000# #<.> Active Jobs: whale-say-cronjob-27108486 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulCreate 6m21s cronjob-controller Created job whale-say-cronjob-27108480 Normal SawCompletedJob 6m1s cronjob-controller Saw completed job: whale-say-cronjob-27108480, status: Complete Normal SuccessfulCreate 5m21s cronjob-controller Created job whale-say-cronjob-27108481 Normal SawCompletedJob 4m56s cronjob-controller Saw completed job: whale-say-cronjob-27108481, status: Complete Normal SuccessfulCreate 4m21s cronjob-controller Created job whale-say-cronjob-27108482 Normal SawCompletedJob 3m56s cronjob-controller Saw completed job: whale-say-cronjob-27108482, status: Complete Normal SuccessfulCreate 3m21s cronjob-controller Created job whale-say-cronjob-27108483 Normal SawCompletedJob 2m48s cronjob-controller Saw completed job: whale-say-cronjob-27108483, status: Complete Normal SuccessfulDelete 2m46s cronjob-controller Deleted job whale-say-cronjob-27108480 Normal SuccessfulCreate 2m20s cronjob-controller Created job whale-say-cronjob-27108484 Normal SawCompletedJob 104s cronjob-controller Saw completed job: whale-say-cronjob-27108484, status: Complete Normal SuccessfulDelete 101s cronjob-controller Deleted job whale-say-cronjob-27108481 Normal SuccessfulCreate 81s cronjob-controller Created job whale-say-cronjob-27108485 Normal SawCompletedJob 54s cronjob-controller Saw completed job: whale-say-cronjob-27108485, status: Complete Normal SuccessfulDelete 52s cronjob-controller Deleted job whale-say-cronjob-27108482 Normal SuccessfulCreate 21s cronjob-controller Created job whale-say-cronjob-27108486 Normal SawCompletedJob 1s cronjob-controller Saw completed job: whale-say-cronjob-27108486, status: Complete ---- <.> Kubernetes cleans up jobs after a certain amount of time <.> Notice that the _Last Schedule Time_ shows the last time a job was executed. It is important to notice that a CronJob creates a `job` (which, in turn, creates pods) whenever the schedule is activated: [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get jobs ---- With example output after the cronjob has been around for more than 3 minutes: [.console-output] [source,bash] ---- NAME COMPLETIONS DURATION AGE whale-say-cronjob-27108487 1/1 19s 2m37s whale-say-cronjob-27108488 1/1 20s 97s whale-say-cronjob-27108489 1/1 21s 37s ---- -- ==== Finally, we can see the effect of job history by logging for all our jobs [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl logs \ -l job-type=whale-say \#<.> --tail=-1 ---- <.> This time we're looking to get the logs on anything created with the label `job-type` (our custom label from above) set to `whale` .NOTE **** It would less specific but we _could_ find out whale job logs without a custom label _by instead not looking to match the value on the label_ like this: [.console-input] [source,bash,subs="+macros,+attributes,+quotes"] ---- kubectl logs #-l job-name# --tail=-1 ---- This basically states that we should match any pod with a label named `job-name` **** [.console-output] [source,bash] ---- _________________ < Hello DevNation > ----------------- \ \ \ ## . ## ## ## == ## ## ## ## === /""""""""""""""""___/ === ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ \______ o __/ \ \ __/ \____\______/ _________________ < Hello DevNation > ----------------- \ \ \ ## . ## ## ## == ## ## ## ## === /""""""""""""""""___/ === ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ \______ o __/ \ \ __/ \____\______/ _________________ < Hello DevNation > ----------------- \ \ \ ## . ## ## ## == ## ## ## ## === /""""""""""""""""___/ === ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ \______ o __/ \ \ __/ \____\______/ ---- -- ==== === Clean Up [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete -f apps/kubefiles/whalesay-cronjob.yaml ---- -- ==== ================================================ FILE: documentation/modules/ROOT/pages/kubectl.adoc ================================================ = kubectl: The Kubernetes Client [[talk]] == Talk to your Cluster [#kubectl-view-config] [.console-input] [source,bash,subs="+macros,+attributes"] ---- echo $KUBECONFIG kubectl config view ---- [[view-nodes]] == View Nodes [#kubectl-get-nodes] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get nodes kubectl get nodes --show-labels kubectl get namespaces ---- [[view-pods]] == View out-of-the-box Pods Your Kubernetes vendor likely includes many pods out-of-the-box: [#kubectl-get-pods] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get pods --all-namespaces kubectl get pods --all-namespaces --show-labels kubectl get pods --all-namespaces -o wide ---- [[deploy-app]] == Deploy Something Create a Namespace and Deploy something: [#kubectl-deploy-app] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl create namespace mystuff kubectl config set-context --current --namespace=mystuff kubectl create deployment myapp --image=quay.io/rhdevelopers/quarkus-demo:v1 ---- [[monitor-events]] == While monitoring Events [#kubectl-get-events] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get events -w --sort-by=.metadata.creationTimestamp ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- LAST SEEN TYPE REASON OBJECT MESSAGE Normal Scheduled pod/myapp-5dcbf46dfc-ghrk4 Successfully assigned mystuff/myapp-5dcbf46dfc-ghrk4 to g cp-5xldg-w-a-5ptpn.us-central1-a.c.ocp42project.internal 29s Normal SuccessfulCreate replicaset/myapp-5dcbf46dfc Created pod: myapp-5dcbf46dfc-ghrk4 29s Normal ScalingReplicaSet deployment/myapp Scaled up replica set myapp-5dcbf46dfc to 1 21s Normal Pulling pod/myapp-5dcbf46dfc-ghrk4 Pulling image "quay.io/burrsutter/quarkus-demo:1.0.0" 15s Normal Pulled pod/myapp-5dcbf46dfc-ghrk4 Successfully pulled image "quay.io/burrsutter/quarkus-dem o:1.0.0" 15s Normal Created pod/myapp-5dcbf46dfc-ghrk4 Created container quarkus-demo 15s Normal Started pod/myapp-5dcbf46dfc-ghrk4 Started container quarkus-demo ---- [[created-objects]] == Created Objects === Deployments [#kubectl-get-deployments] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get deployments ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME READY UP-TO-DATE AVAILABLE AGE myapp 1/1 1 1 95s ---- === Replicasets [#kubectl-get-replicasets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get replicasets ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME DESIRED CURRENT READY AGE myapp-5dcbf46dfc 1 1 1 2m1s ---- === Pods [#kubectl-get-podsx] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get pods --show-labels ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME READY STATUS RESTARTS AGE LABELS myapp-5dcbf46dfc-ghrk4 1/1 Running 0 2m18s app=myapp,pod-template-hash=5dcbf46dfc ---- === Logs [#kubectl-logs] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl logs -l app=myapp ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- 2020-03-22 14:41:30,497 INFO [io.quarkus] (main) Quarkus 0.22.0 started in 0.021s. Listening on: http://0.0.0.0:8080 2020-03-22 14:41:30,497 INFO [io.quarkus] (main) Installed features: [cdi, resteasy] ---- == Expose a Service [#kubectl-expose] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl expose deployment myapp --port=8080 --type=LoadBalancer ---- === while watching Services :section-k8s: kubectl include::partial$watching-services.adoc[] == Talk to the App :section-k8s: kubectl :service-exposed: myapp include::partial$env-curl.adoc[] == Scale the App Open three Terminal Windows. === Terminal 1 [#watch-pods] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get pods -w ---- === Terminal 2 :service-exposed: myapp include::partial$env-curl.adoc[] Poll the endpoint: [#poll-endpoint] [.console-input] [source,bash,subs="+macros,+attributes"] ---- while true do curl $IP:$PORT sleep {curl-loop-sleep-time} done ---- Results of the polling: [.console-output] [source,bash,subs="+macros,+attributes"] ---- Supersonic Subatomic Java with Quarkus myapp-5dcbf46dfc-ghrk4:289 Supersonic Subatomic Java with Quarkus myapp-5dcbf46dfc-ghrk4:290 Supersonic Subatomic Java with Quarkus myapp-5dcbf46dfc-ghrk4:291 Supersonic Subatomic Java with Quarkus myapp-5dcbf46dfc-ghrk4:292 Supersonic Subatomic Java with Quarkus myapp-5dcbf46dfc-ghrk4:293 ---- === Terminal 3 Change replicas: [#change-replicas] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl scale deployment myapp --replicas=3 ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME READY STATUS RESTARTS AGE myapp-5dcbf46dfc-6sn2s 0/1 ContainerCreating 0 4s myapp-5dcbf46dfc-ghrk4 1/1 Running 0 5m32s myapp-5dcbf46dfc-z6hqw 0/1 ContainerCreating 0 4s ---- Start a rolling update by changing the image: [#set-image-myboot-v1] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl set image deployment/myapp quarkus-demo=quay.io/rhdevelopers/myboot:v1 ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- Supersonic Subatomic Java with Quarkus myapp-5dcbf46dfc-6sn2s:188 Supersonic Subatomic Java with Quarkus myapp-5dcbf46dfc-z6hqw:169 Aloha from Spring Boot! 0 on myapp-58b97dbd95-vxd87 Aloha from Spring Boot! 1 on myapp-58b97dbd95-vxd87 Supersonic Subatomic Java with Quarkus myapp-5dcbf46dfc-6sn2s:189 Supersonic Subatomic Java with Quarkus myapp-5dcbf46dfc-z6hqw:170 Aloha from Spring Boot! 2 on myapp-58b97dbd95-vxd87 ---- Annotate the change cause for the records (documentation purpose): [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl annotate deployment/myapp kubernetes.io/change-cause="Reverting to old SpringBoot version" --overwrite ---- Explore the revision history: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl rollout history deployment/myapp ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- deployment.apps/myapp REVISION CHANGE-CAUSE 1 2 Restoring to old SpringBoot version ---- List the ReplicaSets for the deployment: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get rs ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME DESIRED CURRENT READY AGE myapp-65c9d96df4 3 3 3 2m myapp-67fc4b6f94 0 0 0 8m ---- Revert to the most recent successful version (e.g., from Revision 2 back to Revision 1), use the undo command: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl rollout undo deployment/myapp ---- [TIP] ==== You can specify the revision number adding the following option: `--to-revision=1` ==== [.console-output] [source,bash,subs="+macros,+attributes"] ---- Aloha from Spring Boot! 16 on myapp-fc6b78bb-d495j Supersonic Subatomic Java with Quarkus myapp-76d84b5f46-jlzs7:1 Supersonic Subatomic Java with Quarkus myapp-76d84b5f46-jlzs7:2 Aloha from Spring Boot! 17 on myapp-fc6b78bb-d495j Aloha from Spring Boot! 18 on myapp-fc6b78bb-d495j Supersonic Subatomic Java with Quarkus myapp-76d84b5f46-jlzs7:3 Aloha from Spring Boot! 19 on myapp-fc6b78bb-d495j Supersonic Subatomic Java with Quarkus myapp-76d84b5f46-jlzs7:4 ---- === Clean Up [#delete-namespace] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete namespace mystuff kubectl config set-context --current --namespace=default ---- ================================================ FILE: documentation/modules/ROOT/pages/live-ready.adoc ================================================ = Liveness & Readiness Make sure you are in the correct namespace: :section-k8s: liveready :set-namespace: myspace include::partial$set-context.adoc[] Make sure nothing else is deployed: [#no-resources-live-ready] [.console-input] [source, bash] ---- kubectl get all ---- [.console-output] [source.bash] ---- No resources found in myspace namespace. ---- :quick-open-file: myboot-deployment-live-ready.yml Now we're going to deploy our application with a Liveness and Readiness probe set. Take a look at `{quick-open-file}` include::partial$tip_vscode_quick_open.adoc[] [.console-output] [source,yaml] .{quick-open-file} ---- include::example$myboot-deployment-live-ready.yml[] ---- Now apply this deployment with the following command [#create-app-live-ready] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-deployment-live-ready.yml ---- Describe the deployment: :describe-deployment-name: myboot :section-k8s: live-ready include::partial$describe-deployment.adoc[] [.console-output] [source.bash] ---- ... Image: quay.io/rhdevelopers/myboot:v1 Port: 8080/TCP Host Port: 0/TCP Limits: cpu: 1 memory: 400Mi Requests: cpu: 250m memory: 300Mi Liveness: http-get http://:8080/ delay=10s timeout=2s period=5s #success=1 #failure=3 Readiness: http-get http://:8080/health delay=10s timeout=1s period=3s #success=1 #failure=3 ... ---- Deploy a Service: [#deploy-service-live-ready] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-service.yml ---- :section-k8s: liveready :service-exposed: myboot include::partial$env-curl.adoc[] And run loop script: include::partial$loop.adoc[] Change the image: [#change-deployment-v2-live-ready] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl set image deployment/myboot myboot=quay.io/rhdevelopers/myboot:v2 ---- And notice the error free rolling update: [.console-output] [source.bash] ---- Aloha from Spring Boot! 131 on myboot-845968c6ff-k4rvb Aloha from Spring Boot! 134 on myboot-845968c6ff-9wvt9 Aloha from Spring Boot! 122 on myboot-845968c6ff-9824z Bonjour from Spring Boot! 0 on myboot-8449d5468d-m88z4 Bonjour from Spring Boot! 1 on myboot-8449d5468d-m88z4 Aloha from Spring Boot! 135 on myboot-845968c6ff-9wvt9 Aloha from Spring Boot! 133 on myboot-845968c6ff-k4rvb Aloha from Spring Boot! 137 on myboot-845968c6ff-9wvt9 Bonjour from Spring Boot! 3 on myboot-8449d5468d-m88z4 ---- Look at the Endpoints to see which pods are part of the Service: [#get-endpoints-before] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get endpoints myboot -o json | jq '.subsets[].addresses[].ip' ---- These are the Pod IPs that have passed their readiness probes: [.console-output] [source.bash] ---- "10.129.2.40" "10.130.2.37" "10.130.2.38" ---- == Readiness Probe Exec into a single Pod and change its readiness flag: [#misbehave-app-live-ready] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl exec -it myboot-845968c6ff-k5lcb -- /bin/bash ---- [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl localhost:8080/misbehave exit ---- See that the pod is no longer Ready: [.console-output] [source.bash] ---- NAME READY STATUS RESTARTS AGE myboot-845968c6ff-9wshg 1/1 Running 0 11m myboot-845968c6ff-k5lcb 0/1 Running 0 12m myboot-845968c6ff-zsgx2 1/1 Running 0 11m ---- Now check the Endpoints: [#get-endpoints-after] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get endpoints myboot -o json | jq '.subsets[].addresses[].ip' ---- And that pod is now missing from the Service's loadbalancer: [.console-output] [source.bash] ---- "10.130.2.37" "10.130.2.38" ---- Which is also self-evident in the curl loop: [.console-output] [source.bash] ---- Aloha from Spring Boot! 845 on myboot-845968c6ff-9wshg Aloha from Spring Boot! 604 on myboot-845968c6ff-zsgx2 Aloha from Spring Boot! 846 on myboot-845968c6ff-9wshg ---- == Liveness Probe [#change-deployment-v3-live-ready] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl set image deployment/myboot myboot=quay.io/rhdevelopers/myboot:v3 ---- Let the rollout finish to completion across all 3 replicas: [.console-output] [source.bash] ---- kubectl get pods -w NAME READY STATUS RESTARTS AGE myboot-56659c9d69-6sglj 1/1 Running 0 2m2s myboot-56659c9d69-mdllq 1/1 Running 0 97s myboot-56659c9d69-zjt6q 1/1 Running 0 72s ---- And as seen in the curl loop/poller: [.console-output] [source.bash] ---- Jambo from Spring Boot! 40 on myboot-56659c9d69-mdllq Jambo from Spring Boot! 26 on myboot-56659c9d69-zjt6q Jambo from Spring Boot! 71 on myboot-56659c9d69-6sglj ---- Edit the Deployment to point to the /alive URL: include::partial$tip_vscode_kube_editor.adoc[] [#change-liveness-v3-live-ready] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl edit deployment myboot ---- And change the liveness probe: [.console-output] [source.bash] ---- ... spec: containers: - image: quay.io/rhdevelopers/myboot:v3 imagePullPolicy: Always livenessProbe: failureThreshold: 3 httpGet: path: /alive port: 8080 scheme: HTTP initialDelaySeconds: 10 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 2 name: myboot ... ---- Save and close the editor, allowing that change to rollout: [.console-input] [source,bash] ---- kubectl get pods -w ---- [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE myboot-558b4f8678-nw762 1/1 Running 0 59s myboot-558b4f8678-qbrgc 1/1 Running 0 81s myboot-558b4f8678-z7f9n 1/1 Running 0 36s ---- Now pick one of the pods, `exec` into it and shoot it: [#shot-v3-live-ready] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl exec -it myboot-558b4f8678-qbrgc -- /bin/bash ---- [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl localhost:8080/shot ---- And you will see it get restarted: [.console-output] [source.bash] ---- NAME READY STATUS RESTARTS AGE myboot-558b4f8678-nw762 1/1 Running 0 4m7s myboot-558b4f8678-qbrgc 1/1 Running 1 4m29s myboot-558b4f8678-z7f9n 1/1 Running 0 3m44s ---- Plus, your exec will be terminated: [.console-input] [source,bash] ---- kubectl exec -it myboot-558b4f8678-qbrgc -- /bin/bash ---- [.console-output] [source.bash] ---- curl localhost:8080/shot ---- [.console-output] [source.bash] ---- I have been shot in the head1000610000@myboot-558b4f8678-qbrgc:/app$ command terminated with exit code 137 ---- And your end-users will not see any errors: [.console-output] [source.bash] ---- Jambo from Spring Boot! 174 on myboot-558b4f8678-z7f9n Jambo from Spring Boot! 11 on myboot-558b4f8678-qbrgc Jambo from Spring Boot! 12 on myboot-558b4f8678-qbrgc Jambo from Spring Boot! 206 on myboot-558b4f8678-nw762 Jambo from Spring Boot! 207 on myboot-558b4f8678-nw762 Jambo from Spring Boot! 175 on myboot-558b4f8678-z7f9n Jambo from Spring Boot! 176 on myboot-558b4f8678-z7f9n ---- == Clean up [#cleanup-live-ready] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete deployment myboot ---- == Startup Probe Some applications require an additional startup time on their first initialization. It might be tricky to fit this scenario into the liveness/readiness probes as you need to configure them for their normal behaviour to detect abnormalities during the running time and moreover covering the long start up time. :quick-open-file: myboot-deployment-live-ready-aggressive.yml For instance, what if we had an application that might deadlock and we want to catch such issues immediately, we might have liveness and readiness probes that look like in `apps/kubefiles/{quick-open-file}` include::partial$tip_vscode_quick_open.adoc[] [.console-output] [source,yaml] .{quick-open-file} ---- include::example$myboot-deployment-live-ready-aggressive.yml[] ---- Then apply that deployment [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-deployment-live-ready-aggressive.yml ---- As we'll see from the pod watch, the pods are continually getting restarted, sometimes after it successfully boots up (because kubelet schedules for restart) and this is due to the startup time of SpringBoot. [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe pods ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 96s default-scheduler Successfully assigned myspace/myboot-849ccd6948-8vrfq to devnation Normal Pulled 92s kubelet Successfully pulled image "quay.io/rhdevelopers/myboot:v1" in 3.295180194s Normal Created 55s (x2 over 92s) kubelet Created container myboot Normal Started 55s (x2 over 92s) kubelet Started container myboot Normal Pulled 55s kubelet Successfully pulled image "quay.io/rhdevelopers/myboot:v1" in 3.289395484s Warning Unhealthy 52s (x4 over 90s) kubelet Liveness probe failed: Get "http://172.17.0.4:8080/alive": dial tcp 172.17.0.4:8080: connect: connection refused Normal Killing 52s (x2 over 88s) kubelet Container myboot failed liveness probe, will be restarted Normal Pulling 22s (x3 over 95s) kubelet Pulling image "quay.io/rhdevelopers/myboot:v1" Warning Unhealthy 19s (x10 over 88s) kubelet Readiness probe failed: Get "http://172.17.0.4:8080/health": dial tcp 172.17.0.4:8080: connect: connection refused ---- *Startup probes* fix this problem, as once the startup probe has succeeded, the rest of the probes take over, but until the startup probe passes, neither the liveness nor the readiness probes can run. :quick-open-file: myboot-deployment-startup-live-ready.yml `{quick-open-file}` is an example of a deployment with just such a probe include::partial$tip_vscode_quick_open.adoc[] [.console-output] [source,yaml] .{quick-open-file} ---- include::example$myboot-deployment-startup-live-ready.yml[] ---- You'll see the difference is this section [.console-output] [source,yaml] ---- startupProbe: httpGet: path: /alive port: 8080 failureThreshold: 6 periodSeconds: 5 timeoutSeconds: 1 ---- Then apply that deployment [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-deployment-startup-live-ready.yml ---- The startup probe waits for 30 seconds (`5 * 6`) to startup the application. Notice, too, that the delay on the liveness and readiness checks has gone down to 0. [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get pods -w ---- [.console-output] [source.bash] ---- NAME READY STATUS RESTARTS AGE myboot-579cc5cc47-2bk5p 0/1 Running 0 67s ---- Eventually your curl loop should show the pod running ---- Aloha from Spring Boot! 18 on myboot-849ccd6948-8vrfq Aloha from Spring Boot! 19 on myboot-849ccd6948-8vrfq Aloha from Spring Boot! 20 on myboot-849ccd6948-8vrfq Aloha from Spring Boot! 21 on myboot-849ccd6948-8vrfq ---- Let's show that the liveness probe has taken over. Now pick one of the pods, `exec` into it and shoot it: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl exec -it myboot-558b4f8678-qbrgc -- /bin/bash ---- [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl localhost:8080/shot ---- And you will see it get restarted. Describe the pod to get the statistics of probes: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe pod myboot-579cc5cc47-2bk5p ---- [.console-output] [source.yaml] ---- Limits: cpu: 1 memory: 400Mi Requests: cpu: 250m memory: 300Mi Liveness: http-get http://:8080/ delay=10s timeout=2s period=5s #success=1 #failure=3 Readiness: http-get http://:8080/health delay=10s timeout=1s period=3s #success=1 #failure=3 Startup: http-get http://:8080/alive delay=0s timeout=1s period=5s #success=1 #failure=12 Environment: Mounts: ---- == Clean Up [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete deployment myboot kubectl delete svc myboot ---- ================================================ FILE: documentation/modules/ROOT/pages/logs.adoc ================================================ = Logs There are various "production-ready" ways to do log gathering and viewing across a Kubernetes/OpenShift cluster. Many folks like some flavor of ELK (ElasticSearch, Logstash, Kibana) or EFK (ElasticSearch, FluentD, Kibana). The focus here is on things a developer needs to get access to do in order to help understand the behavior of their application running inside of a pod. Make sure you have an application (Deployment) running: [#create-deployment] [.console-input] [source,bash,subs="+macros,+attributes"] ---- cat < Annotations: kubectl.kubernetes.io/last-applied-configuration: {"apiVersion":"apps/v1","kind":"ReplicaSet","metadata":{"annotations":{},"name":"rs-quarkus-demo","namespace":"myspace"},"spec":{"replicas... Replicas: 3 current / 3 desired Pods Status: 3 Running / 0 Waiting / 0 Succeeded / 0 Failed Pod Template: Labels: app=quarkus-demo env=dev Containers: quarkus-demo: Image: quay.io/rhdevelopers/quarkus-demo:v1 Port: Host Port: Environment: Mounts: Volumes: Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulCreate 89s replicaset-controller Created pod: rs-quarkus-demo-jd6jk Normal SuccessfulCreate 89s replicaset-controller Created pod: rs-quarkus-demo-t26gt Normal SuccessfulCreate 89s replicaset-controller Created pod: rs-quarkus-demo-mlnng ---- Pods are "owned" by the ReplicaSet: [#rs-owned-ref] [.console-input] [source,bash] ---- kubectl get pod rs-quarkus-demo-mlnng -o jsonpath='{.metadata.ownerReferences[]}' ---- [.console-output] [source,bash] ---- { "apiVersion": "apps/v1", "blockOwnerDeletion": true, "controller": true, "kind": "ReplicaSet", "name": "rs-quarkus-demo", "uid": "1ed3bb94-dfa5-40ef-8f32-fbc9cf265324" } ---- Now delete a pod, while watching pods: [#delete-pod-rs] [.console-input] [source,bash] ---- kubectl delete pod rs-quarkus-demo-mlnng ---- And a new pod will spring to life to replace it: [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE LABELS rs-quarkus-demo-2txwk 0/1 ContainerCreating 0 2s app=quarkus-demo,env=dev rs-quarkus-demo-jd6jk 1/1 Running 0 109s app=quarkus-demo,env=dev rs-quarkus-demo-t26gt 1/1 Running 0 109s app=quarkus-demo,env=dev ---- Delete the ReplicaSet to remove all the associated pods: [#delete-rs] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete rs rs-quarkus-demo ---- == Deployment [#create-deployment] [.console-input] [source,bash,subs="+macros,+attributes"] ---- cat < Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-vlzsl (ro) Conditions: Type Status Initialized True Ready True ContainersReady True PodScheduled True Volumes: default-token-vlzsl: Type: Secret (a volume populated by a Secret) SecretName: default-token-vlzsl Optional: false QoS Class: BestEffort Node-Selectors: Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s node.kubernetes.io/unreachable:NoExecute for 300s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled default-scheduler Successfully assigned myspace/myboot-66d7d57687-jzbzj to gcp-5xldg-w-b-rlp45.us-central1-b.c.ocp42project.internal Normal Pulled 12m kubelet, gcp-5xldg-w-b-rlp45.us-central1-b.c.ocp42project.internal Container image "quay.io/burrsutter/myboot:v1" already present on machine Normal Created 12m kubelet, gcp-5xldg-w-b-rlp45.us-central1-b.c.ocp42project.internal Created container myboot Normal Started 12m kubelet, gcp-5xldg-w-b-rlp45.us-central1-b.c.ocp42project.internal Started container myboot ---- Delete that deployment: [#delete-deployment-resource] [.console-input] [source, bash] ---- kubectl delete deployment myboot ---- Create a new deployment with resource requests: [#limits-resource] [.console-input] [source, bash] ---- kubectl apply -f apps/kubefiles/myboot-deployment-resources.yml ---- And check the status of the Pod: [#limits-get-pod-resource] [.console-input] [source, bash] ---- kubectl get pods ---- [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE myboot-7b7d754c86-kjwlr 0/1 Pending 0 19s ---- If you want to get more information about the error: [#get-events-resource] [.console-input] [source, bash] ---- kubectl get events --sort-by=.metadata.creationTimestamp ---- [.console-output] [source,bash] ---- Warning FailedScheduling pod/myboot-7b7d754c86-kjwlr 0/6 nodes are available: 6 Insufficient cpu. Warning FailedScheduling pod/myboot-7b7d754c86-kjwlr 0/6 nodes are available: 6 Insufficient cpu. ---- The "resource requests" of the pod specification require that at least one worker node has N cores and X memory available. If there is no worker node that meets the requirements, you receive "PENDING" and the appropriate notations in the events listing. You can also use `kubectl describe` on the pod to find more information about the failure. :section-k8s: resource-limit :label-describe: app=myboot include::partial$describe.adoc[] We should fix the deployment while keeping a history of changes done by `replace`: [#apply-deployment-sane-limit-resource] [.console-input] [source, bash] ---- kubectl replace -f apps/kubefiles/myboot-deployment-resources-limits.yml ---- The above command will replace the Deployment template and instruct the Pod to have container limits. Describe the Pod: :section-k8s: resource-soft-limit :label-describe: app=myboot include::partial$describe.adoc[] Deploy the service: [#apply-service-sane-limit-resource] [.console-input] [source, bash] ---- kubectl apply -f apps/kubefiles/myboot-service.yml ---- And watch your Pods: :section-k8s: resources include::partial$watching-pods.adoc[] In another Terminal, loop and curl that service: :section-k8s: resource-soft-limit :service-exposed: myboot include::partial$env-curl.adoc[] Execute in loop: include::partial$loop.adoc[] In yet another terminal window, curl the /sysresources endpoint: [#sysresources-sane-limit-resource] [.console-input] [source, bash] ---- curl $IP:$PORT/sysresources ---- NOTE: The reported memory vs what was set in the resource limits [#podresources-sane-limit-resource] [.console-input] [source, bash] ---- PODNAME=$(kubectl get pod -l app=myboot -o name) kubectl get $PODNAME -o jsonpath='{.spec.containers[*].resources}' ---- [.console-output] [source,bash] ---- { "limits": { "cpu": "1", "memory": "400Mi" }, "requests": { "cpu": "250m", "memory": "300Mi" } } ---- Then `curl` the `/consume` endpoint: [#consume-sane-limit-resource] [.console-input] [source, bash] ---- curl $IP:$PORT/consume ---- [.console-output] [source,bash] ---- curl: (52) Empty reply from server ---- And you should notice that your loop also fails: [.console-output] [source,bash] ---- Aloha from Spring Boot! 1120 on myboot-d78fb6d58-69kl7 curl: (56) Recv failure: Connection reset by peer ---- Describe the Pod to see the error: :section-k8s: resource-soft-limit-fail :label-describe: app=myboot include::partial$describe.adoc[] And look for the following part: [.console-output] [source,bash] ---- Last State: Terminated Reason: OOMKilled Exit Code: 137 ---- [#terminated-pod-resource] [.console-input] [source, bash] ---- kubectl get $PODNAME -o jsonpath='{.status.containerStatuses[0].lastState.terminated'} ---- [.console-output] [source,bash] ---- { "containerID": "cri-o://7b9be70ce4b616d6083d528dee708cea879da967373dad0d396fb999bd3898d3", "exitCode": 137, "finishedAt": "2020-03-29T19:14:56Z", "reason": "OOMKilled", "startedAt": "2020-03-29T18:50:15Z" } ---- You might even see the STATUS column of the `kubectl get pods -w` reflect the OOMKilled: [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE myboot-d78fb6d58-69kl7 0/1 OOMKilled 1 30m ---- And you will notice that the RESTARTS column increments with each crash of the Spring Boot Pod. ================================================ FILE: documentation/modules/ROOT/pages/rolling-updates.adoc ================================================ = Rolling updates Make sure you are in the correct namespace :section-k8s: rolling :set-namespace: myspace include::partial$set-context.adoc[] [TIP,subs="attributes+,+macros"] ==== If you just came from xref::resources.adoc[the Resources and Limits section, window=_blank] then you should already have the pods and deployments active that you need. If not, you will need run the following commands to deploy the needed elements into {set-namespace} ==== Deploy the Spring Boot app if needed: [#deploy-myboot-rolling] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-deployment-resources-limits.yml kubectl apply -f apps/kubefiles/myboot-service.yml ---- *Terminal 1*: watch the Pods. include::partial$watching-pods.adoc[] *Terminal 2*: curl loop the service. :service-exposed: myboot include::partial$env-curl.adoc[] And run loop script: include::partial$loop.adoc[] *Terminal 3* : Run commands. Describe (or `kubectl edit`) the Deployment: :describe-deployment-name: myboot :section-k8s: rolling-init include::partial$describe-deployment.adoc[] // The .no-query-replace tells the course ui to not attempt to replace tokens between % % [.no-query-replace] [.console-output] [source,bash] ---- . . . Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable StrategyType: RollingUpdate MinReadySeconds: 0 RollingUpdateStrategy: 25% max unavailable, 25% max surge . . . ---- `StrategyType` options include `RollingUpdate` and `Recreate`: Change the replicas: include::partial$tip_vscode_kube_editor.adoc[] [#edit-deployment-replicas-rolling] [.console-input] [source, bash] ---- kubectl edit deployment myboot ---- Look for "replicas": [.console-output] [source,yaml] ---- spec: progressDeadlineSeconds: 600 replicas: 1 revisionHistoryLimit: 10 selector: matchLabels: app: myboot ---- And update to "2": [.console-output] [source, yaml] ---- spec: progressDeadlineSeconds: 600 replicas: 2 revisionHistoryLimit: 10 selector: matchLabels: app: myboot ---- Save and close your editor and a new pod will come to life: [#edit-deployment-replicas-get-pod-rolling] [.console-input] [source, bash] ---- kubectl get pods ---- [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE myboot-d78fb6d58-2fqml 1/1 Running 0 25s myboot-d78fb6d58-ljkjp 1/1 Running 0 3m ---- Change the image associated with the deployment: [#edit-deployment-v2-rolling] [.console-input] [source, bash] ---- kubectl edit deployment myboot ---- Find the image attribute: [source, yaml] ---- spec: containers: - image: quay.io/rhdevelopers/myboot:v1 imagePullPolicy: IfNotPresent name: myboot ---- and change the image `myboot:v2`: [source, yaml] ---- spec: containers: - image: quay.io/rhdevelopers/myboot:v2 imagePullPolicy: IfNotPresent name: myboot ---- [#edit-deployment-v2-get-pod-rolling] [.console-input] [source, bash] ---- kubectl get pods ---- [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE myboot-7fbc4b97df-4ntmk 1/1 Running 0 9s myboot-7fbc4b97df-qtkzj 0/1 ContainerCreating 0 0s myboot-d78fb6d58-2fqml 1/1 Running 0 3m29s myboot-d78fb6d58-ljkjp 1/1 Terminating 0 8m ---- And the output from terminal 2: [.console-output] [source,bash] ---- Aloha from Spring Boot! 211 on myboot-d78fb6d58-2fqml Aloha from Spring Boot! 212 on myboot-d78fb6d58-2fqml Bonjour from Spring Boot! 0 on myboot-7fbc4b97df-4ntmk Bonjour from Spring Boot! 1 on myboot-7fbc4b97df-4ntmk ---- Check the status of the deployment: [#rollout-v2-rolling] [.console-input] [source, bash] ---- kubectl rollout status deployment myboot ---- [.console-output] [source,bash] ---- deployment "myboot" successfully rolled out ---- Notice that there is a new RS: [#rs-v2-rolling] [.console-input] [source, bash] ---- kubectl get rs ---- [.console-output] [source,bash] ---- NAME DESIRED CURRENT READY AGE myboot-7fbc4b97df 2 2 2 116s myboot-d78fb6d58 0 0 0 10m ---- Describe the Deployment: :describe-deployment-name: myboot :section-k8s: rolling include::partial$describe-deployment.adoc[] And check out the Events section: [.console-output] [source,bash] ---- ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ScalingReplicaSet 16m deployment-controller Scaled up replica set myboot-d78fb6d58 to 1 Normal ScalingReplicaSet 6m15s deployment-controller Scaled up replica set myboot-d78fb6d58 to 2 Normal ScalingReplicaSet 2m55s deployment-controller Scaled up replica set myboot-7fbc4b97df to 1 Normal ScalingReplicaSet 2m46s deployment-controller Scaled down replica set myboot-d78fb6d58 to 1 Normal ScalingReplicaSet 2m46s deployment-controller Scaled up replica set myboot-7fbc4b97df to 2 Normal ScalingReplicaSet 2m37s deployment-controller Scaled down replica set myboot-d78fb6d58 to 0 ---- You can list the revisions associated to your deployment by running the following command: [#rollout-history] [.console-input] [source, bash] ---- kubectl rollout history deployment/myboot ---- You can rollback to v1 using the following command: [#describe-rollback-rolling] [.console-input] [source, bash] ---- kubectl rollout undo deployment/myboot --to-revision=1 ---- And it rolls back to Aloha: [.console-output] [source,bash] ---- Bonjour from Spring Boot! 501 on myboot-7fbc4b97df-qtkzj Bonjour from Spring Boot! 502 on myboot-7fbc4b97df-qtkzj Aloha from Spring Boot! 0 on myboot-d78fb6d58-vnlch ---- [IMPORTANT] ==== On minikube, you may receive errors from curl during the rollover activity. [.console-output] [source,bash] ---- Aloha from Spring Boot! 119 on myboot-d78fb6d58-2zp4h curl: (7) Failed to connect to 192.168.99.100 port 31528: Connection refused ---- The reason is the the missing Live and Ready Probes Try using the Quarkus image instead of the Spring Boot one [#describe-rollback-quarkus-rolling] [.console-input] [source, bash] ---- kubectl set image deployment/myboot myboot=quay.io/rhdevelopers/quarkus-demo:v1 ---- And there should be no errors, Quarkus simply boots up crazy fast [.console-output] [source,bash] ---- Aloha from Spring Boot! 62 on myboot-d78fb6d58-smb7h Aloha from Spring Boot! 63 on myboot-d78fb6d58-smb7h Supersonic Subatomic Java with Quarkus myboot-5cf696848b-tlt6l:1 Supersonic Subatomic Java with Quarkus myboot-5cf696848b-tlt6l:2 ---- ==== ================================================ FILE: documentation/modules/ROOT/pages/secrets.adoc ================================================ = Secrets include::_attributes.adoc[] :watch-terminal: Terminal 2 Secrets are an out of the box way Kubernetes provides to store sensitive data. Most similar to config maps, these are treated with a bit of extra care under the hood in Kubernetes. Secrets are meant to give developers a way of specifying common types of sensitive data (basic-auth credentials, image registry credentials, TLS certs, etc) without including it (insecurely) in the code (application or infrastructure) of their containerized application. A typical generic secret that one will come across are the credentials for accessing a database. The heart of any secret is not displayed in plain-text by default. Instead, secret data is base64 encoded and needs to be decoded to be read. [WARNING] ==== Like most data in the Kubernetes API, secrets are stored within the `etc` distributed data store. Whilst access to this data is mediated by the cluster's RBAC, it should be noted that Secrets are NOT encrypted at rest within `etcd` in Kubernetes by default. This can be enabled on generic Kubernetes by following link:https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/[these instructions^]. OpenShift makes this even easier as documented link:https://docs.openshift.com/container-platform/4.7/security/encrypting-etcd.html[here^] ==== == Prerequisites Make sure you are in the correct namespace: :section-k8s: resource :set-namespace: myspace include::partial$namespace-setup-tip.adoc[] include::partial$set-context.adoc[] Make sure nothing is running in your namespace: [#no-resources-resource] [.console-input] [source, bash] ---- kubectl get all ---- [.console-output] [source,bash] ---- No resources found in myspace namespace. ---- Deploy `myboot` service: [#deploy-myboot-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-deployment.yml ---- Deploy myboot Service: [#service-myboot-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-service.yml ---- In a separate terminal (hereafter referred to as *{watch-terminal}*) set up a watch on the pods: :section-k8s: secrets include::partial$watching-pods-with-nodes.adoc[] Meanwhile, in the main terminal, send a request: :service-exposed: myboot include::partial$env-curl.adoc[] which should give us the by now familiar response [.console-output] [source,bash,subs="+macros,+attributes"] ---- Aloha from Spring Boot! 1 on myboot-7cbfbd9b89-dl2hv ---- == Creating Secrets Previously, we used a `ConfigMap` to hold a database connection string (`user=MyUserName;password=pass:[*****]`). Instead, let's create a secret to hold this sensitive data. The `kubectl` CLI has some support for creating generic (or `opaque`) secrets like the one we would use for a database login. [#create-secret-cli-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl create secret generic mysecret --from-literal=user='MyUserName' --from-literal=password='mypassword' ---- [#get-secret-cli-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get secrets ---- Which will now yield output similar to the following [tabs] ==== Minikube:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME TYPE DATA AGE default-token-nxkpw kubernetes.io/service-account-token 3 5d12h mysecret Opaque 2 25s ---- -- OpenShift:: + -- [.console-output] [source,bash] ---- NAME TYPE DATA AGE builder-dockercfg-96ml5 kubernetes.io/dockercfg 1 3d6h builder-token-h5g82 kubernetes.io/service-account-token 4 3d6h builder-token-vqjqz kubernetes.io/service-account-token 4 3d6h default-dockercfg-bsnjr kubernetes.io/dockercfg 1 3d6h default-token-bl77s kubernetes.io/service-account-token 4 3d6h default-token-vlzsl kubernetes.io/service-account-token 4 3d6h deployer-dockercfg-k6npn kubernetes.io/dockercfg 1 3d6h deployer-token-4hb78 kubernetes.io/service-account-token 4 3d6h deployer-token-vvh6r kubernetes.io/service-account-token 4 3d6h mysecret Opaque 2 5s ---- -- ==== Because this is a `Secret` and not a `ConfigMap`, the user & password are not immediately visible: [#describe-secret-cli-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe secret mysecret ---- [.console-output] [source,bash] ---- Name: mysecret Namespace: myspace Labels: Annotations: Type: Opaque Data ==== password: 10 bytes user: 10 bytes ---- [#get-secret-cli-yaml-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get secret mysecret -o yaml ---- [source,yaml] ---- apiVersion: v1 data: password: bXlwYXNzd29yZA== user: TXlVc2VyTmFtZQ== kind: Secret metadata: creationTimestamp: "2020-03-31T20:19:26Z" name: mysecret namespace: myspace resourceVersion: "4944690" selfLink: /api/v1/namespaces/myspace/secrets/mysecret uid: e8c5f12e-bd71-4d6b-8d8c-7af9ed6439f8 type: Opaque ---- Copy the value of the password field above into the echo command below to prove that it is base64 encoded [#get-secret-cli-password-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- echo 'bXlwYXNzd29yZA==' | base64 --decode ---- [.console-output] [source,bash] ---- mypassword ---- [TIP] ==== If pressed for time, you can run the following command instead [.console-input] [source,bash,subs="+macros,+attributes"] ---- B64_PASSWORD=$(kubectl get secret mysecret -o jsonpath='{.data.password}') echo "password:$B64_PASSWORD is decoded as $(echo $B64_PASSWORD | base64 --decode)" ---- ==== And then do the same for the username [#get-secret-cli-username-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- echo 'TXlVc2VyTmFtZQ==' | base64 --decode ---- [.console-output] [source,bash] ---- MyUserName ---- [TIP] ==== If pressed for time, you can run the following command instead [.console-input] [source,bash,subs="+macros,+attributes"] ---- B64_DATA=$(kubectl get secret mysecret -o jsonpath='{.data.user}') echo "username:$B64_DATA is decoded as $(echo $B64_DATA | base64 --decode)" ---- ==== Or get them using `kubectl`: [#get-secret-kubectl-password-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get secret mysecret -o jsonpath='{.data.password}' | base64 --decode ---- == Using Secrets :quick-open-file: myboot-deployment-configuration-secret.yml Let's take a look at a deployment, `{quick-open-file}`, that will make use of our newly created secret. include::partial$tip_vscode_quick_open.adoc[] [.console-output] [source,yaml,subs="+macros,+attributes"] .{quick-open-file} ---- include::example$myboot-deployment-configuration-secret.yml[] ---- <.> This determines where the pod will find the secret. It will be in a file in the `/mystuff/secretstuff` directory in the pod <.> This defines what `mysecretvolume` should actually mount. In this case `mysecret`, the secret we just created above. One way to allow deployments (pods) to use secrets is to provide them via Volume Mounts: [source, yaml] ---- volumeMounts: - name: mysecretvolume mountPath: /mystuff/mysecretvolume ---- Let's update our deployment to use this volume: [#replace-myboot-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl replace -f apps/kubefiles/myboot-deployment-configuration-secret.yml ---- _Once the deployment has been updated_, exec into the newly created Pod: [#print-secrets-volume-secrets] [.console-input] [source,bash,subs="+macros,+attributes"] ---- PODNAME=$(kubectl get pod -l app=myboot --field-selector pass:['status.phase!=Terminating'] -o name) kubectl exec $PODNAME -- ls -l /mystuff/secretstuff kubectl exec $PODNAME -- cat /mystuff/secretstuff/password ---- Results in: [.console-output] [source,bash] ---- total 0 lrwxrwxrwx. 1 root root 15 Jul 19 03:37 password -> ..data/password #<.> lrwxrwxrwx. 1 root root 11 Jul 19 03:37 user -> ..data/user mypassword #<.> ---- <.> Refer back to the secret definition. Each field under the `.data` section of the secret has become a file in this directory that represents the mounted secret <.> `cat` ing the value of the `password` file gives the value of the `.data.password` field in the `secret` we defined above [TIP] ==== Alternatively, you can just run the following command to rsh into the pod and poke around [.console-input] [source,bash,subs="+macros,+attributes"] ---- PODNAME=$(kubectl get pod -l app=myboot --field-selector pass:['status.phase!=Terminating'] -o name) kubectl exec -it $PODNAME -- /bin/bash ---- ==== But how would your application know to look in this directory for credentials? Whilst it could be hardcoded in the application (or via properties) you could also provide the path via `/mystuff/mysecretvolume` to the pod via an environment variable so the application knows where to look. [TIP] ==== It's also possible to expose secrets directly as environment variables, but that's beyond the scope of this tutorial. ==== For more information on secrets, see https://kubernetes.io/docs/concepts/configuration/secret/[here] == Clean Up [.console-input] [source,bash] ---- kubectl delete deployment myboot kubectl delete service myboot ---- ================================================ FILE: documentation/modules/ROOT/pages/service-magic.adoc ================================================ = Service Magic Create a Namespace: [#create-namespace] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl create namespace funstuff kubectl config set-context --current --namespace=funstuff ---- == Deploy mypython [#deploy-mypython] [.console-input] [source,bash,subs="+macros,+attributes"] ---- cat < 2m6s ---- Alternatively, you extract only service endpoints with the following command you'll get no result: [#get-endpoints1] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get endpoints my-service -o jsonpath='{.subsets[].addresses[*].ip}{"\n"}' ---- Initialize `IP` and `PORT` envirnment variables to reach the service: :section-k8s: servicemagic :service-exposed: my-service include::partial$env-curl.adoc[] And run loop script: include::partial$loop.adoc[] The client may experience either *connection refusal* or a *delay* before timing out, depending on the features of the load balancer. [.console-output] [source,bash] ---- curl: (7) Failed to connect to 35.224.233.213 port 8000: Connection refused curl: (7) Failed to connect to 35.224.233.213 port 8000: Connection refused ---- [#label-mypython] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl label pod -l app=mypython inservice=mypods ---- [.console-output] [source,bash] ---- curl: (7) Failed to connect to 35.224.233.213 port 8000: Connection refused Python Hello on mypython-deployment-6874f84d85-2kpjl Python Hello on mypython-deployment-6874f84d85-2kpjl Python Hello on mypython-deployment-6874f84d85-2kpjl ---- [#label-mynode] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl label pod -l app=mynode inservice=mypods ---- [.console-output] [source,bash] ---- Python Hello on mypython-deployment-6874f84d85-2kpjl Python Hello on mypython-deployment-6874f84d85-2kpjl Node Hello on mynode-deployment-fb5457c5-hhz7h 0 Node Hello on mynode-deployment-fb5457c5-hhz7h 1 Python Hello on mypython-deployment-6874f84d85-2kpjl Python Hello on mypython-deployment-6874f84d85-2kpjl Python Hello on mypython-deployment-6874f84d85-2kpjl ---- [#label-mygo] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl label pod -l app=mygo inservice=mypods ---- [.console-output] [source,bash] ---- Node Hello on mynode-deployment-fb5457c5-hhz7h 59 Node Hello on mynode-deployment-fb5457c5-hhz7h 60 Go Hello on mygo-deployment-6d944c5c69-kcvmk Python Hello on mypython-deployment-6874f84d85-2kpjl Python Hello on mypython-deployment-6874f84d85-2kpjl ---- [#get-endpoints2] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get endpoints my-service -o jsonpath='{.subsets[].addresses[*].ip}{"\n"}' ---- [.console-output] [source,bash] ---- 10.130.2.43 10.130.2.44 10.130.2.45 ---- See the Pod IPs: [#pod-ips] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get pods -o wide ---- Remove `mypython` Pod from the Service: [#remove-label] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl label pod -l app=mypython inservice- ---- [#get-endpoints3] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get endpoints my-service -o jsonpath='{.subsets[].addresses[*].ip}{"\n"}' ---- [.console-output] [source,bash] ---- 10.130.2.44 10.130.2.45 ---- == Clean Up [#clean-up] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete namespace funstuff ---- ================================================ FILE: documentation/modules/ROOT/pages/service.adoc ================================================ = Service NOTE: This follows the creation of the Deployment in the previous chapter Make sure you are in the correct namespace: :section-k8s: services :set-namespace: myspace include::partial$set-context.adoc[] Make sure you have Deployment: [#have-deployment-service] [.console-input] [source,bash] ---- kubectl get deployments ---- [.console-output] [source,bash] ---- NAME READY UP-TO-DATE AVAILABLE AGE quarkus-demo-deployment 3/3 3 3 8m33s ---- Make sure you have RS: [#have-rs-service] [.console-input] [source,bash] ---- kubectl get rs ---- [.console-output] [source,bash] ---- NAME DESIRED CURRENT READY AGE quarkus-demo-deployment-5979886fb7 3 3 3 8m56s ---- Make sure you have Pods: [#have-pods-service] [.console-input] [source,bash] ---- kubectl get pods ---- [.console-output] [source,bash] ---- NAME READY STATUS RESTARTS AGE quarkus-demo-deployment-5979886fb7-c888m 1/1 Running 0 9m17s quarkus-demo-deployment-5979886fb7-gdtnz 1/1 Running 0 9m17s quarkus-demo-deployment-5979886fb7-grf59 1/1 Running 0 9m17s ---- Create a Service [#create-service] [.console-input] [source,bash,subs="+macros,+attributes"] ---- cat < 80:31974/TCP 4s ---- Wait until you see an external IP assigned. NOTE: On Minikube without an Ingress controller, will not become a real external IP. https://kubernetes.io/docs/tasks/access-application-cluster/ingress-minikube/[Optional: Setup Minikube Ingress] [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp LoadBalancer 172.30.103.41 34.71.122.153 8080:31974/TCP 44s ---- :section-k8s: services :service-exposed: the-service include::partial$env-curl.adoc[] Results: [.console-output] [source,bash] ---- Supersonic Subatomic Java with Quarkus quarkus-demo-deployment-5979886fb7-grf59:1 ---- NOTE: "5979886fb7-grf59" is part of the unique id for the pod. The `.java` code uses `System.getenv().getOrDefault("HOSTNAME", "unknown");` == Ingress or Route *Kubernetes Ingress* and *OpenShift Route* are functionally similar, resources used to expose applications externally. The `Route` object was developed by Red Hat before the Kubernetes Ingress API was fully mature. It essentially bundles the traffic rule definition and its implementation (via the built-in HAProxy Router) into a single, opinionated feature. Depending on your underlying platform, continue with the relevant path: . <<#_kubernetes_ingress,Kubernetes Ingress>> . <<#_openshift_route,OpenShift Route>> === Kubernetes Ingress [#create-ingress] [.console-input] [source,bash,subs="+macros,+attributes"] ---- cat < myroutes.json ---- It's also possible to extract specific information from the JSON using JSONPath syntax. [#route-jq] [.console-input] [source, bash] ---- oc get route the-service -o jsonpath="{.spec.host}" ---- [.console-output] [source, bash] ---- the-service-myspace.apps.gcp.burrsutter.dev ---- ================================================ FILE: documentation/modules/ROOT/pages/statefulset.adoc ================================================ = StatefulSets include::_attributes.adoc[] :watch-terminal: Terminal 2 A `StatefulSet` provides a unique identity to the Pods that they manage. `StatefulSet` s are particularly useful when your application requires a unique network identifier or persistent storage across Pod (re)scheduling or when your application needs some guarantee about the ordering of deployment and scaling. One of the most typical examples of using `StatefulSet` s is when one needs to deploy primary/secondary servers (i.e database cluster) where you need to know beforehand the hostname of each of the servers to start the cluster. Also, when you scale up and down you want to do it in a specified order (i.e you want to start the primary node first and then the secondary node). [IMPORTANT] ==== `StatefulSet` requires a Kubernetes _Headless Service_ instead of a standard Kubernetes _service_ in order for it to be accessed. We will discuss this more below ==== == Preparation === Namespace Setup Make sure you are in the correct namespace: :section-k8s: stateful :set-namespace: myspace include::partial$namespace-setup-tip.adoc[] include::partial$set-context.adoc[] === Watch Terminal To be able to observe what's going on, let's open another terminal (*{watch-terminal}*) and `watch` what happens as we run our different jobs :section-k8s: stateful include::partial$watching-pods-with-nodes.adoc[] === Multi-node (minikube) If your cluster is running multiple nodes and you need the stateful service to be assigned to a specific node so that you can connect to the service externally, replace `NODE` with the name of the node you don't want to run the stateful service on [.console-input] [source,bash,subs="+macros,+attributes"] ---- NODE=devnation-02 #<.> kubectl taint node pass:[${NODE}] app=quarkus-statefulset:NoExecute ---- <.> Replace this and/or repeat for all the nodes in your cluster that you don't want the stateful set to be assigned to. See also xref:taints-affinity.adoc[Taints and Affinity section, window=_blank] == StatefulSet StatefulSet is created by using the Kubernetes `StatefulSet` resource: [source, yaml] ---- apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: quarkus-statefulset labels: app: quarkus-statefulset spec: serviceName: "quarkus" # <.> replicas: 2 selector: matchLabels: app: quarkus-statefulset template: metadata: labels: app: quarkus-statefulset spec: containers: - name: quarkus-statefulset image: quay.io/rhdevelopers/quarkus-demo:v1 ports: - containerPort: 8080 name: web ---- <.> `serviceName` is the name of the (headless) service that governs this `StatefulSet`. This service must exist before the StatefulSet, and is responsible for the network identity of the set [#hostname-formula] We can predict the hostname for any member pod of a `StatefulSet` by using the following "formula": **** `StatefulSet.name` + `-` + "ordinal index" **** The "ordinal index" is a number starting from `0` for the first pod created by the `StatefulSet` and is incremented by one for each additional replica pod. So in this instance, the we would expect the first pod of the `StatefulSet` above to have the hostname: **** `quarkus-statefulset-0` **** Finally, as mentioned above, to be able to route traffic to the pods of our StatefulSet, we also need to create a *headless service*: [source, yaml,subs="+quotes"] ---- apiVersion: v1 kind: Service metadata: name: #quarkus# #<.> labels: app: quarkus-statefulset spec: ports: - port: 8080 name: web clusterIP: None #<.> selector: app: quarkus-statefulset ---- <.> Notice that this matches the `serviceName` field of the `StatefulSet`. This must match to create the dns entry <.> Setting `clusterIP` to `None` is what makes the service "headless". Apply the following `.yaml` to the cluster to create the `StatefulSet` and the corresponding headless service we looked at above: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/quarkus-statefulset.yaml ---- You should then see the following in the watch terminal [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+quotes"] ---- NAME READY STATUS RESTARTS AGE #quarkus-statefulset-0# 1/1 Running 0 12s ---- -- ==== Notice that the Pod name is the `serviceName` with a `-0`, as it is the first (`0` th if you will) instance. This is as we explained <> Now let's take a look at the stateful set itself [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get statefulsets ---- [.console-output] [source,bash] ---- NAME READY AGE quarkus-statefulset 1/1 109s ---- As with `deployments` we can scale `statefulsets` [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl scale sts \#<.> quarkus-statefulset --replicas=3 ---- <.> `sts` is the shortname of the `statefulset` api-resource Then in the watch terminal see [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+quotes"] ---- NAME READY STATUS RESTARTS AGE quarkus-statefulset-0 1/1 Running 0 95s #quarkus-statefulset-1# 1/1 Running 0 2s #quarkus-statefulset-2# 1/1 Running 0 1s ---- -- ==== Notice that the name of the Pods continues to use <> Also, if you check the order of events in the Kubernetes cluster, you'll notice that the Pod name ending with `-1` is created *before* those with higher ordinal index (e.g. with suffix of `-2`). [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get events --sort-by=.metadata.creationTimestamp ---- [.console-output] [source,bash] ---- 4m4s Normal SuccessfulCreate statefulset/quarkus-statefulset create Pod quarkus-statefulset-1 in StatefulSet quarkus-statefulset successful 4m3s Normal Pulled pod/quarkus-statefulset-1 Container image "quay.io/rhdevelopers/quarkus-demo:v1" already present on machine 4m3s Normal Scheduled pod/quarkus-statefulset-2 Successfully assigned default/quarkus-statefulset-2 to kube 4m3s Normal Created pod/quarkus-statefulset-1 Created container quarkus-statefulset 4m3s Normal Started pod/quarkus-statefulset-1 Started container quarkus-statefulset 4m3s Normal SuccessfulCreate statefulset/quarkus-statefulset create Pod quarkus-statefulset-2 in StatefulSet quarkus-statefulset successful 4m2s Normal Pulled pod/quarkus-statefulset-2 Container image "quay.io/rhdevelopers/quarkus-demo:v1" already present on machine 4m2s Normal Created pod/quarkus-statefulset-2 Created container quarkus-statefulset 4m2s Normal Started pod/quarkus-statefulset-2 Started container quarkus-statefulset ---- === Stable Network Identities The reason we created the *headless service* previously was to ensure that the pods of our stateful set can be found _within_ the cluster (see <> for reaching services from outside the cluster). As each Pod is created, it gets a matching DNS subdomain, taking the form: `$(podname).$(governing service domain)`, where the governing service is defined by the `serviceName` field on the StatefulSetfootnote:[See also the official Kubernetes documentation link:https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#stable-network-id[here]] We can test this by creating a pod within the cluster and doing an `nslookup` from within the cluster. Run the following command to create a pod in the namespace in which we can run cluster local `nslookup` queries [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl run -it --restart=Never --rm --image busybox:1.28 dns-test ---- From within the container, run the folllowing command to see if we can find a pod of our StatefulSet [tabs] ==== Container:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- nslookup quarkus-statefulset-0.quarkus ---- This should yield the following output (though your reported IP address will vary) [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- Server: 10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local Name: quarkus-statefulset-0.quarkus Address 1: 172.17.0.3 #quarkus-statefulset-0.quarkus.myspace.svc.cluster.local# #<.> ---- <.> Notice that the full address is `$(podname).$(governing service domain).$(namespace)`.svc.cluster.local You can now exit the pod (causing it to be cleaned up) by issuing the following command: [.console-input] [source,bash,subs="+macros,+attributes"] ---- exit ---- -- ==== So with the help of a headless service we can find any pod of the StatefulSet by using its internal DNS name as formulated by the StatefulSet and the headless service. == Exposing StatefulSets Given that our stateful set needed to use a headless service, you'll notice that no external IP is assigned that we can use to access our pods from _outside_ the cluster [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe svc quarkus-statefulset ---- [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- Name: quarkus-statefulset Namespace: myspace Labels: app=quarkus-statefulset Annotations: Selector: app=quarkus-statefulset Type: ClusterIP IP Family Policy: SingleStack IP Families: IPv4 #IP: None# #IPs: None# Port: web 8080/TCP TargetPort: 8080/TCP Endpoints: 172.17.0.3:8080,172.17.0.4:8080,172.17.0.5:8080 Session Affinity: None Events: ---- Instead, only (internal) endpoints are assigned [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe endpoints quarkus-statefulset ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- Name: quarkus-statefulset Namespace: myspace Labels: app=quarkus-statefulset service.kubernetes.io/headless= Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2021-07-20T04:45:21Z Subsets: Addresses: 172.17.0.3,172.17.0.4,172.17.0.5 NotReadyAddresses: Ports: Name Port Protocol ---- ---- -------- web 8080 TCP Events: ---- This kind of makes sense since the whole point of using `StatefulSets` is so that we can reference a specific pod by a predictable name instead of having them abstracted away by a normal (non-headless) `Service`. To assist with our ability to access pods by name, kubernetes exposes a label on all `StatefulSet` pods that we can use as a selector to our service [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe pod quarkus-statefulset-2 ---- And the abbreviated output shows our label (highlighted) [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- Name: quarkus-statefulset-2 Namespace: myspace Priority: 0 Node: devnation/192.168.49.2 Start Time: Tue, 20 Jul 2021 04:45:04 +0000 Labels: app=quarkus-statefulset controller-revision-hash=quarkus-statefulset-6bf5d59699 #statefulset.kubernetes.io/pod-name=quarkus-statefulset-2# Annotations: ---- :quick-open-file: quarkus-statefulset-external-svc.yaml We can use this label as a selector for a service that targets this specific pod. Take a look at `{quick-open-file}`: include::partial$tip_vscode_quick_open.adoc[] [.console-output] [source,yaml,subs="+macros,+attributes"] .{quick-open-file} ---- include::example$quarkus-statefulset-external-svc.yaml[] ---- <.> Indicate that this service should be exposed via LoadBalancer <.> Prevent excessive hops by routing traffic directly to the node <.> A selector that leverages the label provided automatically by the Kubernetes StatefulSet functionality Having reviewed the service we can now create it: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/quarkus-statefulset-external-svc.yaml ---- Meanwhile, in the main terminal, send a request: :service-exposed: quarkus-statefulset-2 include::partial$env-curl.adoc[] You should receive the following back [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- Supersonic Subatomic Java with Quarkus quarkus-statefulset-2:1 #<.> ---- <.> Notice the hostname of `quarkus-statefulset-2`. This is part of why we used stateful sets in the first place, so that pods would get predictable hostnames == Scale Down and Cleanup Finally, if we scale down to two instances, the one that is destroyed is not randomly chosen, but the one started later (`quarkus-statefulset-2`). [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl scale sts quarkus-statefulset --replicas=2 ---- [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+quotes"] ---- NAME READY STATUS RESTARTS AGE quarkus-statefulset-0 1/1 Running 0 9m22s quarkus-statefulset-1 1/1 Running 0 7m49s #quarkus-statefulset-2 0/1 Terminating 0 7m48s# ---- -- ==== Beware when using stateful sets and services that this could break things. Remember that the service we created above referenced that exact pod in the stateful set. If you try to reach it now [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl pass:[${IP}:${PORT}] ---- You'll get an error (perhaps like this one) [.console-output] [source,bash,subs="+macros,+attributes"] ---- curl: (7) Failed to connect to 192.168.86.58 port 31834: Connection refused ---- === Clean Up You've now reached the end of this section. You can clean up all aspects of the statefulset by deleting the yaml that spawned it (as well as the external service) [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete -f apps/kubefiles/quarkus-statefulset.yaml kubectl delete -f apps/kubefiles/quarkus-statefulset-external-svc.yaml ---- ================================================ FILE: documentation/modules/ROOT/pages/taints-affinity.adoc ================================================ = Taints and Affinity include::_attributes.adoc[] :watch-terminal: Terminal 2 So far, when we deployed any Pod in the Kubernetes cluster, it was run on any node that met the requirements (ie memory requirements, CPU requirements, ...) However, in Kubernetes there are two concepts that allow you to further configure the scheduler, so that Pods are assigned to Nodes following some business criteria. == Preparation === Minikube Multinode include::https://raw.githubusercontent.com/redhat-developer-demos/rhd-tutorial-common/master/minikube-multinode.adoc[] === Watch Nodes To be able to observe what's going on, let's open another terminal (*{watch-terminal}*) and `watch` what happens to the pods as we change taints on the nodes. :section-k8s: taints include::partial$watching-pods-with-nodes.adoc[] == Taints A Taint is applied to a Kubernetes Node that signals the scheduler to avoid or not schedule certain Pods. A Toleration is applied to a Pod definition and provides an exception to the taint. Let's describe the current nodes, in this case as an OpenShift cluster is used, you can see several nodes: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe nodes | egrep "Name:|Taints:" ---- [.console-output] [source,bash] ---- Name: ip-10-0-136-107.eu-central-1.compute.internal Taints: node-role.kubernetes.io/master:NoSchedule Name: ip-10-0-140-186.eu-central-1.compute.internal Taints: Name: ip-10-0-141-128.eu-central-1.compute.internal Taints: Name: ip-10-0-146-109.eu-central-1.compute.internal Taints: Name: ip-10-0-150-226.eu-central-1.compute.internal Taints: ---- [NOTE] ==== Notice that in this case, the `master` node contains a taint which blocks your application Pods from being scheduled there. ==== Let's add a taint to all nodes: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl taint nodes --all=true color=blue:NoSchedule ---- [.console-output] [source,bash] ---- node/ip-10-0-136-107.eu-central-1.compute.internal tainted node/ip-10-0-140-186.eu-central-1.compute.internal tainted node/ip-10-0-141-128.eu-central-1.compute.internal tainted node/ip-10-0-146-109.eu-central-1.compute.internal tainted node/ip-10-0-150-226.eu-central-1.compute.internal tainted node/ip-10-0-155-122.eu-central-1.compute.internal tainted node/ip-10-0-162-206.eu-central-1.compute.internal tainted node/ip-10-0-168-102.eu-central-1.compute.internal tainted node/ip-10-0-175-64.eu-central-1.compute.internal tainted ---- The color=blue is simply a key=value pair to identify the taint and NoSchedule is the specific effect for pods that can't "tolerate" the taint. In other words, if a pod does not tolerate "color=blue" then the effect will be "NoSchedule" So let's try this out. From the main terminal, we'll deploy a new pod that doesn't have any particular tolerations: [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-deployment.yml ---- -- ==== You'll see the output in the other terminal change [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+quotes"] ---- NAME READY STATUS AGE NODE myboot-7cbfbd9b89-hqx6h 0/1 #Pending# 4m12s devnation ---- -- ==== The pod will remain in `Pending` status as it has no schedulable Node available. We can get more insight into this by entering the following [tabs] ==== Terminal 1 - Minikube:: + -- // include untagged regions and any regions tagged with minikube // See: https://docs.asciidoctor.org/asciidoc/latest/directives/include-tagged-regions/#tagging-regions include::partial$taint-remove-taint.adoc[tags=**;!*;minikube] -- Terminal 1 - OpenShift:: + -- // Include all untagged regions and any regions tagged with openshift // See: https://docs.asciidoctor.org/asciidoc/latest/directives/include-tagged-regions/#tagging-regions include::partial$taint-remove-taint.adoc[tags=**;!*;openshift] -- ==== Now in *{watch-terminal}* you should see the Pending pod scheduled to the newly untained node. [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+quotes"] ---- NAME READY STATUS AGE NODE myboot-7cbfbd9b89-hqx6h 0/1 #ContainerCreating# 20m #devnation-m02# ---- -- ==== Finally, let's take a quick look at the taint status on all the nodes. [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe nodes | egrep "Name:|Taints:" ---- [.console-output] [source,bash] ---- Name: ip-10-0-136-107.eu-central-1.compute.internal Taints: node-role.kubernetes.io/master:NoSchedule Name: ip-10-0-140-186.eu-central-1.compute.internal Taints: Name: ip-10-0-141-128.eu-central-1.compute.internal Taints: color=blue:NoSchedule Name: ip-10-0-146-109.eu-central-1.compute.internal Taints: color=blue:NoSchedule ---- -- ==== === Restore Taint Add the taint back to the node (or in this case all nodes): [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl taint nodes --all=true color=blue:NoSchedule --overwrite ---- [TIP] ==== Setting the taint on all nodes is a bit sloppy. If you'd like you can get the same effect a bit more elegantly by setting the taint only on the node from which it was removed. For example: ---- kubectl taint node ip-10-0-140-186.eu-central-1.compute.internal color=blue:NoSchedule ---- ==== Take a look and notice that the pod is still running despite the change in taint (this is due to scheduling being a one time activity in the lifecycle of a pod) [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- NAME READY STATUS AGE NODE myboot-7cbfbd9b89-bzhxw 1/1 #Running# 18m devnation-m02 ---- -- ==== === Clean Up Undeploy the myboot deployment and add again the taint to the node: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete -f apps/kubefiles/myboot-deployment.yml ---- == Tolerations Let's create a Pod but containing a toleration, so it can be scheduled to a tainted node. [source, yaml] ---- spec: tolerations: - key: "color" operator: "Equal" value: "blue" effect: "NoSchedule" containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ---- [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-toleration.yaml ---- -- ==== And then we should see before too long in our watch window our pod get scheduled and advance to the run state [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+quotes"] ---- NAME READY STATUS AGE NODE myboot-84b457458b-mbf9r 1/1 #Running# 3m18s devnation-m02 ---- -- ==== Now, although all nodes contain a taint, the Pod is scheduled and run as we defined a tolerance against color=blue taint. === Clean Up [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete -f apps/kubefiles/myboot-toleration.yaml ---- == `NoExecution` Taint So far, you've seen the `NoSchedule` taint effect which means that newly created Pods will not be scheduled there unless they have an overriding toleration. But notice that if we add this taint to a node that already has running/scheduled Pods, this taint will not terminate them. Let's change that by using `NoExecution` effect. First of all, let's remove all previous taints. [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl taint nodes --all=true color=blue:NoSchedule- ---- -- ==== Then deploy another instance of myboot (with no Tolerations): [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-deployment.yml ---- -- ==== We should see the following in the watch [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash] ---- NAME READY STATUS AGE NODE myboot-7cbfbd9b89-wpddg 1/1 Running 47s devnation-m02 ---- -- ==== Now let's taint find the node the pod is running on [tabs] ==== Terminal 1:: + -- include::partial$find_node_for_pod.adoc[] [.console-output] [source,bash] ---- "ip-10-0-146-109.eu-central-1.compute.internal" ---- -- ==== [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl taint node pass:[${NODE}] color=blue:NoExecute ---- -- ==== As soon as we do this, we should be able to watch this "rescheduling" occur in the {watch-terminal} watch [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+quotes"] ---- NAME READY STATUS AGE NODE myboot-7cbfbd9b89-5t24z 0/1 #ContainerCreating# 16s devnation myboot-7cbfbd9b89-wpddg 1/1 #Terminating# 65m devnation-m02 ---- -- ==== [NOTE] ==== If you have more nodes available then the Pod is terminated and deployed onto another node, if it is not the case, then the Pod will remain in `Pending` status. ==== === Clean Up [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete -f apps/kubefiles/myboot-deployment.yml ---- -- ==== And remove the NoExecute taint [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl taint node pass:[${NODE}] color=blue:NoExecute- ---- -- ==== == Affinity & Anti-Affinity There is another way of changing where Pods are scheduled using Node/Pod Affinity and Anti-affinity. You can create rules that not only ban where Pods can run but also to favor where they should be run. In addition to creating affinities between Pods and Nodes, you can also create affinities between Pods. You can decide that a group of Pods should be always be deployed together on the same node(s). Reasons such as significant network communication between Pods and you want to avoid external network calls or perhaps shared storage devices. === Node Affinity :quick-open-file: myboot-node-affinity.yml Let's deploy a new pod with a node affinity. Take a look at `{quick-open-file}` (relevant section shown below) include::partial$tip_vscode_quick_open.adoc[] [source, yaml] .{quick-open-file} ---- spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: #<.> nodeSelectorTerms: - matchExpressions: - key: color operator: In values: - blue #<.> containers: - name: myboot image: quay.io/rhdevelopers/myboot:v1 ---- <.> This key highlights that what's follows must be used during scheduling but not a factor once a pod is executing <.> The `matchExpressions` is saying this pod has affinity for any node with a `color` in the value set `blue` Now let's deploy this [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-node-affinity.yml ---- -- ==== And we'll see in our watch window the pod in a pending state [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- NAME READY STATUS AGE NODE myboot-546d4d9b45-7vgfc 0/1 #Pending# 6s ---- -- ==== Let's create a *label* on a node matching the affinity expression: [tabs] ==== Terminal 1 - Minikube:: + -- include::partial$affinity_label.adoc[tags=**;!*;minikube] -- Terminal 1 - OpenShift:: + -- include::partial$affinity_label.adoc[tags=**;!*;openshift] -- ==== And then in the watch window the output should change to: [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- NAME READY STATUS AGE NODE myboot-546d4d9b45-7vgfc 0/1 #ContainerCreating# 15m devnation-m02 ---- -- ==== Let's delete the label from the node that the pod is running on [tabs] ==== Terminal 1:: + -- First find the node the pod is running on include::partial$find_node_for_pod.adoc[] and then remove the color label from it [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl label nodes pass:[${NODE}] color- ---- -- ==== And notice the that watch output is *unchanged* and if running, the pod will continue to run [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash] ---- NAME READY STATUS AGE NODE myboot-546d4d9b45-7vgfc 1/1 Running 22m devnation-m02 ---- -- ==== Since we used the `requiredDuringSchedulingIgnoredDuringExecution` in the deployment spec for our pod, we got our affinity to work like taints (in the previous section) worked, namely, that the rule is set during the scheduling phase but ignore after that (i.e. once executing). Therefore the Pod is not removed in our case. This is an example of a _hard_ rule: .Hard Rule **** If the Kubernetes scheduler does not find any node with the required label then the Pod reminds in _Pending_ state. **** There is also a way to create a _soft_ rule: .Soft Rule **** The Kubernetes scheduler attempts to match the rules but if it can. However, if it can't then the Pod is scheduled to any node. **** Consider the example below: [.console-output] [source,yaml,subs="+macros,+attributes,+quotes"] ---- spec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: #<.> - weight: 1 preference: matchExpressions: - key: color operator: In values: - blue ---- <.> You can see the use of the word _preferred_ vs _required_. ==== Clean Up [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete -f apps/kubefiles/myboot-node-affinity.yml ---- === Pod Affinity/Anti-Affinity :quick-open-file: myboot-pod-affinity.yml Let's deploy a new pod with a Pod Affinity. See this relevant part of `{quick-open-file}`. include::partial$tip_vscode_quick_open.adoc[] [source, yaml] .{quick-open-file} ---- spec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - topologyKey: kubernetes.io/hostname # <1> labelSelector: matchExpressions: - key: app operator: In values: - myboot # <2> containers: ---- <1> The node label key. If two nodes are labeled with this key and have identical values, the scheduler treats both nodes as being in the same topology. In this case, `hostname` is a label that is different for each node. <2> The affinity is with Pods labeled with `app=myboot`. [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-pod-affinity.yml ---- [.console-output] [source,bash] ---- NAME READY STATUS AGE NODE myboot2-7c5f46cbc9-hwm2v 0/1 Pending 5h38m ---- -- ==== The `myboot2` Pod is pending as couldn't find any Pod matching the affinity rule. To address this, let's deploy a `myboot` application labeled with `app=myboot`. [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-deployment.yml ---- -- ==== And we'll see that both start up, and run on _the same node_ [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+quotes"] ---- NAME READY STATUS AGE NODE myboot-7cbfbd9b89-267k6 0/1 ContainerCreating 5s #devnation-m02# myboot2-7c5f46cbc9-hwm2v 0/1 ContainerCreating 5h45m #devnation-m02# ---- -- ==== [TIP] ==== What you've just seen is a _hard_ rule, you can use a "soft" rules as well in Pod Affinity. [.console-output] [source, yaml, subs="+quotes"] ---- spec: affinity: podAntiAffinity: #preferredDuringSchedulingIgnoredDuringExecution:# - weight: 1 podAffinityTerm: topologyKey: kubernetes.io/hostname labelSelector: matchExpressions: - key: app operator: In values: - myboot ---- ==== *Anti-affinity* is used to insure that two Pods do NOT run together on the same node. :quick-open-file: myboot-pod-antiaffinity.yaml Let's add another pod. Open `{quick-open-file}` and focus on the following part include::partial$tip_vscode_quick_open.adoc[] [.console-output] [source, yaml] .{quick-open-file} ---- spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - topologyKey: kubernetes.io/hostname labelSelector: matchExpressions: - key: app operator: In values: - myboot ---- This basically says that this pod should not be scheduled on any individual node (`topologyKey: kubernetes.io/hostname`) that has a pod with the `app=myboot` label. Deploy a myboot3 with the above anti-affinity rule [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-pod-antiaffinity.yaml ---- -- ==== And then notice what happens in the watch window [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- NAME READY STATUS AGE NODE myboot-7cbfbd9b89-267k6 1/1 Running 10m devnation-m02 myboot2-7c5f46cbc9-hwm2v 1/1 Running 5h56m devnation-m02 myboot3-6f95c866f6-7kvdw 0/1 ContainerCreating 6s #devnation# ---- -- ==== As you can see from the highlight, the `myboot3` Pod is deployed in a different node than the `myboot` Pod ==== Clean Up [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl delete -f apps/kubefiles/myboot-pod-affinity.yml kubectl delete -f apps/kubefiles/myboot-pod-antiaffinity.yaml kubectl delete -f apps/kubefiles/myboot-deployment.yml ---- -- ==== ================================================ FILE: documentation/modules/ROOT/pages/volumes-persistentvolumes.adoc ================================================ = Volumes & Persistent Volumes include::_attributes.adoc[] :watch-terminal: Terminal 2 :file-watch-terminal: Terminal 3 Containers are ephemeral by definition, which means that anything that it is stored at running time is lost when the container is stopped. This might cause problems with containers that need to persist their data, like database containers. A Kubernetes volume is just a directory that is accessible to the Containers in a Pod. The concept is similar to Docker volumes, but in Docker you are mapping the container to a computer host, whereas in the case of Kubernetes volumes, the medium that backs it and the contents of it are determined by the particular volume type used. Some of the volume types are: * awsElasticBlockStore * azureDisk * cephfs * nfs * local * empty dir * host path == Preparation === Namespace :section-k8s: volumes :set-namespace: myspace Make sure the proper namespace `{set-namespace}` is created and context is set to point to it. include::partial$namespace-setup-tip.adoc[] include::partial$set-context.adoc[] === Watch If it's not open already, you'll want to have a terminal open (call it *{watch-terminal}*) to watch what's going on with the pods in our current namespace :section-k8s: volumes include::partial$watching-pods-with-nodes.adoc[] == Volumes Let's start with two examples of `Volumes`. === EmptyDir An `emptyDir` volume is first created when a Pod is assigned to a node and exists as long as that Pod is running on that node. As the name says, it is initially empty. All Containers in the same Pod can read and write in the same `emptyDir` volume. When a Pod is restarted or removed, the data in the `emptyDir` is lost forever. :quick-open-file: myboot-pod-volume.yml Let's deploy a service that exposes two endpoints, one to write content to a file and another one to retrieve the content from that file. Open `{quick-open-file}` include::partial$tip_vscode_quick_open.adoc[] [source, yaml] .{quick-open-file} ---- include::example$myboot-pod-volume.yml[] ---- <.> Notice that this is a `Pod` and not a `Deployment` <.> This is where this mount point will appear in the pod. See below <.> This must match the name of a volume that we define, in this case it is defined right at the bottom of the file In `volumes` section, you are defining the volume, and in `volumeMounts` section, how the volume is mounted inside the container. [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-pod-volume.yml ---- Then in our watch window we should see something like [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME READY STATUS AGE NODE myboot-demo 0/1 ContainerCreating 9s devnation ---- -- ==== Once the pod is running, let's exec into the container: [.console-input] [source,bash] ---- kubectl exec -ti myboot-demo -- /bin/bash ---- And once `exec` 'd into the container, run the following commands: [tabs] ==== Container:: + -- [.console-input] [source,bash] ---- curl localhost:8080/appendgreetingfile curl localhost:8080/readgreetingfile ---- Which should return [.console-output] [source,bash,subs="+macros,+attributes"] ---- Jambo ---- In this case, the `emptyDir` was set to `/tmp/demo` so you can check the directory content by running `ls`: [.console-input] [source,bash] ---- ls /tmp/demo ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- greeting.txt ---- -- ==== ==== EmptyDir Ephemerality If you haven't already, close the container's shell: [tabs] ==== Container:: + -- [.console-input] [source,bash] ---- exit ---- -- ==== And delete the pod: [.console-input] [source,bash] ---- kubectl delete pod myboot-demo ---- [IMPORTANT] ==== You need to wait until the pod is completely deleted before trying to deploy it again ==== Then if you deploy the same service again: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl apply -f apps/kubefiles/myboot-pod-volume.yml ---- And once in the `Running` state `exec` into the pod: [.console-input] [source,bash] ---- kubectl exec -ti myboot-demo -- /bin/bash ---- [tabs] ==== Container:: + -- Let's list the contents of our mount point in our new pod [.console-input] [source,bash] ---- ls /tmp/demo ---- You'll notice that the directory content is empty, meaning that the file we created with the last pod was destroyed when the pod was deleted [.console-output] [source,bash] ---- root@myboot-demo:/app# ---- Exit the pod [.console-input] [source,bash] ---- exit ---- -- ==== Now delete the pod. [.console-input] [source,bash] ---- kubectl delete pod myboot-demo ---- ==== EmptyDir Sharing in Pod `emptyDir` is shared between containers of the same Pod. Let's take a look at a deployment that creates two containers in the same pod that mount the same `emptyDir` volume. :quick-open-file: myboot-pods-volume.yml include::partial$tip_vscode_quick_open.adoc[] Consider `{quick-open-file}`: [.console-output] [source,yaml] .{quick-open-file} ---- include::example$myboot-pods-volume.yml[] ---- <.> The first container in the pod is called myboot-demo-1 and mounts `demo-volume` at `/tmp/demo` <.> The second container in the pod is called `myboot-demo-2` and mounts `demo-volume` at the same `/tmp/demo` point <.> Both containers use the same exact image <.> Notice that the second container needs to listen on a different port from the first since the containers share ports on the pod. The `env` directive at this level only applies to the `myboot-demo-2` container <.> The volume is defined only once but referenced by each container in the pod Now let's create that deployment in the `{set-namespace}` namespace [.console-input] [source,bash] ---- kubectl apply -f apps/kubefiles/myboot-pods-volume.yml ---- And in our pod watch we should see [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes,+quotes"] ---- NAME READY STATUS RESTARTS AGE myboot-demo #2/2# Running 0 4s ---- Notice the `2/2` ready status. This represents the 2 containers in the pod definition -- ==== First, let's exec into the *second* container in the pod and start a watch on the mount point. For this we'll open yet another terminal (*{file-watch-terminal}*) `exec` into the other container in the pod to run the `cat` command [tabs] ==== {file-watch-terminal}:: + -- include::partial$open-terminal-in-editor-inset.adoc[] [.console-input] [source,bash] ---- kubectl exec -it myboot-demo -c myboot-demo-2 -- bash ---- And then from inside the `myboot-demo-2` container in the pod, run the following command: [.console-input] [source,bash] ---- watch -n1 -- "ls -l /tmp/demo && eval ""cat /tmp/demo/greeting.txt""" ---- Which will at first return [.console-output] [source,bash,subs="+macros,+attributes"] ---- total 0 cat: /tmp/demo/greeting.txt: No such file or directory ---- -- ==== Let's access into the *first* container in the main terminal and see if we can get it to create a file that the *second* container can see [tabs] ==== Terminal 1:: + -- [.console-input] [source,bash] ---- kubectl exec -ti myboot-demo -c myboot-demo-1 -- /bin/bash ---- and generate some content to `/tmp/demo` directory. [.console-input] [source,bash] ---- curl localhost:8080/appendgreetingfile ---- And then show that the file exists and what its content is: [.console-input] [source,bash] ---- ls -l /tmp/demo && echo $(cat /tmp/demo/greeting.txt) ---- [.console-output] [source,bash] ---- total 4 -rw-r--r--. 1 root root 5 Jul 13 08:11 greeting.txt Jambo ---- -- ==== Meanwhile in *{file-watch-terminal}* you should see something like: [tabs] ==== {file-watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- total 4 -rw-r--r--. 1 root root 5 Jul 13 08:11 greeting.txt Jambo ---- Hit kbd:[CTRL+c] to exit the watch and then exit out of the `exec` to the pod [.console-input] [source,bash] ---- exit ---- Now, back in your terminal you can get the volume information from a Pod by running: [.console-input] [source,bash] ---- kubectl describe pod myboot-demo ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- Volumes: demo-volume: Type: EmptyDir (a temporary directory that shares a pods lifetime) Medium: SizeLimit: ---- -- ==== ==== Clean Up include::partial$terminal-cleanup.adoc[tags=**;!*;term3;term-exec] === HostPath :quick-open-file: myboot-pod-volume-hostpath.yml A `hostPath` volume mounts a file or directory from the node's filesystem into the Pod. Take a look at `{quick-open-file}` include::partial$tip_vscode_quick_open.adoc[] [source, yaml] .{quick-open-file} ---- include::example$myboot-pod-volume-hostpath.yaml[] ---- <.> We're mounting the same location as before, but you can see that we define the volume as `hostPath` here instead of `emptyDir` <.> `/mnt/data` is a location on the kubernetes `node` to which this pod gets assigned In this case, you are defining the host/node directory where the contents are going to be stored. [.console-input] [source,bash] ---- kubectl apply -f apps/kubefiles/myboot-pod-volume-hostpath.yaml ---- Now, if you describe the Pod, in volumes section, you'll see: [.console-input] [source,bash] ---- kubectl describe pod myboot-demo ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- Volumes: demo-volume: Type: HostPath (bare host directory volume) Path: /mnt/data HostPathType: ---- [tabs] ==== {file-watch-terminal}:: + -- Let's open a terminal where we can watch the directory on the 'host' or the 'node' include::partial$open-terminal-in-editor-inset.adoc[] :mount-dir: /mnt/data include::partial$watch-node-directory.adoc[] -- ==== [tabs] ==== Terminal 1:: + -- include::partial$create-greeting-file.adoc[] -- ==== Meanwhile in the other terminal (*{file-watch-terminal}*) you should at the same time see the watch output change [tabs] ==== {file-watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- Every 1.0s: eval ls -al /mnt/da... devnation: Tue Jul 13 09:14:28 2021 total 4 drwxr-xr-x. 1 root root 24 Jul 13 09:13 . drwxr-xr-x. 1 root root 8 Jul 13 08:24 .. -rw-r--r--. 1 root root 5 Jul 13 09:13 greeting.txt Jambo ---- -- ==== Notice that now the content stored in `/tmp/demo` inside the Pod is stored at host path `/mnt/data`, so if the Pod dies, the content is not lost. But this might not solve all the problems as if the Pod goes down and it is rescheduled in another node, then the data will not be in this other node. Let's see another example, in this case for an Amazon EBS Volume: [source, yaml] ---- apiVersion: v1 kind: Pod metadata: name: test-ebs spec: ... volumes: - name: test-volume awsElasticBlockStore: volumeID: fsType: ext4 ---- What we want you to notice from the previous snippet is that you are mixing things from your application (ie the container, probes, ports, ...) things that are more in the _dev_ side with things more related to the cloud (ie physical storage), which falls more in the _ops_ side. To avoid this mix of concepts, Kubernetes offers some layer of abstractions, so developers just ask for space to store data (_persistent volume claim_), and the operations team offers the physical storage configuration. ==== Clean Up include::partial$terminal-cleanup.adoc[tags=**;!*;term-exec] == Persistent Volume & Persistent Volume Claim A `PersistentVolume` (_PV_) is a Kubernetes resource that is created by an administrator or dynamically using `Storage Classes` independently from the Pod. It captures the details of the implementation of the storage and can be NFS, Ceph, iSCSI, or a cloud-provider-specific storage system. A `PersistentVolumeClaim` (_PVC_) is a request for storage by a user. It can request for a specific volume size or, for example, the access mode. === Persistent volume/claim with hostPath :quick-open-file: demo-persistent-volume-hostpath.yaml Let's use `hostPath` strategy, but not configuring it directly as volume, but using persistent volume and persistent volume claim. Check out `{quick-open-file}`: include::partial$tip_vscode_quick_open.adoc[] [source, yaml] .{quick-open-file} ---- kind: PersistentVolume apiVersion: v1 metadata: name: my-persistent-volume labels: type: local spec: storageClassName: pv-demo capacity: storage: 100Mi accessModes: - ReadWriteOnce hostPath: path: "/mnt/persistent-volume" ---- Now, the `volume` information is not in the pod anymore but in the _persistent volume_ object. [.console-input] [source,bash] ---- kubectl apply -f apps/kubefiles/demo-persistent-volume-hostpath.yaml kubectl get pv -w ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE my-persistent-volume 100Mi RWO Retain Available pv-demo 5s ---- :mount-dir: /mnt/persistent-volume Once the volume is established, let's update our file watch terminal to look in the volume's new location: `{mount-dir}` [tabs] ==== {file-watch-terminal}:: + -- Hit kbd:[CTRL+c] to exit out of the current watch Then start a new watch include::partial$file-watch-command.adoc[] -- ==== :quick-open-file: myboot-persistent-volume-claim.yaml Then from the dev side, we need to claim what we need from the _PV_. In the following example, we are requesting for *10Mi* space. See `{quick-open-file}`: include::partial$tip_vscode_quick_open.adoc[] [source, yaml] .{quick-open-file} ---- kind: PersistentVolumeClaim apiVersion: v1 metadata: name: myboot-volumeclaim spec: storageClassName: pv-demo accessModes: - ReadWriteOnce resources: requests: storage: 10Mi ---- [.console-input] [source,bash] ---- kubectl apply -f apps/kubefiles/myboot-persistent-volume-claim.yaml kubectl get pvc -w ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE myboot-volumeclaim Bound my-persistent-volume 100Mi RWO pv-demo 3s ---- :quick-open-file: myboot-pod-volume-pvc.yaml The big difference is that now in the pod you are just defining in the `volumes` section, not the volume configuration directly, but the _persistent volume claim_ to use. See `{quick-open-file}`: include::partial$tip_vscode_quick_open.adoc[] [source, yaml] .{quick-open-file} ---- apiVersion: v1 kind: Pod metadata: name: myboot-demo spec: containers: - name: myboot-demo image: quay.io/rhdevelopers/myboot:v4 volumeMounts: - mountPath: /tmp/demo name: demo-volume volumes: - name: demo-volume persistentVolumeClaim: claimName: myboot-volumeclaim ---- [.console-input] [source,bash] ---- kubectl apply -f apps/kubefiles/myboot-pod-volume-pvc.yaml kubectl describe pod myboot-demo ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- Volumes: demo-volume: Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace) ClaimName: myboot-volumeclaim ReadOnly: false ---- Notice that now the description of the pod shows that the volume is not set directly but through a persistent volume claim. include::partial$create-greeting-file.adoc[] And as soon as we've done that we'll expect to see the following on the path on the node that the `PersistentVolume` maps to: [tabs] ==== {file-watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- Every 1.0s: ls -al {mount-dir} && eval c... devnation: Mon Jul 19 14:07:53 2021 total 4 drwxr-xr-x. 1 root root 24 Jul 19 14:06 . drwxr-xr-x. 1 root root 42 Jul 13 09:21 .. -rw-r--r--. 1 root root 5 Jul 19 14:06 greeting.txt Jambo ---- -- ==== ==== Clean Up include::partial$terminal-cleanup.adoc[tags=**;!*;term-exec;term3-ssh] Once all is cleaned, run the following: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get pvc ---- Results in: [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE myboot-volumeclaim Bound my-persistent-volume 100Mi RWO pv-demo 14m ---- Even though the pod has been deleted, the PVC (and the PV) are still there and need to be deleted manually. [.console-input] [source,bash] ---- kubectl delete -f apps/kubefiles/myboot-persistent-volume-claim.yaml kubectl delete -f apps/kubefiles/demo-persistent-volume-hostpath.yaml ---- == Static vs Dynamic Provisioning Persistent Volumes can be provisioned dynamically or statically. Static provisioning allows cluster administrators to make *existing* storage device available to a cluster. When it is done in this way, the PV and the PVC must be provided manually. So far, in the last example, you've seen static provisioning. The dynamic provisioning eliminates the need for cluster administrators to pre-provision storage. Instead, it automatically provisions storage when it is requested by users. To make it run you need to provide a Storage Class object and a PVC referring to it. After the PVC is created, the storage device and the PV are automatically created for you. The main purpose of dynamic provisioning is to work with cloud provider solutions. Normally, the Kubernetes implementation offers a default Storage Class so anyone can get started quickly with dynamic provisioning. You can get information from the default Storage Class by running: [.console-input] [source,bash] ---- kubectl get sc ---- [tabs] ==== Minikube:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE standard (default) k8s.io/minikube-hostpath Delete Immediate false 47d ---- -- OpenShift:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME PROVISIONER AGE gp2 (default) kubernetes.io/aws-ebs 31h ---- By default, when OpenShift is installed in a cloud provider, it automatically creates a Storage Class with the underlying persistent technology of the cloud. For example in the case of AWS, a default Storage Class is provided pointing out to AWS EBS. -- ==== Then you can create a Persistent Volume Claim which will create a Persistent Volume automatically. Use kbd:[CTRL+p] to open `demo-dynamic-persistent.yaml` quickly: [source, yaml] ---- kind: PersistentVolumeClaim apiVersion: v1 metadata: name: myboot-volumeclaim spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Mi ---- Since we've not specified any _storage class_ but there is one defined as the default, the _PVC_ implicitly refers to that one. (You might consider comparing this pod definition to `myboot-persistent-volume-claim.yaml`) .Difference between static and dynamic PVC (with static PV) image::pv-static-vs-dynamic.png[] [.console-input] [source,bash] ---- kubectl apply -f apps/kubefiles/demo-dynamic-persistent.yaml kubectl get pvc ---- [tabs] ==== Minikube:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE myboot-volumeclaim Pending standard 2s ---- -- OpenShift:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE myboot-volumeclaim Pending gp2 46sç ---- -- ==== Notice that the _PVC_ is in _Pending_ STATUS, because remember that we are creating dynamic storage and it means that while the _pod_ doesn't request the volume, the _PVC_ will remain in a pending state and the _PV_ will not be created. [.console-input] [source,bash] ---- kubectl apply -f apps/kubefiles/myboot-pod-volume-pvc.yaml ---- [tabs] ==== {watch-terminal}:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME READY STATUS RESTARTS AGE myboot-demo 1/1 Running 0 2m36s ---- -- ==== When the pod is in _Running_ status, then you can get _PVC_ and _PV_ parameters. [.console-input] [source,bash] ---- kubectl get pvc ---- [tabs] ==== Minikube:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE myboot-volumeclaim Bound pvc-170f2e9a-4afc-4869-bd19-f10c86bff34b 10Mi RWO standard 5s ---- -- OpenShift:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE myboot-volumeclaim Bound pvc-6de4f27e-bd40-4b58-bb46-91eb08ca5bd7 1Gi RWO gp2 116s ---- -- ==== Notice that now the volume claim is _Bound_ to a volume. Finally, you can check that the _PV_ has been created automatically: [.console-input] [source,bash] ---- kubectl get pv ---- [tabs] ==== Minikube:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-170f2e9a-4afc-4869-bd19-f10c86bff34b 10Mi RWO Delete Bound myspace/myboot-volumeclaim standard 56s ---- -- OpenShift:: + -- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-6de4f27e-bd40-4b58-bb46-91eb08ca5bd7 1Gi RWO Delete Bound default/myboot-volumeclaim gp2 77s ---- -- ==== Notice that the _CLAIM_ field points to the _PVC_ responsible for the creation of the _PV_. === Clean Up [.console-input] [source,bash] ---- kubectl delete -f apps/kubefiles/myboot-pod-volume-pvc.yaml kubectl delete -f apps/kubefiles/demo-dynamic-persistent.yaml ---- == Distributed Filesystems It is important to notice that cloud-providers offer distributed storages so data is always available in all the nodes. As you've seen in the last example, this storage class guarantees that all nodes see the same disk content. For example, if you are using Kubernetes/OpenShift on-prem or if you don't want to relay to a vendor solution, there is also support for distributed filesystems in Kubernetes. If that's the case, we recommend you use NFS, https://www.gluster.org/[GlusterFS ] or https://ceph.io/[Ceph]. ================================================ FILE: documentation/modules/ROOT/partials/create-greeting-file.adoc ================================================ [.console-input] [source,bash] ---- kubectl exec -ti myboot-demo -- /bin/bash ---- and then from within the pod, generate some content to `/tmp/demo` directory. [.console-input] [source,bash] ---- curl localhost:8080/appendgreetingfile ---- ================================================ FILE: documentation/modules/ROOT/partials/describe-deployment.adoc ================================================ [#{section-k8s}-kubectl-describe-deployment] [.console-input] [source, bash,subs="+macros,+attributes"] ---- kubectl describe deployment {describe-deployment-name} ---- ================================================ FILE: documentation/modules/ROOT/partials/describe.adoc ================================================ [#{section-k8s}-kubectl-describe-services] [.console-input] [source,bash,subs="+macros,+attributes"] ---- PODNAME=$(kubectl get pod -l {label-describe} --field-selector pass:['status.phase!=Terminating'] -o name) kubectl describe $PODNAME ---- ================================================ FILE: documentation/modules/ROOT/partials/env-curl.adoc ================================================ [tabs] ==== Minikube:: + -- :tmp-service-exposed: {service-exposed} [.console-input] [source,bash,subs="+macros,+attributes"] ---- IP=$(minikube ip -p devnation) PORT=$(kubectl get service/{tmp-service-exposed} -o jsonpath="{.spec.ports[*].nodePort}") ---- -- Hosted:: + -- If using a hosted Kubernetes cluster like OpenShift then use curl and the EXTERNAL-IP address with port `8080` or get it using `kubectl`: :tmp-service-exposed: {service-exposed} [.console-input] [source,bash,subs="+macros,+attributes"] ---- IP=$(kubectl get service {tmp-service-exposed} -o jsonpath="{.status.loadBalancer.ingress[0].ip}") PORT=$(kubectl get service {tmp-service-exposed} -o jsonpath="{.spec.ports[*].port}") ---- IMPORTANT: If you are in AWS, you need to get the `hostname` instead of `ip.` [.console-input] [source,bash,subs="+macros,+attributes"] ---- IP=$(kubectl get service {tmp-service-exposed} -o jsonpath="{.status.loadBalancer.ingress[0].hostname}") ---- -- ==== Curl the Service: [.console-input] [source,bash,subs="+macros,+attributes"] ---- curl $IP:$PORT ---- ================================================ FILE: documentation/modules/ROOT/partials/file-watch-command.adoc ================================================ [.console-input] [source,bash,subs="+macros,+attributes"] ---- watch -n1 -- "ls -al {mount-dir} && eval ""cat {mount-dir}/greeting.txt""" ---- ================================================ FILE: documentation/modules/ROOT/partials/loop.adoc ================================================ [#{section-k8s}-curl-loop] [.console-input] [source,bash,subs="+macros,+attributes"] ---- while true do curl $IP:$PORT sleep {curl-loop-sleep-time} done ---- ================================================ FILE: documentation/modules/ROOT/partials/namespace-setup-tip.adoc ================================================ [TIP] ==== You will need to create the `{set-namespace}` if you haven't already. Check for the existence of the namespace with [.console-input] [source, bash, subs="+attributes"] ---- kubectl get ns {set-namespace} ---- If the response is: [.console-output] [source,bash, subs="+attributes"] ---- Error from server (NotFound): namespaces "{set-namespace}" not found ---- Then you can create the namespace with: [.console-input] [source, bash, subs="+attributes"] ---- kubectl create ns {set-namespace} ---- ==== ================================================ FILE: documentation/modules/ROOT/partials/open-terminal-in-editor-inset.adoc ================================================ .VSCode: Open Terminal in Editor **** If you're doing this tutorial from within VSCode you may be running out of space to put your terminals at this point! If you have a recent release of VSCode, you might consider opening a new terminal in the editor pane by using kbd:[CTRL+SHIFT+p] (or kbd:[CMD+SHIFT+p] on Mac OSX) to run the `Terminal: Create Terminal in Editor Area` command **** ================================================ FILE: documentation/modules/ROOT/partials/optional-requisites.adoc ================================================ The following CLI tools are optional for running the exercises in this tutorial. Although they are used in the tutorial, you could use others without any problem. [cols="4*^,4*.",options="header,+attributes"] |=== |**Tool**|**macOS**|**Fedora**|**windows** | https://github.com/mikefarah/yq[yq v2.4.1] | https://github.com/mikefarah/yq/releases/download/2.4.1/yq_darwin_amd64[Download] | https://github.com/mikefarah/yq/releases/download/2.4.1/yq_linux_amd64[Download] | https://github.com/mikefarah/yq/releases/download/2.4.1/yq_windows_amd64.exe[Download] | https://github.com/stedolan/jq[jq v1.6.0] | https://github.com/stedolan/jq/releases/download/jq-1.6/jq-osx-amd64[Download] | https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64[Download] | https://github.com/stedolan/jq/releases/download/jq-1.6/jq-win64.exe[Download] | https://httpie.org/[httpie] | `brew install httpie` | `dnf install httpie` | https://httpie.org/doc#windows-etc | watch | `brew install watch` | `dnf install procps-ng` | | kubectx and kubens | `brew install kubectx` | https://github.com/ahmetb/kubectx | | https://github.com/rakyll/hey[hey] | `brew install hey` | https://storage.googleapis.com/jblabs/dist/hey_linux_v0.1.2[Download] | https://hey-release.s3.us-east-2.amazonaws.com/hey_windows_amd64[Download] | https://github.com/wercker/stern[stern] | `brew install stern` | https://github.com/stern/stern/releases/download/v{stern-version}/stern_{stern-version}_linux_amd64.tar.gz[Download] | https://github.com/stern/stern/releases/download/v{stern-version}/stern_{stern-version}_windows_amd64.tar.gz[Download] ================================================ FILE: documentation/modules/ROOT/partials/prerequisites-kubernetes.adoc ================================================ :kubernetes-version: v1.34.0 :minikube-version: v1.37.0 :maven-version: 3.9.9 :java-version: 21 :stern-version: 1.33.0 The following CLI tools are required for running the exercises in this tutorial. Please have them installed and configured before you get started with any of the tutorial chapters. [cols="4*^,4*.",options="header,+attributes"] |=== |**Tool**|**macOS**|**Fedora**|**windows** | `Git` | https://git-scm.com/download/mac[Download] | https://git-scm.com/download/linux[Download] | https://git-scm.com/download/win[Download] | `Docker` | https://docs.docker.com/docker-for-mac/install[Docker for Mac] | `dnf install docker` | https://docs.docker.com/docker-for-windows/install[Docker for Windows] | `VirtualBox` | https://download.virtualbox.org/virtualbox/7.2.2/VirtualBox-7.2.2-170484-OSX.dmg[Download] | No need for VirtualBox on Linux since you can rely on embedded kernel virtualization | https://download.virtualbox.org/virtualbox/7.2.2/VirtualBox-7.2.2-170484-Win.exe[Download] | `https://kubernetes.io/docs/tasks/tools/install-minikube[Minikube] {minikube-version}` | https://github.com/kubernetes/minikube/releases/download/{minikube-version}/minikube-darwin-amd64[Download] | https://github.com/kubernetes/minikube/releases/download/{minikube-version}/minikube-linux-amd64[Download] | https://github.com/kubernetes/minikube/releases/download/{minikube-version}/minikube-windows-amd64.exe[Download] | `kubectl {kubernetes-version}` | https://storage.googleapis.com/kubernetes-release/release/{kubernetes-version}/bin/darwin/amd64/kubectl[Download] | https://storage.googleapis.com/kubernetes-release/release/{kubernetes-version}/bin/linux/amd64/kubectl[Download] | https://storage.googleapis.com/kubernetes-release/release/{kubernetes-version}/bin/windows/amd64/kubectl.exe[Download] | `Apache Maven {maven-version}` | https://archive.apache.org/dist/maven/maven-3/{maven-version}/binaries/apache-maven-{maven-version}-bin.tar.gz[Download] | https://archive.apache.org/dist/maven/maven-3/{maven-version}/binaries/apache-maven-{maven-version}-bin.tar.gz[Download] | https://archive.apache.org/dist/maven/maven-3/{maven-version}/binaries/apache-maven-{maven-version}-bin.tar.gz[Download] | `Java {java-version}` | https://adoptium.net/installation/ | https://adoptium.net/installation/ alternatively: `dnf install java-{java-version}-openjdk-devel` | https://adoptium.net/installation/ (Make sure you set the `JAVA_HOME` environment variable and add `%JAVA_HOME%\bin` to your `PATH`) ================================================ FILE: documentation/modules/ROOT/partials/set-context.adoc ================================================ [#{section-k8s}-change-context-resource] [.console-input] [source, bash, subs="+macros,+attributes"] ---- kubectl config set-context --current --namespace={set-namespace} ---- ================================================ FILE: documentation/modules/ROOT/partials/stern-watch.adoc ================================================ [#{section-k8s}-kubectl-watch-logs] // FIXME: the attributes inside the code block in the tab don't get filled in // if they are not first used outside the tab block We are going to have stern watch the {stern-namespace} namespace for {stern-pattern} [tabs] ==== {log-terminal} :: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- stern -n {stern-namespace} {stern-pattern} ---- -- ==== ================================================ FILE: documentation/modules/ROOT/partials/taint-remove-taint.adoc ================================================ // tag::openshift[] :chosen-node: ip-10-0-140-186.eu-central-1.compute.internal // end::openshift[] // tag::minikube[] :chosen-node: devnation-m02 // end::minikube[] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl describe pod #<.> ---- <.> There is only one pod in this case. If we wanted to be specific, we could add the name of the pod (e.g. `myboot-7f889dd6d-n5z55`) // tag::openshift[] [.console-output] [source,bash,subs="+quotes"] ---- Name: myboot-7f889dd6d-n5z55 Namespace: kubetut Priority: 0 Node: Labels: app=myboot pod-template-hash=7f889dd6d Annotations: openshift.io/scc: restricted Status: Pending Node-Selectors: Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s node.kubernetes.io/unreachable:NoExecute for 300s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling default-scheduler #0/9 nodes are available: 9 node(s) had taints that the pod didn't tolerate.# Warning FailedScheduling default-scheduler #0/9 nodes are available: 9 node(s) had taints that the pod didn't tolerate.# ---- // end::openshift[] // tag::minikube[] [.console-output] [source,bash,subs="+quotes"] ---- Name: myboot-7cbfbd9b89-bzhxw Namespace: myspace Priority: 0 Node: Labels: app=myboot pod-template-hash=7cbfbd9b89 Annotations: Status: Pending ... Node-Selectors: Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s node.kubernetes.io/unreachable:NoExecute op=Exists for 300s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 13s (x2 over 14s) default-scheduler #0/2 nodes are available: 2 node(s) had taint {color: blue}, that the pod didn't tolerate.# ---- // end::minikube[] Let's get the list of nodes in our cluster [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get nodes ---- [.console-output] [source,bash] ---- # tag::openshift[] NAME STATUS ROLES AGE VERSION ip-10-0-136-107.eu-central-1.compute.internal Ready master 20h v1.16.2 ip-10-0-140-186.eu-central-1.compute.internal Ready worker 20h v1.16.2 ip-10-0-141-128.eu-central-1.compute.internal Ready worker 18h v1.16.2 ip-10-0-146-109.eu-central-1.compute.internal Ready worker 18h v1.16.2 ip-10-0-150-226.eu-central-1.compute.internal Ready worker 20h v1.16.2 ip-10-0-155-122.eu-central-1.compute.internal Ready master 20h v1.16.2 ip-10-0-162-206.eu-central-1.compute.internal Ready worker 20h v1.16.2 ip-10-0-168-102.eu-central-1.compute.internal Ready master 20h v1.16.2 ip-10-0-175-64.eu-central-1.compute.internal Ready worker 18h v1.16.2 # end::openshift[] # tag::minikube[] NAME STATUS ROLES AGE VERSION devnation Ready control-plane,master 2d22h v1.21.2 devnation-m02 Ready 40h v1.21.2 # end::minikube[] ---- And pick one node that we will *remove* the taint from: [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl taint node {chosen-node} color:NoSchedule- #<.> ---- <.> adding the `-` here means to remove the taint in question (the `color` with the action `NoSchedule`) [.console-output] [source,bash,subs="+attributes"] ---- node/{chosen-node} untainted ---- ================================================ FILE: documentation/modules/ROOT/partials/terminal-cleanup.adoc ================================================ [tabs] ==== Terminal 1:: + -- // tag::term-exec[] Exit the `exec` command [.console-input] [source,bash] ---- exit ---- // end::term-exec[] Now delete the pod [.console-input] [source,bash] ---- kubectl delete pod myboot-demo ---- -- // tag::term2[] Terminal 2:: + -- [.console-input] [source,bash] ---- exit ---- This should close out the terminal -- // end::term2[] // tag::term3[] Terminal 3:: + -- Close out the terminal window by typing the following in it [.console-input] [source,bash] ---- exit ---- -- // end::term3[] // tag::term3-ssh[] Terminal 3:: + -- Hit kbd:[CTRL+c] to exit out of the `watch` And then in the `ssh` shell type [.console-input] [source,bash,subs="+macros,+attributes"] ---- exit ---- -- // end::term3-ssh[] ==== ================================================ FILE: documentation/modules/ROOT/partials/tip_vscode_kube_editor.adoc ================================================ [TIP] ==== If you're running this tutorial from within VSCode or would like to use VSCode to edit the resource targeted, make sure you set the following environment variable before issuing the `kubectl edit`: [.console-input] [source,bash,subs="+macros,+attributes"] ---- export KUBE_EDITOR="code -w" ---- ==== ================================================ FILE: documentation/modules/ROOT/partials/tip_vscode_quick_open.adoc ================================================ [TIP] ==== If you're running this from within VSCode you can use kbd:[CTRL+p] (or kbd:[CMD+p] on Mac OSX) to quickly open `{quick-open-file}` ==== ================================================ FILE: documentation/modules/ROOT/partials/watch-node-directory.adoc ================================================ Let's use the `minikube ssh` command to simulate a connection to the kubernetes node. (There is only one node running in minikube) [.console-input] [source,bash] ---- minikube ssh ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- Last login: Tue Jul 13 08:26:18 2021 from 192.168.49.1 docker@devnation:~$ ---- Now that we're there, let's watch the `{mount-dir}` directory that the pod has mounted as `/tmp/demo` include::partial$file-watch-command.adoc[] ================================================ FILE: documentation/modules/ROOT/partials/watching-pods-with-nodes.adoc ================================================ [#{section-k8s}-kubectl-watch-pods] [tabs] ==== {watch-terminal}:: + -- [.console-input] [source,bash,subs="+macros,+attributes"] ---- watch -n 1 "kubectl get pods -o wide \#<.> | awk '{print \$1 \" \" \$2 \" \" \$3 \" \" \$5 \" \" \$7}' | column -t" #<.> ---- <.> the `-o wide` option allows us to see the node that the pod is schedule to <.> to keep the line from getting too long we'll use `awk` and `column` to get and format only the columns we want -- ==== ================================================ FILE: documentation/modules/ROOT/partials/watching-pods.adoc ================================================ [#{section-k8s}-kubectl-watch-pods] [.console-input] [source,bash,subs="+macros,+attributes"] ---- watch -n 1 -- kubectl get pods ---- ================================================ FILE: documentation/modules/ROOT/partials/watching-services.adoc ================================================ [#{section-k8s}-kubectl-watch-services] [.console-input] [source,bash,subs="+macros,+attributes"] ---- kubectl get services -w ---- [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp LoadBalancer 172.30.103.41 8080:31974/TCP 4s ---- Wait until you see an external IP assigned. NOTE: On Minikube without an Ingress controller, will not become a real external IP. https://kubernetes.io/docs/tasks/access-application-cluster/ingress-minikube/[Optional: Setup Minikube Ingress] [.console-output] [source,bash,subs="+macros,+attributes"] ---- NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp LoadBalancer 172.30.103.41 34.71.122.153 8080:31974/TCP 44s ---- ================================================ FILE: documentation/modules/_attributes.adoc ================================================ ================================================ FILE: github-pages-stage.yml ================================================ runtime: cache_dir: ./.cache/antora site: title: Kubernetes Tutorial (Proposed Changes) url: https://hatmarch.github.io/kubernetes-tutorial/ start_page: kubernetes-tutorial::index.adoc content: sources: - url: . branches: [v1.29,v1.34] start_path: documentation asciidoc: attributes: release-version: v1.34 # This attribute enables creating footer based navigation to move to the next or prev page as defined in nav.adoc page-pagination: true # Set this to true to hide the versions component that appears at the bottom of the side nav page-hide-versions-component: true # Set myrepo to the organization in the image registry you want to reference in the tutorial myrepo: your-repo # Set the docker-host attribute to the hostname that should be used to refer to target of curl commands in a container # for example: docker.for.mac.localhost docker-host: localhost experimental: true curl-loop-sleep-time: .8 extensions: - ./lib/tab-block.js - ./lib/remote-include-processor.js ui: bundle: url: https://github.com/redhat-scholars/course-ui/releases/download/v0.1.14/ui-bundle.zip snapshot: true supplemental_files: ./supplemental-ui output: dir: ./gh-pages ================================================ FILE: github-pages.yml ================================================ runtime: cache_dir: ./.cache/antora site: title: Kubernetes Tutorial url: https://redhat-scholars.github.io/kubernetes-tutorial/ start_page: kubernetes-tutorial::index.adoc content: sources: - url: . branches: [v1.29,v1.34] start_path: documentation asciidoc: attributes: # This attribute allows you to control which version of your documentation is generated by Antora release-version: v1.29 # This attribute enables creating footer based navigation to move to the next or prev page as defined in nav.adoc page-pagination: true # Set this to true to hide the versions component that appears at the bottom of the side nav page-hide-versions-component: false # Set myrepo to the organization in the image registry you want to reference in the tutorial myrepo: your-repo # Set the docker-host attribute to the hostname that should be used to refer to target of curl commands in a container # for example: docker.for.mac.localhost docker-host: localhost experimental: true curl-loop-sleep-time: .8 extensions: - ./lib/tab-block.js - ./lib/remote-include-processor.js ui: bundle: url: https://github.com/redhat-scholars/course-ui/releases/download/v0.1.14/ui-bundle.zip snapshot: true supplemental_files: ./supplemental-ui output: dir: ./gh-pages ================================================ FILE: gulpfile.babel.js ================================================ /*jshint esversion: 6 */ import { series, watch } from "gulp"; import { remove } from "fs-extra"; import { readFileSync } from "fs"; import {load as yamlLoad} from "yaml-js"; import generator from "@antora/site-generator-default"; import browserSync from "browser-sync"; const filename = "github-pages.yml"; const server = browserSync.create(); const args = ["--playbook", filename]; //Watch Paths function watchGlobs() { let json_content = readFileSync(`${__dirname}/${filename}`, "UTF-8"); let yaml_content = yamlLoad(json_content); let dirs = yaml_content.content.sources.map(source => [ `**/*.yml`, `**/*.adoc`, `**/*.hbs` ]); dirs.push(["${filename}"]); dirs = [].concat(...dirs); //console.log(dirs); return dirs; } const siteWatch = () => watch(watchGlobs(), series(build, reload)); const removeSite = done => remove("gh-pages", done); const removeCache = done => remove(".cache", done); function build(done) { generator(args, process.env) .then(() => { done(); }) .catch(err => { console.log(err); done(); }); } function workshopSite(done){ generator(["--pull", "--stacktrace","--playbook","workshop-site.yaml"], process.env) .then(() => { done(); }) .catch(err => { console.log(err); done(); }); } function reload(done) { server.reload(); done(); } function serve(done) { server.init({ server: { baseDir: "./gh-pages" } }); done(); } const _build = build; export { _build as build }; const _clean = series(removeSite, removeCache); export { _clean as clean }; const _default = series(_clean, build, serve, siteWatch); export { _default as default }; //build workshop docs const _wsite = series(_clean, workshopSite); export { _wsite as workshopSite }; ================================================ FILE: lib/copy-to-clipboard.js ================================================ const BlockCopyToClipboardMacro = (() => { const $context = Symbol("context"); const superclass = Opal.module(null, "Asciidoctor").Extensions .BlockMacroProcessor; const scope = Opal.klass( Opal.module(null, "Antora"), superclass, "BlockCopyToClipboardMacro", function() {} ); Opal.defn(scope, "$initialize", function initialize(name, config, context) { Opal.send( this, Opal.find_super_dispatcher(this, "initialize", initialize), [name, config] ); this[$context] = context; }); Opal.defn(scope, "$process", function(parent, target, attrs) { const t = target.startsWith(":") ? target.substr(1) : target; //console.log("target:", t); const createHtmlFragment = html => this.createBlock(parent, "pass", html); const html = `
`; parent.blocks.push(createHtmlFragment(html)); }); return scope; })(); module.exports.register = (registry, context) => { registry.blockMacro( BlockCopyToClipboardMacro.$new("copyToClipboard", Opal.hash(), context) ); }; ================================================ FILE: lib/remote-include-processor.js ================================================ module.exports = function () { this.includeProcessor(function () { this.$option('position', '>>') this.handles((target) => target.startsWith('http')) this.process((doc, reader, target, attrs) => { const contents = require('child_process').execFileSync('curl', ['--silent', '-L', target], { encoding: 'utf8' }) reader.pushInclude(contents, target, target, 1, attrs) }) }) } ================================================ FILE: lib/tab-block.js ================================================ /** * Extends the AsciiDoc syntax to support a tabset element. The tabset is * created from a dlist that is enclosed in an example block marked with the * tabs style. * * Usage: * * [tabs] * ==== * Tab A:: * + * -- * Contents of tab A. * -- * Tab B:: * + * -- * Contents of tab B. * -- * ==== * * To use this extension, register the extension.js file with Antora (i.e., * list it as an AsciiDoc extension in the Antora playbook file), combine * styles.css with the styles for the site, and combine behavior.js with the * JavaScript loaded by the page. * * @author Dan Allen */ const IdSeparatorChar = '-' const InvalidIdCharsRx = /[^a-zA-Z0-9_]/g const List = Opal.const_get_local(Opal.module(null, 'Asciidoctor'), 'List') const ListItem = Opal.const_get_local(Opal.module(null, 'Asciidoctor'), 'ListItem') const generateId = (str, idx) => `tabset${idx}_${str.toLowerCase().replace(InvalidIdCharsRx, IdSeparatorChar)}` function tabsBlock () { this.onContext('example') this.process((parent, reader, attrs) => { const createHtmlFragment = (html) => this.createBlock(parent, 'pass', html) const tabsetIdx = parent.getDocument().counter('idx-tabset') const nodes = [] nodes.push(createHtmlFragment('
')) const container = this.parseContent(this.createBlock(parent, 'open'), reader) const sourceTabs = container.getBlocks()[0] if (!(sourceTabs && sourceTabs.getContext() === 'dlist' && sourceTabs.getItems().length)) return const tabs = List.$new(parent, 'ulist') tabs.addRole('tabs') const panes = {} sourceTabs.getItems().forEach(([[title], details]) => { const tab = ListItem.$new(tabs) tabs.$append(tab) const id = generateId(title.getText(), tabsetIdx) tab.text = `[[${id}]]${title.text}` let blocks = details.getBlocks() const numBlocks = blocks.length if (numBlocks) { if (blocks[0].context === 'open' && numBlocks === 1) blocks = blocks[0].getBlocks() panes[id] = blocks.map((block) => (block.parent = parent) && block) } }) nodes.push(tabs) nodes.push(createHtmlFragment('
')) Object.entries(panes).forEach(([id, blocks]) => { nodes.push(createHtmlFragment(`
`)) nodes.push(...blocks) nodes.push(createHtmlFragment('
')) }) nodes.push(createHtmlFragment('
')) nodes.push(createHtmlFragment('
')) parent.blocks.push(...nodes) }) } module.exports.register = (registry, context) => { registry.block('tabs', tabsBlock) } ================================================ FILE: package.json ================================================ { "name": "kubernetes-tutorial-site", "description": "Kubernetes Tutorial Documentation", "homepage": "https://redhat-scholars.github.io/kubernetes-tutorial", "author": { "email": "kamesh.sampath@hotmail.com", "name": "Kamesh Sampath", "url": "https://twitter.com/@kamesh_sampath" }, "dependencies": { "@antora/cli": "2.3.1", "@antora/site-generator-default": "2.3.1", "@babel/cli": "^7.5.5", "@babel/core": "^7.5.5", "@babel/polyfill": "^7.4.4", "@babel/preset-env": "^7.5.5", "@babel/register": "^7.5.5", "browser-sync": "^2.26.7", "fs-extra": "^8.1.0", "gulp": "^4.0.0", "yaml-js": "^0.2.3" }, "devDependencies": {}, "scripts": { "dev": "gulp", "clean": "gulp clean", "workshop": "gulp workshopSite" }, "repository": { "type": "git", "url": "git+https://github.com/redhat-scholars/kubernetes-tutorial.git" }, "license": "Apache-2.0", "babel": { "presets": [ "@babel/preset-env" ] } } ================================================ FILE: scripts/create-kubeconfig.sh ================================================ #!/bin/bash set -euo pipefail declare MINIKUBE_HOST=${1:-192.168.86.48} # private key to be used to authenticate with MINIKUBE_HOST for USER declare KEYFILE_PATH=${2:-${HOME}/.ssh/emu-fedora} # user on the minikube host declare USER=${3:-mwh} declare MINIKUBE_PROFILE_NAME=${4:-minikube} # as the remote server, assume we want the kubeconfig at the exported KUBECONFIG location declare REMOTE_KUBECONFIG_PATH=${KUBECONFIG} declare CERTS_DIR="$DEMO_HOME/$CONFIG_SUBDIR/certs" if [[ -f $REMOTE_KUBECONFIG_PATH ]]; then echo "Removing old config file at ${REMOTE_KUBECONFIG_PATH}" rm ${REMOTE_KUBECONFIG_PATH} fi if [[ ! -d ${CERTS_DIR} ]]; then echo "Creating certs dir ${CERTS_DIR}" mkdir -p ${CERTS_DIR} fi # Assume this is run right after minikube is setup on host machine for user ${USER} if [[ $MINIKUBE_HOST == "localhost" ]]; then cp ~/.kube/config ${REMOTE_KUBECONFIG_PATH} else scp -i ${KEYFILE_PATH} ${USER}@${MINIKUBE_HOST}:~/.kube/config ${REMOTE_KUBECONFIG_PATH} fi # find the host directory for certs. For kubectl config documentation and examples, see # this site: https://kubernetes.io/docs/reference/kubectl/cheatsheet/#kubectl-context-and-configuration CA_CRT=$(kubectl config view -o jsonpath="{.clusters[?(@.name == \"${MINIKUBE_PROFILE_NAME}\")].cluster.certificate-authority}" --kubeconfig=${REMOTE_KUBECONFIG_PATH}) kubectl config set clusters.${MINIKUBE_PROFILE_NAME}.certificate-authority "${CERTS_DIR}/$(basename ${CA_CRT})" --kubeconfig=${REMOTE_KUBECONFIG_PATH} CLIENT_CRT=$(kubectl config view -o jsonpath="{.users[?(@.name == \"${MINIKUBE_PROFILE_NAME}\")].user.client-certificate}" --kubeconfig=${REMOTE_KUBECONFIG_PATH}) kubectl config set users.${MINIKUBE_PROFILE_NAME}.client-certificate "${CERTS_DIR}/$(basename ${CLIENT_CRT})" --kubeconfig=${REMOTE_KUBECONFIG_PATH} CLIENT_KEY=$(kubectl config view -o jsonpath="{.users[?(@.name == \"${MINIKUBE_PROFILE_NAME}\")].user.client-key}" --kubeconfig=${REMOTE_KUBECONFIG_PATH}) kubectl config set users.${MINIKUBE_PROFILE_NAME}.client-key "${CERTS_DIR}/$(basename ${CLIENT_KEY})" --kubeconfig=${REMOTE_KUBECONFIG_PATH} declare FILES=( ${CA_CRT} ${CLIENT_CRT} ${CLIENT_KEY} ) for HOST_FILE_PATH in ${FILES[@]}; do FILE_NAME=$(basename ${HOST_FILE_PATH}) REMOTE_CERT_FILE_PATH="${CERTS_DIR}/${FILE_NAME}" if [[ $MINIKUBE_HOST == "localhost" ]]; then LOCAL_CERT_FILE_PATH=~/.minikube/profiles/${MINIKUBE_PROFILE_NAME}/${FILE_NAME} if [[ ! -f "$LOCAL_CERT_FILE_PATH" ]]; then LOCAL_CERT_FILE_PATH=~/.minikube/${FILE_NAME} fi echo "Copying ${FILE_NAME} from ${LOCAL_CERT_FILE_PATH} to ${REMOTE_CERT_FILE_PATH}." cp "${LOCAL_CERT_FILE_PATH}" "${REMOTE_CERT_FILE_PATH}" else echo "Copying ${FILE_NAME} from ${MINIKUBE_HOST}:${HOST_FILE_PATH} to ${REMOTE_CERT_FILE_PATH}." scp -i ${KEYFILE_PATH} ${USER}@${MINIKUBE_HOST}:${HOST_FILE_PATH} ${REMOTE_CERT_FILE_PATH} fi done # Reset the server on the config to the current host if [[ ${MINIKUBE_HOST} != "localhost" ]]; then kubectl config set clusters.${MINIKUBE_PROFILE_NAME}.server "https://${MINIKUBE_HOST}:8443" --kubeconfig=${REMOTE_KUBECONFIG_PATH} fi ================================================ FILE: scripts/github-pages-publish.sh ================================================ #!/bin/bash set -euo pipefail declare SITE=${1:-github-pages-stage.yml} declare REPO=${2:-$(git remote get-url origin)} declare BRANCH="gh-pages" echo "Removing old publish directory" if [[ -d $DEMO_HOME/gh-publish ]]; then rm -rf $DEMO_HOME/gh-publish fi echo "Removing antora cache directory" if [[ -d $DEMO_HOME/.cache ]]; then rm -rf $DEMO_HOME/.cache fi git clone -b ${BRANCH} ${REPO} $DEMO_HOME/gh-publish echo "Generating the site documentation from ${SITE}" antora generate --stacktrace $DEMO_HOME/${SITE} --to-dir $DEMO_HOME/gh-publish echo "Pushing site to ${BRANCH} branch of ${REPO}" cd $DEMO_HOME/gh-publish git add --all . git commit -m"Automated Publish" git push origin echo "Site published successfully!" ================================================ FILE: scripts/minikube-server-setup.sh ================================================ set -euo pipefail declare MINIKUBE_PROFILE=${1:-devnation} declare MINIKUBE_IP=${2:-$(hostname -I | awk '{print $1}')} declare MINIKUBE_MEM=${3:-4096} declare MINIKUBE_CPU=${4:-2} declare DRIVER=${5:-kvm2} # for MacOS you might want to use hyperkit or virtualbox minikube start --memory=${MINIKUBE_MEM} --cpus=${MINIKUBE_CPU} --driver=${DRIVER} -p ${MINIKUBE_PROFILE} --apiserver-ips=${MINIKUBE_IP} minikube config set profile ${MINIKUBE_PROFILE} IPTABLE_RULES=('LIBVIRT_FWI' 'LIBVIRT_FWO') for RULE in "${IPTABLE_RULES[@]}"; do declare RULE_INDEX=$(sudo iptables -L ${RULE} --line-numbers 2>/dev/null | grep REJECT | awk '{print $1}' | head -n 1) while [[ -n "${RULE_INDEX}" ]]; do echo "Deleting from rule: ${RULE} index: ${RULE_INDEX}> $(sudo iptables -L ${RULE} ${RULE_INDEX})" sudo iptables -D ${RULE} ${RULE_INDEX} # see if there are any other reject indeces RULE_INDEX=$(sudo iptables -L ${RULE} --line-numbers 2>/dev/null | grep REJECT | awk '{print $1}' | head -n 1) # echo "New Rule Index is $RULE_INDEX" done done # turn on DNAT sudo iptables -t nat -A PREROUTING -p tcp --dport 30000:32767 -j DNAT --to-destination $(minikube ip):30000-32767 sudo iptables -t nat -A PREROUTING -p tcp --dport 8443 -j DNAT --to-destination $(minikube ip):8443 # This can be used to restore errors in the above script removing rules # sudo iptables -A LIBVIRT_FWI -d 192.168.122.0/24 -o virbr0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT # sudo iptables -A LIBVIRT_FWO -s 192.168.122.0/24 -i virbr0 -j ACCEPT # turn off firewall sudo systemctl disable firewalld; ================================================ FILE: scripts/pod-node-columns-template.txt ================================================ NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES myboot2-7c5f46cbc9-hwm2v 0/1 Pending 0 31s ================================================ FILE: scripts/shell-setup.sh ================================================ #!/bin/bash # per the following $0 doesn't work reliably when the script is sourced: # https://stackoverflow.com/questions/35006457/choosing-between-0-and-bash-source. But # in some cases I've found BASH_SOURCE hasn't been set correctly. declare SCRIPT=$0 if [[ "$SCRIPT" == "/bin/bash" ]]; then SCRIPT="${BASH_SOURCE}" fi if [[ -z "${SCRIPT}" ]]; then echo "BASH_SOURCE: ${BASH_SOURCE}, 0 is: $0" echo "Failed to find the running name of the script, you need to set DEMO_HOME manually" fi export DEMO_HOME=$( cd "$(dirname "${SCRIPT}")/.." ; pwd -P ) echo "Welcome to kubernetes tutorial" ================================================ FILE: supplemental-ui/partials/header-content.hbs ================================================
================================================ FILE: supplemental-ui/partials/nav-explore.hbs ================================================ ================================================ FILE: supplemental-ui/partials/nav-menu.hbs ================================================ {{#with page.navigation}} {{/with}} ================================================ FILE: supplemental-ui/partials/nav.hbs ================================================ ================================================ FILE: vscode-asciidoc-extra.json ================================================ { "Add Tabs": { "prefix": "tabs", "body": [ "[tabs]", "====", "${1:tab1}::", "+", "--", "--", "${2:tab2}::", "+", "--", "--", "====" ], "description": "Add Tabs macro" }, "Add clipboard": { "prefix": "clipboard", "body": [ "[#${1:clipboardid}]", "[source,${2:bash},subs=\"+macros,+attributes\"]", "----", "${3}", "----", "copyToClipboard::$1[]" ], "description": "Add Source with Clipboard" }, "Add Navigation": { "prefix": "nav", "body": [ "${1:*} xref:${2:page.adoc}[${3:Nav Title}]" ], "description": "Add new navigation" } }