From 135bd53414114ca4a5016f086ac14d437b459d95 Mon Sep 17 00:00:00 2001 From: mg Date: Fri, 4 Nov 2022 20:58:37 +0100 Subject: [PATCH] k3s - Basics (#423) Co-authored-by: Michael Grote Reviewed-on: https://git.mgrote.net/mg/homeserver/pulls/423 --- .drone.yml | 2 +- docker-compose/drone/docker-compose.yml.j2 | 19 - docker-compose/miniflux/filter.txt | 8 + docker-compose/munin/docker-compose.yml.j2 | 3 + .../nextcloud/docker-compose.yml.j2 | 2 +- group_vars/k3s.yml | 95 +++ host_vars/docker10.grote.lan.yml | 2 - host_vars/k3s1.grote.lan.yml | 3 + host_vars/k3s2.grote.lan.yml | 3 + host_vars/k3s3.grote.lan.yml | 3 + host_vars/pve5.grote.lan.yml | 4 +- inventory | 16 + keepass_db.kdbx | Bin 65454 -> 70910 bytes playbooks/3_service/k3s.yml | 9 + playbooks/3_service/nfs.yml | 4 + roles/geerlingguy.nfs_server/.ansible-lint | 3 + .../.github/FUNDING.yml | 4 + .../geerlingguy.nfs_server/.github/stale.yml | 57 ++ .../.github/workflows/ci.yml | 67 ++ .../.github/workflows/release.yml | 40 ++ roles/geerlingguy.nfs_server/.gitignore | 5 + roles/geerlingguy.nfs_server/.yamllint | 10 + roles/geerlingguy.nfs_server/LICENSE | 20 + roles/geerlingguy.nfs_server/README.md | 40 ++ .../geerlingguy.nfs_server/defaults/main.yml | 5 + .../geerlingguy.nfs_server/handlers/main.yml | 3 + roles/geerlingguy.nfs_server/meta/main.yml | 30 + .../molecule/default/converge.yml | 13 + .../molecule/default/molecule.yml | 18 + roles/geerlingguy.nfs_server/tasks/main.yml | 36 + .../tasks/setup-Debian.yml | 7 + .../tasks/setup-RedHat.yml | 9 + .../templates/exports.j2 | 13 + roles/geerlingguy.nfs_server/vars/Debian.yml | 2 + roles/geerlingguy.nfs_server/vars/Fedora.yml | 2 + roles/geerlingguy.nfs_server/vars/RedHat.yml | 2 + roles/mgrote.fluxcd/LICENSE | 674 ++++++++++++++++++ roles/mgrote.fluxcd/README.md | 3 + roles/mgrote.fluxcd/defaults/main.yml | 17 + roles/mgrote.fluxcd/tasks/main.yml | 97 +++ roles/mgrote.fluxcd/tasks/user.yml | 18 + roles/mgrote.fluxcd/templates/bootstrap.sh | 4 + roles/mgrote.k8s_autocompletion/LICENSE | 674 ++++++++++++++++++ roles/mgrote.k8s_autocompletion/README.md | 5 + .../mgrote.k8s_autocompletion/tasks/main.yml | 13 + roles/mgrote.k8s_misc/LICENSE | 674 ++++++++++++++++++ roles/mgrote.k8s_misc/README.md | 3 + roles/mgrote.k8s_misc/tasks/main.yml | 7 + roles/mgrote.sealed-secrets/LICENSE | 674 ++++++++++++++++++ roles/mgrote.sealed-secrets/README.md | 49 ++ roles/mgrote.sealed-secrets/defaults/main.yml | 8 + roles/mgrote.sealed-secrets/tasks/import.yml | 29 + roles/mgrote.sealed-secrets/tasks/main.yml | 26 + roles/mgrote.sealed-secrets/tasks/user.yml | 18 + .../templates/private.key.j2 | 1 + .../defaults/main.yml | 4 + .../tasks/main.yml | 28 + .../vars/main.yml | 5 + roles/xanmanning.k3s/.ansible-lint | 4 + roles/xanmanning.k3s/.devcontainer/Dockerfile | 26 + .../.devcontainer/devcontainer.json | 28 + .../.github/ISSUE_TEMPLATE/bug_report.md | 55 ++ .../.github/ISSUE_TEMPLATE/config.yml | 3 + .../.github/ISSUE_TEMPLATE/feature_request.md | 33 + .../.github/PULL_REQUEST_TEMPLATE.md | 37 + roles/xanmanning.k3s/.github/stale.yml | 18 + roles/xanmanning.k3s/.github/workflows/ci.yml | 75 ++ .../.github/workflows/release.yml | 32 + roles/xanmanning.k3s/.gitignore | 13 + roles/xanmanning.k3s/.yamllint | 33 + roles/xanmanning.k3s/CHANGELOG.md | 600 ++++++++++++++++ roles/xanmanning.k3s/CONTRIBUTING.md | 46 ++ roles/xanmanning.k3s/LICENSE.txt | 30 + roles/xanmanning.k3s/README.md | 412 +++++++++++ roles/xanmanning.k3s/defaults/main.yml | 157 ++++ roles/xanmanning.k3s/documentation/README.md | 44 ++ .../configuration/2-node-ha-ext-datastore.md | 79 ++ .../configuration/ipv4-ipv6-dual-stack.md | 21 + .../multiple-standalone-k3s-nodes.md | 71 ++ .../node-labels-and-component-args.md | 39 + .../configuration/systemd-config.md | 19 + .../configuration/use-an-alternate-cni.md | 63 ++ .../operations/extending-a-cluster.md | 69 ++ .../operations/shrinking-a-cluster.md | 74 ++ .../operations/stop-start-cluster.md | 93 +++ .../documentation/operations/updating-k3s.md | 52 ++ .../documentation/quickstart-cluster.md | 147 ++++ .../documentation/quickstart-ha-cluster.md | 154 ++++ .../documentation/quickstart-single-node.md | 121 ++++ roles/xanmanning.k3s/handlers/main.yml | 39 + roles/xanmanning.k3s/meta/main.yml | 86 +++ .../molecule/autodeploy/converge.yml | 28 + .../molecule/autodeploy/molecule.yml | 60 ++ .../molecule/autodeploy/prepare.yml | 26 + .../templates/00-ns-monitoring.yml.j2 | 4 + .../molecule/autodeploy/verify.yml | 9 + .../molecule/debug/converge.yml | 14 + .../molecule/debug/molecule.yml | 60 ++ .../xanmanning.k3s/molecule/debug/prepare.yml | 10 + .../xanmanning.k3s/molecule/debug/verify.yml | 9 + .../molecule/default/Dockerfile.j2 | 26 + .../molecule/default/INSTALL.rst | 22 + .../molecule/default/converge.yml | 12 + .../molecule/default/molecule.yml | 60 ++ .../molecule/default/playbook-download.yml | 9 + .../default/playbook-restart-cluster.yml | 9 + .../molecule/default/playbook-rootless.yml | 15 + .../molecule/default/playbook-standalone.yml | 9 + .../default/playbook-start-cluster.yml | 9 + .../default/playbook-stop-cluster.yml | 9 + .../default/playbook-uninstall-cluster.yml | 9 + .../molecule/default/prepare-rootless.yml | 23 + .../molecule/default/prepare.yml | 10 + .../molecule/default/tests/test_default.py | 14 + .../molecule/default/tests/test_default.pyc | Bin 0 -> 1025 bytes .../molecule/highavailabilitydb/Dockerfile.j2 | 7 + .../molecule/highavailabilitydb/INSTALL.rst | 22 + .../molecule/highavailabilitydb/converge.yml | 21 + .../haproxy-loadbalancer.conf.j2 | 13 + .../molecule/highavailabilitydb/molecule.yml | 73 ++ .../molecule/highavailabilitydb/prepare.yml | 48 ++ .../highavailabilitydb/tests/test_default.py | 14 + .../highavailabilitydb/tests/test_default.pyc | Bin 0 -> 1025 bytes .../highavailabilityetcd/converge.yml | 22 + .../haproxy-loadbalancer.conf.j2 | 13 + .../highavailabilityetcd/molecule.yml | 65 ++ .../molecule/highavailabilityetcd/prepare.yml | 59 ++ .../molecule/nodeploy/.gitignore | 1 + .../molecule/nodeploy/converge.yml | 12 + .../molecule/nodeploy/k3s_agent.yml | 9 + .../molecule/nodeploy/k3s_server.yml | 14 + .../molecule/nodeploy/molecule.yml | 60 ++ .../molecule/nodeploy/prepare.yml | 27 + .../molecule/nodeploy/verify.yml | 9 + .../xanmanning.k3s/molecule/requirements.txt | 6 + roles/xanmanning.k3s/requirements.txt | 1 + .../tasks/determine_systemd_context.yml | 10 + roles/xanmanning.k3s/tasks/ensure_cluster.yml | 106 +++ .../tasks/ensure_containerd_registries.yml | 11 + .../ensure_control_plane_started_openrc.yml | 15 + .../ensure_control_plane_started_systemd.yml | 16 + .../tasks/ensure_directories.yml | 12 + .../xanmanning.k3s/tasks/ensure_downloads.yml | 51 ++ .../tasks/ensure_drain_and_remove_nodes.yml | 56 ++ .../xanmanning.k3s/tasks/ensure_installed.yml | 28 + .../tasks/ensure_installed_node.yml | 103 +++ .../tasks/ensure_k3s_auto_deploy.yml | 62 ++ .../tasks/ensure_k3s_config_files.yml | 31 + .../tasks/ensure_pre_configuration.yml | 129 ++++ roles/xanmanning.k3s/tasks/ensure_started.yml | 20 + roles/xanmanning.k3s/tasks/ensure_stopped.yml | 20 + .../tasks/ensure_uninstalled.yml | 42 ++ roles/xanmanning.k3s/tasks/ensure_uploads.yml | 15 + roles/xanmanning.k3s/tasks/main.yml | 5 + .../tasks/post_checks_control_plane.yml | 10 + .../tasks/post_checks_nodes.yml | 21 + .../tasks/post_checks_uninstalled.yml | 41 ++ roles/xanmanning.k3s/tasks/pre_checks.yml | 123 ++++ .../tasks/pre_checks_cgroups.yml | 17 + .../tasks/pre_checks_cluster.yml | 19 + .../tasks/pre_checks_control_node_count.yml | 46 ++ .../pre_checks_experimental_variables.yml | 31 + .../tasks/pre_checks_issue_data.yml | 87 +++ .../tasks/pre_checks_packages.yml | 26 + .../tasks/pre_checks_unsupported_rootless.yml | 63 ++ .../tasks/pre_checks_variables.yml | 57 ++ .../tasks/pre_checks_version.yml | 32 + .../xanmanning.k3s/tasks/state_downloaded.yml | 12 + .../xanmanning.k3s/tasks/state_installed.yml | 38 + .../xanmanning.k3s/tasks/state_restarted.yml | 5 + roles/xanmanning.k3s/tasks/state_started.yml | 3 + roles/xanmanning.k3s/tasks/state_stopped.yml | 3 + .../tasks/state_uninstalled.yml | 12 + .../xanmanning.k3s/tasks/state_validated.yml | 5 + .../xanmanning.k3s/templates/cluster-token.j2 | 1 + roles/xanmanning.k3s/templates/config.yaml.j2 | 8 + .../templates/k3s-killall.sh.j2 | 70 ++ .../templates/k3s-uninstall.sh.j2 | 50 ++ .../xanmanning.k3s/templates/k3s.logrotate.j2 | 5 + roles/xanmanning.k3s/templates/k3s.openrc.j2 | 47 ++ roles/xanmanning.k3s/templates/k3s.service.j2 | 70 ++ .../templates/registries.yaml.j2 | 2 + roles/xanmanning.k3s/vars/main.yml | 206 ++++++ 183 files changed, 9132 insertions(+), 25 deletions(-) create mode 100644 group_vars/k3s.yml create mode 100644 host_vars/k3s1.grote.lan.yml create mode 100644 host_vars/k3s2.grote.lan.yml create mode 100644 host_vars/k3s3.grote.lan.yml create mode 100644 playbooks/3_service/k3s.yml create mode 100644 playbooks/3_service/nfs.yml create mode 100644 roles/geerlingguy.nfs_server/.ansible-lint create mode 100644 roles/geerlingguy.nfs_server/.github/FUNDING.yml create mode 100644 roles/geerlingguy.nfs_server/.github/stale.yml create mode 100644 roles/geerlingguy.nfs_server/.github/workflows/ci.yml create mode 100644 roles/geerlingguy.nfs_server/.github/workflows/release.yml create mode 100644 roles/geerlingguy.nfs_server/.gitignore create mode 100644 roles/geerlingguy.nfs_server/.yamllint create mode 100644 roles/geerlingguy.nfs_server/LICENSE create mode 100644 roles/geerlingguy.nfs_server/README.md create mode 100644 roles/geerlingguy.nfs_server/defaults/main.yml create mode 100644 roles/geerlingguy.nfs_server/handlers/main.yml create mode 100644 roles/geerlingguy.nfs_server/meta/main.yml create mode 100644 roles/geerlingguy.nfs_server/molecule/default/converge.yml create mode 100644 roles/geerlingguy.nfs_server/molecule/default/molecule.yml create mode 100644 roles/geerlingguy.nfs_server/tasks/main.yml create mode 100644 roles/geerlingguy.nfs_server/tasks/setup-Debian.yml create mode 100644 roles/geerlingguy.nfs_server/tasks/setup-RedHat.yml create mode 100644 roles/geerlingguy.nfs_server/templates/exports.j2 create mode 100644 roles/geerlingguy.nfs_server/vars/Debian.yml create mode 100644 roles/geerlingguy.nfs_server/vars/Fedora.yml create mode 100644 roles/geerlingguy.nfs_server/vars/RedHat.yml create mode 100644 roles/mgrote.fluxcd/LICENSE create mode 100644 roles/mgrote.fluxcd/README.md create mode 100644 roles/mgrote.fluxcd/defaults/main.yml create mode 100644 roles/mgrote.fluxcd/tasks/main.yml create mode 100644 roles/mgrote.fluxcd/tasks/user.yml create mode 100644 roles/mgrote.fluxcd/templates/bootstrap.sh create mode 100644 roles/mgrote.k8s_autocompletion/LICENSE create mode 100644 roles/mgrote.k8s_autocompletion/README.md create mode 100644 roles/mgrote.k8s_autocompletion/tasks/main.yml create mode 100644 roles/mgrote.k8s_misc/LICENSE create mode 100644 roles/mgrote.k8s_misc/README.md create mode 100644 roles/mgrote.k8s_misc/tasks/main.yml create mode 100644 roles/mgrote.sealed-secrets/LICENSE create mode 100644 roles/mgrote.sealed-secrets/README.md create mode 100644 roles/mgrote.sealed-secrets/defaults/main.yml create mode 100644 roles/mgrote.sealed-secrets/tasks/import.yml create mode 100644 roles/mgrote.sealed-secrets/tasks/main.yml create mode 100644 roles/mgrote.sealed-secrets/tasks/user.yml create mode 100644 roles/mgrote.sealed-secrets/templates/private.key.j2 create mode 100644 roles/pandemonium1986.ansible-role-k9s/defaults/main.yml create mode 100644 roles/pandemonium1986.ansible-role-k9s/tasks/main.yml create mode 100644 roles/pandemonium1986.ansible-role-k9s/vars/main.yml create mode 100644 roles/xanmanning.k3s/.ansible-lint create mode 100644 roles/xanmanning.k3s/.devcontainer/Dockerfile create mode 100644 roles/xanmanning.k3s/.devcontainer/devcontainer.json create mode 100644 roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/config.yml create mode 100644 roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 roles/xanmanning.k3s/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 roles/xanmanning.k3s/.github/stale.yml create mode 100644 roles/xanmanning.k3s/.github/workflows/ci.yml create mode 100644 roles/xanmanning.k3s/.github/workflows/release.yml create mode 100644 roles/xanmanning.k3s/.gitignore create mode 100644 roles/xanmanning.k3s/.yamllint create mode 100644 roles/xanmanning.k3s/CHANGELOG.md create mode 100644 roles/xanmanning.k3s/CONTRIBUTING.md create mode 100644 roles/xanmanning.k3s/LICENSE.txt create mode 100644 roles/xanmanning.k3s/README.md create mode 100644 roles/xanmanning.k3s/defaults/main.yml create mode 100644 roles/xanmanning.k3s/documentation/README.md create mode 100644 roles/xanmanning.k3s/documentation/configuration/2-node-ha-ext-datastore.md create mode 100644 roles/xanmanning.k3s/documentation/configuration/ipv4-ipv6-dual-stack.md create mode 100644 roles/xanmanning.k3s/documentation/configuration/multiple-standalone-k3s-nodes.md create mode 100644 roles/xanmanning.k3s/documentation/configuration/node-labels-and-component-args.md create mode 100644 roles/xanmanning.k3s/documentation/configuration/systemd-config.md create mode 100644 roles/xanmanning.k3s/documentation/configuration/use-an-alternate-cni.md create mode 100644 roles/xanmanning.k3s/documentation/operations/extending-a-cluster.md create mode 100644 roles/xanmanning.k3s/documentation/operations/shrinking-a-cluster.md create mode 100644 roles/xanmanning.k3s/documentation/operations/stop-start-cluster.md create mode 100644 roles/xanmanning.k3s/documentation/operations/updating-k3s.md create mode 100644 roles/xanmanning.k3s/documentation/quickstart-cluster.md create mode 100644 roles/xanmanning.k3s/documentation/quickstart-ha-cluster.md create mode 100644 roles/xanmanning.k3s/documentation/quickstart-single-node.md create mode 100644 roles/xanmanning.k3s/handlers/main.yml create mode 100644 roles/xanmanning.k3s/meta/main.yml create mode 100644 roles/xanmanning.k3s/molecule/autodeploy/converge.yml create mode 100644 roles/xanmanning.k3s/molecule/autodeploy/molecule.yml create mode 100644 roles/xanmanning.k3s/molecule/autodeploy/prepare.yml create mode 100644 roles/xanmanning.k3s/molecule/autodeploy/templates/00-ns-monitoring.yml.j2 create mode 100644 roles/xanmanning.k3s/molecule/autodeploy/verify.yml create mode 100644 roles/xanmanning.k3s/molecule/debug/converge.yml create mode 100644 roles/xanmanning.k3s/molecule/debug/molecule.yml create mode 100644 roles/xanmanning.k3s/molecule/debug/prepare.yml create mode 100644 roles/xanmanning.k3s/molecule/debug/verify.yml create mode 100644 roles/xanmanning.k3s/molecule/default/Dockerfile.j2 create mode 100644 roles/xanmanning.k3s/molecule/default/INSTALL.rst create mode 100644 roles/xanmanning.k3s/molecule/default/converge.yml create mode 100644 roles/xanmanning.k3s/molecule/default/molecule.yml create mode 100644 roles/xanmanning.k3s/molecule/default/playbook-download.yml create mode 100644 roles/xanmanning.k3s/molecule/default/playbook-restart-cluster.yml create mode 100644 roles/xanmanning.k3s/molecule/default/playbook-rootless.yml create mode 100644 roles/xanmanning.k3s/molecule/default/playbook-standalone.yml create mode 100644 roles/xanmanning.k3s/molecule/default/playbook-start-cluster.yml create mode 100644 roles/xanmanning.k3s/molecule/default/playbook-stop-cluster.yml create mode 100644 roles/xanmanning.k3s/molecule/default/playbook-uninstall-cluster.yml create mode 100644 roles/xanmanning.k3s/molecule/default/prepare-rootless.yml create mode 100644 roles/xanmanning.k3s/molecule/default/prepare.yml create mode 100644 roles/xanmanning.k3s/molecule/default/tests/test_default.py create mode 100644 roles/xanmanning.k3s/molecule/default/tests/test_default.pyc create mode 100644 roles/xanmanning.k3s/molecule/highavailabilitydb/Dockerfile.j2 create mode 100644 roles/xanmanning.k3s/molecule/highavailabilitydb/INSTALL.rst create mode 100644 roles/xanmanning.k3s/molecule/highavailabilitydb/converge.yml create mode 100644 roles/xanmanning.k3s/molecule/highavailabilitydb/haproxy-loadbalancer.conf.j2 create mode 100644 roles/xanmanning.k3s/molecule/highavailabilitydb/molecule.yml create mode 100644 roles/xanmanning.k3s/molecule/highavailabilitydb/prepare.yml create mode 100644 roles/xanmanning.k3s/molecule/highavailabilitydb/tests/test_default.py create mode 100644 roles/xanmanning.k3s/molecule/highavailabilitydb/tests/test_default.pyc create mode 100644 roles/xanmanning.k3s/molecule/highavailabilityetcd/converge.yml create mode 100644 roles/xanmanning.k3s/molecule/highavailabilityetcd/haproxy-loadbalancer.conf.j2 create mode 100644 roles/xanmanning.k3s/molecule/highavailabilityetcd/molecule.yml create mode 100644 roles/xanmanning.k3s/molecule/highavailabilityetcd/prepare.yml create mode 100644 roles/xanmanning.k3s/molecule/nodeploy/.gitignore create mode 100644 roles/xanmanning.k3s/molecule/nodeploy/converge.yml create mode 100644 roles/xanmanning.k3s/molecule/nodeploy/k3s_agent.yml create mode 100644 roles/xanmanning.k3s/molecule/nodeploy/k3s_server.yml create mode 100644 roles/xanmanning.k3s/molecule/nodeploy/molecule.yml create mode 100644 roles/xanmanning.k3s/molecule/nodeploy/prepare.yml create mode 100644 roles/xanmanning.k3s/molecule/nodeploy/verify.yml create mode 100644 roles/xanmanning.k3s/molecule/requirements.txt create mode 100644 roles/xanmanning.k3s/requirements.txt create mode 100644 roles/xanmanning.k3s/tasks/determine_systemd_context.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_cluster.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_containerd_registries.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_control_plane_started_openrc.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_control_plane_started_systemd.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_directories.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_downloads.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_drain_and_remove_nodes.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_installed.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_installed_node.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_k3s_auto_deploy.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_k3s_config_files.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_pre_configuration.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_started.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_stopped.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_uninstalled.yml create mode 100644 roles/xanmanning.k3s/tasks/ensure_uploads.yml create mode 100644 roles/xanmanning.k3s/tasks/main.yml create mode 100644 roles/xanmanning.k3s/tasks/post_checks_control_plane.yml create mode 100644 roles/xanmanning.k3s/tasks/post_checks_nodes.yml create mode 100644 roles/xanmanning.k3s/tasks/post_checks_uninstalled.yml create mode 100644 roles/xanmanning.k3s/tasks/pre_checks.yml create mode 100644 roles/xanmanning.k3s/tasks/pre_checks_cgroups.yml create mode 100644 roles/xanmanning.k3s/tasks/pre_checks_cluster.yml create mode 100644 roles/xanmanning.k3s/tasks/pre_checks_control_node_count.yml create mode 100644 roles/xanmanning.k3s/tasks/pre_checks_experimental_variables.yml create mode 100644 roles/xanmanning.k3s/tasks/pre_checks_issue_data.yml create mode 100644 roles/xanmanning.k3s/tasks/pre_checks_packages.yml create mode 100644 roles/xanmanning.k3s/tasks/pre_checks_unsupported_rootless.yml create mode 100644 roles/xanmanning.k3s/tasks/pre_checks_variables.yml create mode 100644 roles/xanmanning.k3s/tasks/pre_checks_version.yml create mode 100644 roles/xanmanning.k3s/tasks/state_downloaded.yml create mode 100644 roles/xanmanning.k3s/tasks/state_installed.yml create mode 100644 roles/xanmanning.k3s/tasks/state_restarted.yml create mode 100644 roles/xanmanning.k3s/tasks/state_started.yml create mode 100644 roles/xanmanning.k3s/tasks/state_stopped.yml create mode 100644 roles/xanmanning.k3s/tasks/state_uninstalled.yml create mode 100644 roles/xanmanning.k3s/tasks/state_validated.yml create mode 100644 roles/xanmanning.k3s/templates/cluster-token.j2 create mode 100644 roles/xanmanning.k3s/templates/config.yaml.j2 create mode 100644 roles/xanmanning.k3s/templates/k3s-killall.sh.j2 create mode 100644 roles/xanmanning.k3s/templates/k3s-uninstall.sh.j2 create mode 100644 roles/xanmanning.k3s/templates/k3s.logrotate.j2 create mode 100644 roles/xanmanning.k3s/templates/k3s.openrc.j2 create mode 100644 roles/xanmanning.k3s/templates/k3s.service.j2 create mode 100644 roles/xanmanning.k3s/templates/registries.yaml.j2 create mode 100644 roles/xanmanning.k3s/vars/main.yml diff --git a/.drone.yml b/.drone.yml index bf8dc834..251fedc1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,6 +1,6 @@ --- kind: pipeline -type: docker +type: kubernetes name: default steps: diff --git a/docker-compose/drone/docker-compose.yml.j2 b/docker-compose/drone/docker-compose.yml.j2 index 13b4534a..6ec4fb5b 100644 --- a/docker-compose/drone/docker-compose.yml.j2 +++ b/docker-compose/drone/docker-compose.yml.j2 @@ -23,25 +23,6 @@ services: - intern -# runner - drone-runner-docker: - volumes: - - '/var/run/docker.sock:/var/run/docker.sock' - environment: - - DRONE_RPC_PROTO=http - # container-name des servers - - DRONE_RPC_HOST=drone-server - - DRONE_RPC_SECRET={{ lookup('keepass', 'drone_rpc_secret', 'password') }} - - DRONE_RUNNER_CAPACITY=4 - - DRONE_RUNNER_NAME=drone-runner - ports: - - '3000:3000' - restart: always - container_name: drone-runner - image: 'drone/drone-runner-docker:latest' - networks: - - intern - ######## Volumes ######## volumes: data: diff --git a/docker-compose/miniflux/filter.txt b/docker-compose/miniflux/filter.txt index ee97c6f0..97013087 100644 --- a/docker-compose/miniflux/filter.txt +++ b/docker-compose/miniflux/filter.txt @@ -9,6 +9,8 @@ computerbase.de::Fotowettbewerb: computerbase.de::Screenshot-Wettbewerb computerbase.de::Sonntagsfrage: computerbase.de::Wochenrück- und Ausblick: +computerbase.de::Twitter +computerbase.de::Elon Musk facebook.com::Bridge returned error golem.de::Anzeige golem.de::Aus dem Verlag: @@ -22,9 +24,13 @@ golem.de::Podcast golem.de::Sonst noch was? golem.de::Star Trek: golem.de::Tech Talks: +golem.de::Twitter +golem.de::Elon Musk golem.de::Wochenrückblick hardwareluxx.de::Der Hardwareluxx-Webwatch: hardwareluxx.de::Unsere Artikel der letzten Woche +heise.de::Elon Musk +heise.de::Twitter heise.de::Anzeige heise.de::Auslegungssache heise.de::Bit-Rauschen @@ -141,6 +147,8 @@ stackoverflow.blog::Podcast stackoverflow.blog::The Overflow stadt-bremerhaven.de::Bundesliga stadt-bremerhaven.de::Disney+ +stadt-bremerhaven.de::Twitter +stadt-bremerhaven.de::Elon Musk stadt-bremerhaven.de::eFootball stadt-bremerhaven.de::FIFA stadt-bremerhaven.de::Formel 1 diff --git a/docker-compose/munin/docker-compose.yml.j2 b/docker-compose/munin/docker-compose.yml.j2 index bcdee71b..ab7809c9 100644 --- a/docker-compose/munin/docker-compose.yml.j2 +++ b/docker-compose/munin/docker-compose.yml.j2 @@ -22,6 +22,9 @@ services: ansible2.grote.lan:ansible2.grote.lan pve5.grote.lan:pve5.grote.lan dokuwiki2.grote.lan:dokuwiki2.grote.lan + k3s1.grote.lan:k3s1.grote.lan + k3s2.grote.lan:k3s2.grote.lan + k3s3.grote.lan:k3s3.grote.lan gitea.grote.lan:gitea.grote.lan docker10.grote.lan:docker10.grote.lan dnsmasq.grote.lan:dnsmasq.grote.lan' diff --git a/docker-compose/nextcloud/docker-compose.yml.j2 b/docker-compose/nextcloud/docker-compose.yml.j2 index 5553b5e3..41edeaf7 100644 --- a/docker-compose/nextcloud/docker-compose.yml.j2 +++ b/docker-compose/nextcloud/docker-compose.yml.j2 @@ -71,7 +71,7 @@ services: PHP_MEMORY_LIMIT: 1024M PHP_UPLOAD_LIMIT: 10G APACHE_DISABLE_REWRITE_IP: 1 - TRUSTED_PROXIES: "192.168.2.43" # docker10.grote.lan/traefik #prüfen + TRUSTED_PROXIES: "192.168.2.43" # docker10.grote.lan/traefik volumes: - app:/var/www/html - data:/var/www/html/data diff --git a/group_vars/k3s.yml b/group_vars/k3s.yml new file mode 100644 index 00000000..c8c55c19 --- /dev/null +++ b/group_vars/k3s.yml @@ -0,0 +1,95 @@ +--- + ### mgrote.restic + restic_folders_to_backup: "/ /var" # --one-file-system ist gesetzt, also werden weitere Dateisysteme nicht eingeschlossen, es sei denn sie werden hier explizit angegeben; https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files + + ### pandemonium1986.ansible-role-k9s + k9s_version: "v0.26.7" + + ### mrlesmithjr.ansible-manage-lvm + #lvm_groups: + # - vgname: vg_gitea_data + # disks: + # - /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi1 + # create: true + # lvnames: + # - lvname: lv_gitea_data + # size: +100%FREE + # create: true + # filesystem: xfs + # mount: true + # mntp: /var/lib/gitea + #manage_lvm: true + #pvresize_to_max: true + + ### oefenweb.ufw + ufw_rules: +# - rule: allow +# to_port: 22 +# protocol: tcp +# comment: 'ssh' +# from_ip: 0.0.0.0/0 +# - rule: allow +# to_port: 4949 +# protocol: tcp +# comment: 'munin' +# from_ip: 192.168.2.0/24 +# # https://rancher.com/docs/k3s/latest/en/installation/installation-requirements/ +# - rule: allow +# to_port: 6443 +# protocol: tcp +# comment: 'k8s-api-server' +# from_ip: 192.168.2.0/24 +# - rule: allow +# to_port: 2379 +# protocol: tcp +# comment: 'k8s-embedded-etcd' +# from_ip: 192.168.2.0/24 +# - rule: allow +# to_port: 2380 +# protocol: tcp +# comment: 'k8s-embedded-etcd' +# from_ip: 192.168.2.0/24 +# - rule: allow +# to_port: 10250 +# protocol: tcp +# comment: 'k8s-kubelet-metrics' +# from_ip: 192.168.2.0/24 + - rule: allow + comment: 'k3s - alles offen' + from_ip: 0.0.0.0/0 + ### xanmanning.k3s + k3s_state: installed + k3s_airgap: false + k3s_config_file: /etc/rancher/k3s/config.yaml + k3s_build_cluster: true + k3s_install_dir: /usr/local/bin + k3s_etcd_datastore: true + k3s_become: true + k3s_use_experimental: true + k3s_server: + # siehe https://docs.k3s.io/reference/server-config + # cli parameter OHNE -- am anfang + write-kubeconfig-mode: '644' + cluster-cidr: "10.42.0.0/16" + service-cidr: "10.43.0.0/16" + disable: + - traefik + - local-storage # disables local-path-provisioner + + ### mgrote.fluxcd + flux_repo_url: + flux_repo_host: git.mgrote.net + flux_repo_host_port: 2222 + flux_repo_branch: master + flux_repo_url_complete: ssh://gitea@git.mgrote.net:2222/mg/k3s-fluxcd.git + flux_install_host: k3s1.grote.lan + flux_homedir: /home/flux + flux_path_ssh_dir: /home/flux/.ssh + flux_user_group: flux + flux_user: flux + flux_download_url: https://github.com/fluxcd/flux2/releases/download/v0.35.0/flux_0.35.0_linux_amd64.tar.gz + flux_path_bin: /usr/local/sbin + flux_path_ssh_id_file: id_rsa + flux_ssh_key_format: ed25519 + kubeconfig: /etc/rancher/k3s/k3s.yaml + flux_sync_interval: 1m diff --git a/host_vars/docker10.grote.lan.yml b/host_vars/docker10.grote.lan.yml index 29ad809f..9c540f2c 100644 --- a/host_vars/docker10.grote.lan.yml +++ b/host_vars/docker10.grote.lan.yml @@ -28,8 +28,6 @@ ### mgrote.restic restic_folders_to_backup: "/ /var/lib/docker /mnt/httpd" # --one-file-system ist gesetzt, also werden weitere Dateisysteme nicht eingeschlossen, es sei denn sie werden hier explizit angegeben - # die vars noch in rolle übertragen mit beispiel - ### mgrote.docker-compose-inline compose_owner: "docker-user" compose_group: "docker-user" diff --git a/host_vars/k3s1.grote.lan.yml b/host_vars/k3s1.grote.lan.yml new file mode 100644 index 00000000..1adced1d --- /dev/null +++ b/host_vars/k3s1.grote.lan.yml @@ -0,0 +1,3 @@ +--- + ### xanmanning.k3s + k3s_control_node: true diff --git a/host_vars/k3s2.grote.lan.yml b/host_vars/k3s2.grote.lan.yml new file mode 100644 index 00000000..1adced1d --- /dev/null +++ b/host_vars/k3s2.grote.lan.yml @@ -0,0 +1,3 @@ +--- + ### xanmanning.k3s + k3s_control_node: true diff --git a/host_vars/k3s3.grote.lan.yml b/host_vars/k3s3.grote.lan.yml new file mode 100644 index 00000000..1adced1d --- /dev/null +++ b/host_vars/k3s3.grote.lan.yml @@ -0,0 +1,3 @@ +--- + ### xanmanning.k3s + k3s_control_node: true diff --git a/host_vars/pve5.grote.lan.yml b/host_vars/pve5.grote.lan.yml index fd2454c3..d2b2b2ef 100644 --- a/host_vars/pve5.grote.lan.yml +++ b/host_vars/pve5.grote.lan.yml @@ -158,8 +158,8 @@ ### mgrote.cv4pve-autosnap cv4pve_api_user: root@pam!cv4pve-autosnap - cv4pve_api_token: "{{ lookup('keepass', 'cv4pve_api_token', 'password') }}" - cv4pve_vmid: all,-127 + cv4pve_api_token: "{{ lookup('keepass', 'cv4pve_api_token_pve2', 'password') }}" + cv4pve_vmid: all,-127,-112,-116,-101,-102,-106 # k3s* wieder in snaps aktivieren! cv4pve_keep_snapshots: 5 cv4pve_dl_link: "https://github.com/Corsinvest/cv4pve-autosnap/releases/download/v1.10.0/cv4pve-autosnap-linux-x64.zip" diff --git a/inventory b/inventory index c098483d..22edc3be 100644 --- a/inventory +++ b/inventory @@ -18,6 +18,15 @@ all: docker: hosts: docker10.grote.lan: + k3s: + hosts: + k3s1.grote.lan: + k3s2.grote.lan: + k3s3.grote.lan: + vmtest: + hosts: + vm-test-2004.grote.lan: + vm-test-2204.grote.lan: proxmox: hosts: pve5.grote.lan: @@ -26,6 +35,10 @@ all: proxmoxprod: hosts: pve5.grote.lan: + children: + proxmoxtest: + hosts: + pve5-test.grote.lan: physical: hosts: pve5.grote.lan: @@ -43,6 +56,9 @@ all: gitea.grote.lan: dnsmasq.grote.lan: docker10.grote.lan: + k3s1.grote.lan: + k3s2.grote.lan: + k3s3.grote.lan: test: hosts: vm-test-2004.grote.lan: diff --git a/keepass_db.kdbx b/keepass_db.kdbx index 83832b7c531389526ed52e9f7125d46fd2149314..7f30bb64ce21e76fe3deef80446bd964b2935c78 100644 GIT binary patch literal 70910 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 My!=nsCn4-~S!)jZ0RR91 literal 65454 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 + This pull request is no longer marked for closure. + + closeComment: >- + This pull request has been closed due to inactivity. If you feel this is in error, please reopen the pull request or file a new PR with the relevant details. + +issues: + markComment: |- + This issue has been marked 'stale' due to lack of recent activity. If there is no further activity, the issue will be closed in another 30 days. Thank you for your contribution! + + Please read [this blog post](https://www.jeffgeerling.com/blog/2020/enabling-stale-issue-bot-on-my-github-repositories) to see the reasons why I mark issues as stale. + + unmarkComment: >- + This issue is no longer marked for closure. + + closeComment: >- + This issue has been closed due to inactivity. If you feel this is in error, please reopen the issue or file a new issue with the relevant details. diff --git a/roles/geerlingguy.nfs_server/.github/workflows/ci.yml b/roles/geerlingguy.nfs_server/.github/workflows/ci.yml new file mode 100644 index 00000000..a250d28e --- /dev/null +++ b/roles/geerlingguy.nfs_server/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +--- +name: CI +'on': + pull_request: + push: + branches: + - master + schedule: + - cron: "30 1 * * 3" + +defaults: + run: + working-directory: 'geerlingguy.nfs' + +jobs: + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Check out the codebase. + uses: actions/checkout@v2 + with: + path: 'geerlingguy.nfs' + + - name: Set up Python 3. + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install test dependencies. + run: pip3 install yamllint + + - name: Lint code. + run: | + yamllint . + + molecule: + name: Molecule + runs-on: ubuntu-latest + strategy: + matrix: + distro: + - rockylinux8 + - centos7 + - ubuntu2004 + + steps: + - name: Check out the codebase. + uses: actions/checkout@v2 + with: + path: 'geerlingguy.nfs' + + - name: Set up Python 3. + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install test dependencies. + run: pip3 install ansible molecule[docker] docker + + - name: Run Molecule tests. + run: molecule test + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + MOLECULE_DISTRO: ${{ matrix.distro }} diff --git a/roles/geerlingguy.nfs_server/.github/workflows/release.yml b/roles/geerlingguy.nfs_server/.github/workflows/release.yml new file mode 100644 index 00000000..d166c7b1 --- /dev/null +++ b/roles/geerlingguy.nfs_server/.github/workflows/release.yml @@ -0,0 +1,40 @@ +--- +# This workflow requires a GALAXY_API_KEY secret present in the GitHub +# repository or organization. +# +# See: https://github.com/marketplace/actions/publish-ansible-role-to-galaxy +# See: https://github.com/ansible/galaxy/issues/46 + +name: Release +'on': + push: + tags: + - '*' + +defaults: + run: + working-directory: 'geerlingguy.nfs' + +jobs: + + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Check out the codebase. + uses: actions/checkout@v2 + with: + path: 'geerlingguy.nfs' + + - name: Set up Python 3. + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install Ansible. + run: pip3 install ansible-core + + - name: Trigger a new import on Galaxy. + run: >- + ansible-galaxy role import --api-key ${{ secrets.GALAXY_API_KEY }} + $(echo ${{ github.repository }} | cut -d/ -f1) $(echo ${{ github.repository }} | cut -d/ -f2) diff --git a/roles/geerlingguy.nfs_server/.gitignore b/roles/geerlingguy.nfs_server/.gitignore new file mode 100644 index 00000000..8840c8f0 --- /dev/null +++ b/roles/geerlingguy.nfs_server/.gitignore @@ -0,0 +1,5 @@ +*.retry +*/__pycache__ +*.pyc +.cache + diff --git a/roles/geerlingguy.nfs_server/.yamllint b/roles/geerlingguy.nfs_server/.yamllint new file mode 100644 index 00000000..76a383c6 --- /dev/null +++ b/roles/geerlingguy.nfs_server/.yamllint @@ -0,0 +1,10 @@ +--- +extends: default + +rules: + line-length: + max: 120 + level: warning + +ignore: | + .github/stale.yml diff --git a/roles/geerlingguy.nfs_server/LICENSE b/roles/geerlingguy.nfs_server/LICENSE new file mode 100644 index 00000000..4275cf3c --- /dev/null +++ b/roles/geerlingguy.nfs_server/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2017 Jeff Geerling + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/roles/geerlingguy.nfs_server/README.md b/roles/geerlingguy.nfs_server/README.md new file mode 100644 index 00000000..97e7f042 --- /dev/null +++ b/roles/geerlingguy.nfs_server/README.md @@ -0,0 +1,40 @@ +# Ansible Role: NFS + +[![CI](https://github.com/geerlingguy/ansible-role-nfs/workflows/CI/badge.svg?event=push)](https://github.com/geerlingguy/ansible-role-nfs/actions?query=workflow%3ACI) + +Installs NFS utilities on RedHat/CentOS or Debian/Ubuntu. + +## Requirements + +None. + +## Role Variables + +Available variables are listed below, along with default values (see `defaults/main.yml`): + + nfs_exports: [] + +A list of exports which will be placed in the `/etc/exports` file. See Ubuntu's simple [Network File System (NFS)](https://ubuntu.com/server/docs/service-nfs) guide for more info and examples. (Simple example: `nfs_exports: [ "/home/public *(rw,sync,no_root_squash)" ]`). + + nfs_rpcbind_state: started + nfs_rpcbind_enabled: true + +(RedHat/CentOS/Fedora only) The state of the `rpcbind` service, and whether it should be enabled at system boot. + +## Dependencies + +None. + +## Example Playbook + + - hosts: db-servers + roles: + - { role: geerlingguy.nfs } + +## License + +MIT / BSD + +## Author Information + +This role was created in 2014 by [Jeff Geerling](https://www.jeffgeerling.com/), author of [Ansible for DevOps](https://www.ansiblefordevops.com/). diff --git a/roles/geerlingguy.nfs_server/defaults/main.yml b/roles/geerlingguy.nfs_server/defaults/main.yml new file mode 100644 index 00000000..0bc919fb --- /dev/null +++ b/roles/geerlingguy.nfs_server/defaults/main.yml @@ -0,0 +1,5 @@ +--- +nfs_exports: [] + +nfs_rpcbind_state: started +nfs_rpcbind_enabled: true diff --git a/roles/geerlingguy.nfs_server/handlers/main.yml b/roles/geerlingguy.nfs_server/handlers/main.yml new file mode 100644 index 00000000..4cdcec55 --- /dev/null +++ b/roles/geerlingguy.nfs_server/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: reload nfs + command: 'exportfs -ra' diff --git a/roles/geerlingguy.nfs_server/meta/main.yml b/roles/geerlingguy.nfs_server/meta/main.yml new file mode 100644 index 00000000..c16ec8ba --- /dev/null +++ b/roles/geerlingguy.nfs_server/meta/main.yml @@ -0,0 +1,30 @@ +--- +dependencies: [] + +galaxy_info: + role_name: nfs + author: geerlingguy + description: NFS installation for Linux. + company: "Midwestern Mac, LLC" + license: "license (BSD, MIT)" + min_ansible_version: 2.4 + platforms: + - name: EL + versions: + - all + - name: Fedora + versions: + - all + - name: Debian + versions: + - all + - name: Ubuntu + versions: + - all + galaxy_tags: + - system + - nfs + - filesystem + - share + - nfsv4 + - efs diff --git a/roles/geerlingguy.nfs_server/molecule/default/converge.yml b/roles/geerlingguy.nfs_server/molecule/default/converge.yml new file mode 100644 index 00000000..ee8ce1da --- /dev/null +++ b/roles/geerlingguy.nfs_server/molecule/default/converge.yml @@ -0,0 +1,13 @@ +--- +- name: Converge + hosts: all + become: true + + pre_tasks: + - name: Update apt cache. + apt: update_cache=yes cache_valid_time=600 + when: ansible_os_family == 'Debian' + changed_when: false + + roles: + - role: geerlingguy.nfs diff --git a/roles/geerlingguy.nfs_server/molecule/default/molecule.yml b/roles/geerlingguy.nfs_server/molecule/default/molecule.yml new file mode 100644 index 00000000..44ed1958 --- /dev/null +++ b/roles/geerlingguy.nfs_server/molecule/default/molecule.yml @@ -0,0 +1,18 @@ +--- +role_name_check: 1 +dependency: + name: galaxy +driver: + name: docker +platforms: + - name: instance + image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}-ansible:latest" + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: true +provisioner: + name: ansible + playbooks: + converge: ${MOLECULE_PLAYBOOK:-converge.yml} diff --git a/roles/geerlingguy.nfs_server/tasks/main.yml b/roles/geerlingguy.nfs_server/tasks/main.yml new file mode 100644 index 00000000..41000309 --- /dev/null +++ b/roles/geerlingguy.nfs_server/tasks/main.yml @@ -0,0 +1,36 @@ +--- +# Include variables and define needed variables. +- name: Include OS-specific variables. + include_vars: "{{ ansible_os_family }}.yml" + +- name: Include overrides specific to Fedora. + include_vars: Fedora.yml + when: + - ansible_os_family == 'RedHat' + - ansible_distribution == "Fedora" + +# Setup/install tasks. +- include_tasks: setup-RedHat.yml + when: ansible_os_family == 'RedHat' + +- include_tasks: setup-Debian.yml + when: ansible_os_family == 'Debian' + +- name: Ensure directories to export exist + file: # noqa 208 + path: "{{ item.strip().split()[0] }}" + state: directory + with_items: "{{ nfs_exports }}" + +- name: Copy exports file. + template: + src: exports.j2 + dest: /etc/exports + owner: root + group: root + mode: 0644 + notify: reload nfs + +- name: Ensure nfs is running. + service: "name={{ nfs_server_daemon }} state=started enabled=yes" + when: nfs_exports|length diff --git a/roles/geerlingguy.nfs_server/tasks/setup-Debian.yml b/roles/geerlingguy.nfs_server/tasks/setup-Debian.yml new file mode 100644 index 00000000..829b614d --- /dev/null +++ b/roles/geerlingguy.nfs_server/tasks/setup-Debian.yml @@ -0,0 +1,7 @@ +--- +- name: Ensure NFS utilities are installed. + apt: + name: + - nfs-common + - nfs-kernel-server + state: present diff --git a/roles/geerlingguy.nfs_server/tasks/setup-RedHat.yml b/roles/geerlingguy.nfs_server/tasks/setup-RedHat.yml new file mode 100644 index 00000000..d7198af3 --- /dev/null +++ b/roles/geerlingguy.nfs_server/tasks/setup-RedHat.yml @@ -0,0 +1,9 @@ +--- +- name: Ensure NFS utilities are installed. + package: name=nfs-utils state=present + +- name: Ensure rpcbind is running as configured. + service: + name: rpcbind + state: "{{ nfs_rpcbind_state }}" + enabled: "{{ nfs_rpcbind_enabled }}" diff --git a/roles/geerlingguy.nfs_server/templates/exports.j2 b/roles/geerlingguy.nfs_server/templates/exports.j2 new file mode 100644 index 00000000..fa27c554 --- /dev/null +++ b/roles/geerlingguy.nfs_server/templates/exports.j2 @@ -0,0 +1,13 @@ +# /etc/exports: the access control list for filesystems which may be exported +# to NFS clients. See exports(5). +# +# Example for NFSv2 and NFSv3: +# /srv/homes hostname1(rw,sync,no_subtree_check) hostname2(ro,sync,no_subtree_check) +# +# Example for NFSv4: +# /srv/nfs4 gss/krb5i(rw,sync,fsid=0,crossmnt,no_subtree_check) +# /srv/nfs4/homes gss/krb5i(rw,sync,no_subtree_check) +# +{% for export in nfs_exports %} +{{ export }} +{% endfor %} \ No newline at end of file diff --git a/roles/geerlingguy.nfs_server/vars/Debian.yml b/roles/geerlingguy.nfs_server/vars/Debian.yml new file mode 100644 index 00000000..d7ee6b5d --- /dev/null +++ b/roles/geerlingguy.nfs_server/vars/Debian.yml @@ -0,0 +1,2 @@ +--- +nfs_server_daemon: nfs-kernel-server diff --git a/roles/geerlingguy.nfs_server/vars/Fedora.yml b/roles/geerlingguy.nfs_server/vars/Fedora.yml new file mode 100644 index 00000000..05388584 --- /dev/null +++ b/roles/geerlingguy.nfs_server/vars/Fedora.yml @@ -0,0 +1,2 @@ +--- +nfs_server_daemon: nfs-server diff --git a/roles/geerlingguy.nfs_server/vars/RedHat.yml b/roles/geerlingguy.nfs_server/vars/RedHat.yml new file mode 100644 index 00000000..05388584 --- /dev/null +++ b/roles/geerlingguy.nfs_server/vars/RedHat.yml @@ -0,0 +1,2 @@ +--- +nfs_server_daemon: nfs-server diff --git a/roles/mgrote.fluxcd/LICENSE b/roles/mgrote.fluxcd/LICENSE new file mode 100644 index 00000000..e72bfdda --- /dev/null +++ b/roles/mgrote.fluxcd/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/roles/mgrote.fluxcd/README.md b/roles/mgrote.fluxcd/README.md new file mode 100644 index 00000000..fe50b027 --- /dev/null +++ b/roles/mgrote.fluxcd/README.md @@ -0,0 +1,3 @@ +# mgrote.fluxcd + +Diese Rolle installiert Flux innerhalb des Culsters und generiert einen SSH-Key der im Gitea-Repo als Deploytoken MIT Schreibrechten abgelegt werden muss. diff --git a/roles/mgrote.fluxcd/defaults/main.yml b/roles/mgrote.fluxcd/defaults/main.yml new file mode 100644 index 00000000..486e7a98 --- /dev/null +++ b/roles/mgrote.fluxcd/defaults/main.yml @@ -0,0 +1,17 @@ +--- + flux_repo_url: + flux_repo_host: git.mgrote.net + flux_repo_host_port: 2222 + flux_repo_branch: master + flux_repo_url_complete: ssh://gitea@git.mgrote.net:2222/mg/k3s-fluxcd.git + flux_install_host: k3s1.grote.lan + flux_homedir: /home/flux + flux_path_ssh_dir: /home/flux/.ssh + flux_user_group: flux + flux_user: flux + flux_download_url: https://github.com/fluxcd/flux2/releases/download/v0.35.0/flux_0.35.0_linux_amd64.tar.gz + flux_path_bin: /usr/local/sbin + flux_path_ssh_id_file: id_rsa + flux_ssh_key_format: ed25519 + kubeconfig: /etc/rancher/k3s/k3s.yaml + flux_sync_interval: 1m diff --git a/roles/mgrote.fluxcd/tasks/main.yml b/roles/mgrote.fluxcd/tasks/main.yml new file mode 100644 index 00000000..4ccf14ce --- /dev/null +++ b/roles/mgrote.fluxcd/tasks/main.yml @@ -0,0 +1,97 @@ +--- + - name: include user tasks + include_tasks: user.yml + + - name: check if flux is installed + command: which flux + changed_when: false + failed_when: flux_installed.rc not in [0,1] + register: flux_installed + + - name: download flux binary + ansible.builtin.unarchive: + src: "{{ flux_download_url }}" + dest: "{{ flux_path_bin }}" + mode: 0755 + owner: "{{ flux_user }}" + group: "{{ flux_user_group }}" + remote_src: yes + creates: "{{ flux_path_bin }}/flux" + when: flux_installed.rc not in [ 0 ] + + - name: install bash-completion packages + become: yes + ansible.builtin.package: + name: bash-completion + state: present + + - name: activate autocompletion for flux + become: yes + ansible.builtin.shell: + cmd: "flux completion bash | sudo tee /etc/bash_completion.d/flux" + args: + creates: /etc/bash_completion.d/flux + + - name: install flux + block: + - name: ensure .ssh directory exists + ansible.builtin.file: + path: "{{ flux_path_ssh_dir }}" + owner: "{{ flux_user }}" + group: "{{ flux_user_group }}" + state: directory + + - name: check if OpenSSH keypair exists + stat: + path: "{{ flux_path_ssh_dir }}/{{ flux_path_ssh_id_file }}" + register: ssh_exist + + - name: generate SSH keypair + community.crypto.openssh_keypair: + path: "{{ flux_path_ssh_dir }}/{{ flux_path_ssh_id_file }}" + owner: "{{ flux_user }}" + group: "{{ flux_user_group }}" + type: "{{ flux_ssh_key_format }}" + when: ssh_exist.stat.exists == False + register: create_ssh_key + + - name: get publickey + ansible.builtin.command: cat "{{ flux_path_ssh_dir }}/{{ flux_path_ssh_id_file }}.pub" + when: create_ssh_key.changed + register: ssh_public_key + + - name: show publickey + debug: + msg: "{{ ssh_public_key.stdout }}" + when: create_ssh_key.changed + + - name: copy publickey to gitea as deploy-key WITH write-permissions! + ansible.builtin.pause: + prompt: "Make sure the key is saved!" + when: create_ssh_key.changed + + - name: set permissions for $kubeconfig + ansible.builtin.file: + path: "{{ kubeconfig }}" + owner: root + group: root + mode: '0644' + + - name: template bootstrap script + ansible.builtin.template: + src: bootstrap.sh + dest: "{{ flux_homedir }}/bootstrap.sh" + mode: 0544 + owner: "{{ flux_user }}" + group: "{{ flux_user_group }}" + register: bootstrap + + - name: flux install + ansible.builtin.shell: "{{ flux_homedir }}/bootstrap.sh" + become_user: "{{ flux_user }}" + when: bootstrap.changed + # der ganze block nur auf enem host + run_once: true + when: ansible_host == flux_install_host + +# podeinfo: http://k3s1.grote.lan:30778 diff --git a/roles/mgrote.fluxcd/tasks/user.yml b/roles/mgrote.fluxcd/tasks/user.yml new file mode 100644 index 00000000..83c47487 --- /dev/null +++ b/roles/mgrote.fluxcd/tasks/user.yml @@ -0,0 +1,18 @@ +--- + - name: ensure group exists + become: true + ansible.builtin.group: + name: "{{ flux_user_group }}" + state: present + when: + - flux_user_group is defined + + - name: ensure user exists + become: true + ansible.builtin.user: + name: "{{ flux_user }}" + group: "{{ flux_user_group }}" + create_home: no + when: + - flux_user_group is defined + - flux_user is defined diff --git a/roles/mgrote.fluxcd/templates/bootstrap.sh b/roles/mgrote.fluxcd/templates/bootstrap.sh new file mode 100644 index 00000000..abf0df30 --- /dev/null +++ b/roles/mgrote.fluxcd/templates/bootstrap.sh @@ -0,0 +1,4 @@ +#!/bin/bash +ssh-keyscan -p {{ flux_repo_host_port }} {{ flux_repo_host }} > {{ flux_path_ssh_dir }}/known_hosts + +flux bootstrap git --url={{ flux_repo_url_complete }} --interval={{ flux_sync_interval }} --branch={{ flux_repo_branch }} --path=cluster --kubeconfig {{ kubeconfig }} --private-key-file {{ flux_path_ssh_dir }}/{{ flux_path_ssh_id_file }} --silent diff --git a/roles/mgrote.k8s_autocompletion/LICENSE b/roles/mgrote.k8s_autocompletion/LICENSE new file mode 100644 index 00000000..e72bfdda --- /dev/null +++ b/roles/mgrote.k8s_autocompletion/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/roles/mgrote.k8s_autocompletion/README.md b/roles/mgrote.k8s_autocompletion/README.md new file mode 100644 index 00000000..367b9a96 --- /dev/null +++ b/roles/mgrote.k8s_autocompletion/README.md @@ -0,0 +1,5 @@ +## mgrote.kubectl_autocompletion + +### Beschreibung + +Aktiviere kubectl autocompletion: https://kubernetes.io/docs/tasks/tools/included/optional-kubectl-configs-bash-linux/ diff --git a/roles/mgrote.k8s_autocompletion/tasks/main.yml b/roles/mgrote.k8s_autocompletion/tasks/main.yml new file mode 100644 index 00000000..1502f9f5 --- /dev/null +++ b/roles/mgrote.k8s_autocompletion/tasks/main.yml @@ -0,0 +1,13 @@ +--- + - name: install bash-completion packages + become: yes + ansible.builtin.package: + name: bash-completion + state: present + + - name: activate autocompletion for kubectl + become: yes + ansible.builtin.shell: + cmd: "kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl" + args: + creates: /etc/bash_completion.d/kubectl diff --git a/roles/mgrote.k8s_misc/LICENSE b/roles/mgrote.k8s_misc/LICENSE new file mode 100644 index 00000000..e72bfdda --- /dev/null +++ b/roles/mgrote.k8s_misc/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/roles/mgrote.k8s_misc/README.md b/roles/mgrote.k8s_misc/README.md new file mode 100644 index 00000000..41d54ae9 --- /dev/null +++ b/roles/mgrote.k8s_misc/README.md @@ -0,0 +1,3 @@ +# mgrote.k8s_misc + +Diese Rolle hinterlegt die Kubeconfig-Variable in der `.bashrc` von `root`. diff --git a/roles/mgrote.k8s_misc/tasks/main.yml b/roles/mgrote.k8s_misc/tasks/main.yml new file mode 100644 index 00000000..9393cade --- /dev/null +++ b/roles/mgrote.k8s_misc/tasks/main.yml @@ -0,0 +1,7 @@ +--- + - name: set $KUBECONFIG for root + ansible.builtin.blockinfile: + path: /root/.bashrc + block: | + export KUBECONFIG=/etc/rancher/k3s/k3s.yaml + marker: "# {mark} ANSIBLE MANAGED BLOCK - K3S" diff --git a/roles/mgrote.sealed-secrets/LICENSE b/roles/mgrote.sealed-secrets/LICENSE new file mode 100644 index 00000000..e72bfdda --- /dev/null +++ b/roles/mgrote.sealed-secrets/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/roles/mgrote.sealed-secrets/README.md b/roles/mgrote.sealed-secrets/README.md new file mode 100644 index 00000000..807f747f --- /dev/null +++ b/roles/mgrote.sealed-secrets/README.md @@ -0,0 +1,49 @@ +# mgrote.sealed-secrets + +Diese Rolle installiert das kubeseal-Binary und hinterlegt den Private-Key im Cluster. + +Der Key ist im Keepass im Eintrag unter "Notes" abgelegt. Sollten die Secrets neu verschlüsselt werden ist hier wieder der aktuelle Private-Key abzulegen. + +Siehe: https://github.com/bitnami-labs/sealed-secrets#how-can-i-do-a-backup-of-my-sealedsecrets + +## Backup + +`kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml >main.key` + +## Restore + +``` +kubectl apply -f main.key +kubectl delete pod -n kube-system -l name=sealed-secrets-controller +``` + + +## Erstellen eines verschlüsselten Secrets + +- Wichtig ist "stringData", wird nur "data" verwendet ist der Inhalt base64 zu enkodieren. + +1. lege Secret mit Klartext VALUE als Datei() an + ``` + kind: Secret + apiVersion: v1 + metadata: + name: NAME_DES_SECRETS + namespace: drone + stringData: + ICH_BIN_DER VARIABLEN_NAME: ICH_BIN_DAS_PASSWORT + ``` + 2. diese Datei mit kubeseal verschlüsseln + ``` + cat | kubeseal --format=yaml + ``` + 3. den Inhalt dann als Secret im Repo ablegen ablegen + +## Verwenden des Secrets + +``` +- name: DRONE_RPC_SECRET + valueFrom: + secretKeyRef: + name: NAME_DES_SECRETS + key: ICH_BIN_DER VARIABLEN_NAME +``` diff --git a/roles/mgrote.sealed-secrets/defaults/main.yml b/roles/mgrote.sealed-secrets/defaults/main.yml new file mode 100644 index 00000000..4bcdbed4 --- /dev/null +++ b/roles/mgrote.sealed-secrets/defaults/main.yml @@ -0,0 +1,8 @@ +--- + sealed_secrets_homedir: /home/sealed_secrets + sealed_secrets_user_group: sealed_secrets + sealed_secrets_user: sealed_secrets + kubeseal_download_url: "https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.19.1/kubeseal-0.19.1-linux-amd64.tar.gz" + kubeseal_path_bin: /usr/local/sbin + kubeconfig: /etc/rancher/k3s/k3s.yaml + sealed_secrets_keepass_entry_name: "{{ lookup('keepass', 'k3s-sealed-secrets-private-key', 'notes') }}" diff --git a/roles/mgrote.sealed-secrets/tasks/import.yml b/roles/mgrote.sealed-secrets/tasks/import.yml new file mode 100644 index 00000000..e24aeb8c --- /dev/null +++ b/roles/mgrote.sealed-secrets/tasks/import.yml @@ -0,0 +1,29 @@ +--- + - name: check if private key exists + ansible.builtin.command: kubectl get secrets sealed-secrets-key9mpfq -n kube-system + register: key + ignore_errors: yes + changed_when: false + + - name: Template private key file + ansible.builtin.template: + src: private.key.j2 + dest: /root/private.key + owner: root + group: root + mode: '0400' + when: key.rc not in [ 0 ] + no_log: True + + - name: apply private key + ansible.builtin.command: kubectl apply -f /root/private.key + when: key.rc not in [ 0 ] + + - name: remove old pod + ansible.builtin.command: kubectl delete pod -n kube-system -l name=sealed-secrets-controller + when: key.rc not in [ 0 ] + + - name: remove private key file + ansible.builtin.file: + path: /root/private.key + state: absent diff --git a/roles/mgrote.sealed-secrets/tasks/main.yml b/roles/mgrote.sealed-secrets/tasks/main.yml new file mode 100644 index 00000000..247adfe9 --- /dev/null +++ b/roles/mgrote.sealed-secrets/tasks/main.yml @@ -0,0 +1,26 @@ +--- + - name: include user tasks + include_tasks: user.yml + + - name: check if kubeseal is installed + command: which kubeseal + changed_when: false + failed_when: kubeseal_installed.rc not in [0,1] + register: kubeseal_installed + + - name: download sealed_secrets binary + ansible.builtin.unarchive: + src: "{{ kubeseal_download_url }}" + dest: "{{ kubeseal_path_bin }}" + mode: 0755 + owner: "{{ sealed_secrets_user }}" + group: "{{ sealed_secrets_user_group }}" + remote_src: yes + creates: "{{ kubeseal_path_bin }}/kubeseal" + when: kubeseal_installed.rc not in [ 0 ] + + - name: include import tasks + include_tasks: import.yml + # der ganze block nur auf enem host + run_once: true + when: ansible_host == flux_install_host diff --git a/roles/mgrote.sealed-secrets/tasks/user.yml b/roles/mgrote.sealed-secrets/tasks/user.yml new file mode 100644 index 00000000..73a30083 --- /dev/null +++ b/roles/mgrote.sealed-secrets/tasks/user.yml @@ -0,0 +1,18 @@ +--- + - name: ensure group exists + become: true + ansible.builtin.group: + name: "{{ sealed_secrets_user_group }}" + state: present + when: + - sealed_secrets_user_group is defined + + - name: ensure user exists + become: true + ansible.builtin.user: + name: "{{ sealed_secrets_user }}" + group: "{{ sealed_secrets_user_group }}" + create_home: no + when: + - sealed_secrets_user_group is defined + - sealed_secrets_user is defined diff --git a/roles/mgrote.sealed-secrets/templates/private.key.j2 b/roles/mgrote.sealed-secrets/templates/private.key.j2 new file mode 100644 index 00000000..db25cc6b --- /dev/null +++ b/roles/mgrote.sealed-secrets/templates/private.key.j2 @@ -0,0 +1 @@ +{{ sealed_secrets_keepass_entry_name }} diff --git a/roles/pandemonium1986.ansible-role-k9s/defaults/main.yml b/roles/pandemonium1986.ansible-role-k9s/defaults/main.yml new file mode 100644 index 00000000..8f2231da --- /dev/null +++ b/roles/pandemonium1986.ansible-role-k9s/defaults/main.yml @@ -0,0 +1,4 @@ +--- +k9s_cache_path: "/var/cache/github" +k9s_installation_path: "/opt/github/k9s" +k9s_version: "v0.24.15" diff --git a/roles/pandemonium1986.ansible-role-k9s/tasks/main.yml b/roles/pandemonium1986.ansible-role-k9s/tasks/main.yml new file mode 100644 index 00000000..5ac7fc37 --- /dev/null +++ b/roles/pandemonium1986.ansible-role-k9s/tasks/main.yml @@ -0,0 +1,28 @@ +--- + - name: Ensure k9s directories are created + file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ k9s_cache_path }}" + - "{{ k9s_installation_path }}" + + - name: Ensure k9s is installed + get_url: + url: "{{ source_url }}/{{ github_namespace }}/releases/download/{{ k9s_version }}/{{ asset_name }}" + dest: "{{ k9s_cache_path }}" + mode: "0755" + + - name: Unarchive k9s archive into k9s_installation_path + unarchive: + src: "{{ k9s_cache_path }}/{{ asset_name }}" + dest: "{{ k9s_installation_path }}" + remote_src: true + mode: "0755" + + - name: Ensure k9s link is created + file: + src: "{{ k9s_installation_path }}/{{ binary_name }}" + dest: "/usr/local/bin/{{ binary_name }}" + state: link diff --git a/roles/pandemonium1986.ansible-role-k9s/vars/main.yml b/roles/pandemonium1986.ansible-role-k9s/vars/main.yml new file mode 100644 index 00000000..ed6af89e --- /dev/null +++ b/roles/pandemonium1986.ansible-role-k9s/vars/main.yml @@ -0,0 +1,5 @@ +--- +source_url: "https://github.com" +github_namespace: "derailed/k9s" +asset_name: "k9s_Linux_x86_64.tar.gz" +binary_name: "k9s" diff --git a/roles/xanmanning.k3s/.ansible-lint b/roles/xanmanning.k3s/.ansible-lint new file mode 100644 index 00000000..32133767 --- /dev/null +++ b/roles/xanmanning.k3s/.ansible-lint @@ -0,0 +1,4 @@ +--- + +skip_list: + - role-name diff --git a/roles/xanmanning.k3s/.devcontainer/Dockerfile b/roles/xanmanning.k3s/.devcontainer/Dockerfile new file mode 100644 index 00000000..25f84d5c --- /dev/null +++ b/roles/xanmanning.k3s/.devcontainer/Dockerfile @@ -0,0 +1,26 @@ +ARG VARIANT=focal +FROM ubuntu:${VARIANT} + +COPY molecule/requirements.txt /tmp/molecule/requirements.txt +COPY requirements.txt /tmp/requirements.txt + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install curl git python3-dev python3-pip \ + python3-venv shellcheck sudo unzip docker.io jq \ + && curl -L \ + "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ + -o /usr/bin/kubectl \ + && chmod +x /usr/bin/kubectl \ + && python3 -m pip install pip --upgrade \ + && python3 -m pip install -r /tmp/molecule/requirements.txt + +RUN useradd -s /bin/bash -m vscode && \ + usermod -aG docker vscode && \ + echo 'vscode ALL=(ALL:ALL) NOPASSWD: ALL' > /etc/sudoers.d/vscode && \ + echo 'source /etc/bash_completion.d/git-prompt' >> /home/vscode/.bashrc && \ + echo 'sudo chown vscode /var/run/docker-host.sock' >> /home/vscode/.bashrc && \ + echo 'export PS1="${PS1:0:-1}\[\033[38;5;196m\]$(__git_ps1)\[$(tput sgr0)\] "' >> /home/vscode/.bashrc + +RUN ln -s /var/run/docker-host.sock /var/run/docker.sock + +USER vscode diff --git a/roles/xanmanning.k3s/.devcontainer/devcontainer.json b/roles/xanmanning.k3s/.devcontainer/devcontainer.json new file mode 100644 index 00000000..b442a63d --- /dev/null +++ b/roles/xanmanning.k3s/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +{ + "name": "Ubuntu", + "build": { + "context": "..", + "dockerfile": "Dockerfile", + "args": { "VARIANT": "focal" } + }, + + "settings": { + "terminal.integrated.profiles.linux": { + "bash (login)": { + "path": "/bin/bash", + "args": ["-l"] + } + } + }, + + "extensions": [ + "ms-azuretools.vscode-docker", + "redhat.vscode-yaml" + ], + + "mounts": [ + "source=/var/run/docker.sock,target=/var/run/docker-host.sock,type=bind" + ], + + "remoteUser": "vscode" +} diff --git a/roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/bug_report.md b/roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..5a1862ce --- /dev/null +++ b/roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,55 @@ +--- +name: Bug report +about: Create a report to help us improve +--- + + + + +### Summary + + + +### Issue Type + +- Bug Report + +### Controller Environment and Configuration + + + + + + + +```text + +``` + +### Steps to Reproduce + + + + + +```yaml + +``` + +### Expected Result + + + +```text + +``` + +### Actual Result + + + + + +```text + +``` diff --git a/roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/config.yml b/roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..8b76b046 --- /dev/null +++ b/roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,3 @@ +--- + +blank_issues_enabled: true diff --git a/roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/feature_request.md b/roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..d3f15a67 --- /dev/null +++ b/roles/xanmanning.k3s/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,33 @@ +--- +name: Feature request +about: Suggest an idea for this project +--- + + + + +### Summary + + + +### Issue Type + +- Feature Request + +### User Story + + + + +_As a_ \ +_I want to_ \ +_So that_ + +### Additional Information + + + + +```yaml + +``` diff --git a/roles/xanmanning.k3s/.github/PULL_REQUEST_TEMPLATE.md b/roles/xanmanning.k3s/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..620f20ef --- /dev/null +++ b/roles/xanmanning.k3s/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,37 @@ +## TITLE + +### Summary + + + + + +### Issue type + + +- Bugfix +- Documentation +- Feature + +### Test instructions + + + +### Acceptance Criteria + + + + + +### Additional Information + + + + + +```text + +``` diff --git a/roles/xanmanning.k3s/.github/stale.yml b/roles/xanmanning.k3s/.github/stale.yml new file mode 100644 index 00000000..82c0d003 --- /dev/null +++ b/roles/xanmanning.k3s/.github/stale.yml @@ -0,0 +1,18 @@ +--- +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/roles/xanmanning.k3s/.github/workflows/ci.yml b/roles/xanmanning.k3s/.github/workflows/ci.yml new file mode 100644 index 00000000..bafac6da --- /dev/null +++ b/roles/xanmanning.k3s/.github/workflows/ci.yml @@ -0,0 +1,75 @@ +--- + +name: CI +'on': + pull_request: + push: + branches: + - master + - main + - v1_release + schedule: + - cron: "0 1 1 * *" + +defaults: + run: + working-directory: "xanmanning.k3s" + +jobs: + molecule: + name: Molecule + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - distro: geerlingguy/docker-debian11-ansible:latest + scenario: default + prebuilt: 'true' + - distro: geerlingguy/docker-ubuntu2204-ansible:latest + scenario: default + prebuilt: 'true' + - distro: geerlingguy/docker-amazonlinux2-ansible:latest + scenario: default + prebuilt: 'true' + - distro: geerlingguy/docker-ubuntu2004-ansible:latest + scenario: default + prebuilt: 'true' + - distro: geerlingguy/docker-fedora35-ansible:latest + scenario: nodeploy + prebuilt: 'true' + - distro: geerlingguy/docker-fedora34-ansible:latest + scenario: highavailabilitydb + prebuilt: 'true' + - distro: geerlingguy/docker-fedora33-ansible:latest + scenario: autodeploy + - distro: xanmanning/docker-alpine-ansible:3.16 + scenario: highavailabilityetcd + prebuilt: 'false' + - distro: geerlingguy/docker-rockylinux8-ansible:latest + scenario: highavailabilityetcd + prebuilt: 'true' + + steps: + - name: Checkout codebase + uses: actions/checkout@v2 + with: + path: "xanmanning.k3s" + + - name: Set up Python 3 + uses: actions/setup-python@v2 + with: + python-version: "3.x" + + - name: Install test dependencies + run: pip3 install -r molecule/requirements.txt + + - name: Run Molecule tests + run: molecule test --scenario-name "${{ matrix.scenario }}" + # continue-on-error: true + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + MOLECULE_DISTRO: ${{ matrix.distro }} + MOLECULE_PREBUILT: ${{ matrix.prebuilt }} + MOLECULE_DOCKER_COMMAND: ${{ matrix.command }} diff --git a/roles/xanmanning.k3s/.github/workflows/release.yml b/roles/xanmanning.k3s/.github/workflows/release.yml new file mode 100644 index 00000000..22a7d22b --- /dev/null +++ b/roles/xanmanning.k3s/.github/workflows/release.yml @@ -0,0 +1,32 @@ +--- + +name: Release +'on': + push: + tags: + - '*' + +defaults: + run: + working-directory: "xanmanning.k3s" + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout codebase + uses: actions/checkout@v2 + with: + path: "xanmanning.k3s" + + - name: Set up Python 3 + uses: actions/setup-python@v2 + with: + python-version: "3.x" + + - name: Install Ansible + run: pip3 install -r requirements.txt + + - name: Trigger a new import on Galaxy + run: ansible-galaxy role import --api-key ${{ secrets.GALAXY_API_KEY }} $(echo ${{ github.repository }} | cut -d/ -f1) $(echo ${{ github.repository }} | cut -d/ -f2) diff --git a/roles/xanmanning.k3s/.gitignore b/roles/xanmanning.k3s/.gitignore new file mode 100644 index 00000000..8ccd6a52 --- /dev/null +++ b/roles/xanmanning.k3s/.gitignore @@ -0,0 +1,13 @@ +.vagrant +*.retry +VAULT_PASSWORD +VAULT_PASS +.vault_pass +.vault_pass.asc +vagramt/fetch +vagrant/ubuntu-*.log +__pycache__ +ansible.cfg +pyratlabs-issue-dump.txt +.cache +/.idea/ diff --git a/roles/xanmanning.k3s/.yamllint b/roles/xanmanning.k3s/.yamllint new file mode 100644 index 00000000..88276760 --- /dev/null +++ b/roles/xanmanning.k3s/.yamllint @@ -0,0 +1,33 @@ +--- +# Based on ansible-lint config +extends: default + +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + colons: + max-spaces-after: -1 + level: error + commas: + max-spaces-after: -1 + level: error + comments: disable + comments-indentation: disable + document-start: disable + empty-lines: + max: 3 + level: error + hyphens: + level: error + indentation: disable + key-duplicates: enable + line-length: disable + new-line-at-end-of-file: disable + new-lines: + type: unix + trailing-spaces: disable + truthy: disable diff --git a/roles/xanmanning.k3s/CHANGELOG.md b/roles/xanmanning.k3s/CHANGELOG.md new file mode 100644 index 00000000..5bf3aede --- /dev/null +++ b/roles/xanmanning.k3s/CHANGELOG.md @@ -0,0 +1,600 @@ +# Change Log + + + +--- + +## 2022-06-17, v3.2.0 + +### Notable changes + + - feature: added support for alpine #182 + - fix: `k3s_control_token` not working #187 + +## 2022-05-02, v3.1.2 + +### Notable changes + + - fix: molecule tests + +--- + +## 2022-02-18, v3.1.1 + +### Notable changes + + - fix: support nftables for debian 11 + +### Contributors + + - [eaglesemanation](https://github.com/eaglesemanation) + +--- + +## 2022-01-30, v3.1.0 + +### Notable changes + + - feat: use basename of url for items in `k3s_server_manifests_urls` and + `k3s_server_pod_manifests_urls` if filename is not provided #177 + +### Contributors + + - [kossmac](https://github.com/kossmac) + +--- + +## 2022-01-06, v3.0.1 + +### Notable changes + + - fix: adding become to pre checks packages #173 + +### Contributors + + - [xlejo](https://github.com/xlejo) + +--- + +## 2022-01-02, v3.0.0 + +### Notable changes + + - feat: Flattened task filesystem + - feat: Moved some tasks into `vars/` as templated variables + - feat: Airgap installation method added #165 + +### Breaking changes + + - Minimum `python` version on targets is 3.6 + - `k3s_become_for_all` renamed to `k3s_become` + - `k3s_become_for_*` removed. + +### Contributors + + - [crutonjohn](https://github.com/crutonjohn) + +--- + +## 2021-12-23, v2.12.1 + +### Notable changes + + - Fix typo in systemd unit file + +### Contributors + + - [andrewchen5678](https://github.com/andrewchen5678) + +--- + +## 2021-12-20, v2.12.0 + +### Notable changes + + - Fix RockyLinux HA etcd tests + - add Debian 11 test + - Fix Snapshotter in Molecule tests + - Added missing documentation for `k3s_api_url` + - Added option to change K3s updates API url + - Custom environment variables in systemd unit files + - Debian Bullseye support + - Fix HA etcd cluster startup + - Fix rootless for Debian + +### Contributors + + - [janar153](https://github.com/janar153) + +--- + +## 2021-10-10, v2.11.1 + +### Notable changes + + - docs: fixed references to `write-kubeconfig-mode` to set correct permissions #157 + - fix: Flag --delete-local-data has been deprecated #159 + +--- + +## 2021-09-08, v2.11.0 + +### Notable changes + + - docs: example of IPv6 configuration + - feat: checks for s3 backup configuration + - feat: implement config.yaml.d + +### Contributors + + - [onedr0p](https://github.com/onedr0p) + +--- + +## 2021-08-18, v2.10.6 + +### Notable changes + + - Fix: Define registration address from node-ip #142 + +--- + +## 2021-08-14, v2.10.5 + +### Notable changes + + - Add advertised address #139 + +### Contributors + +- [@abelfodil](https://github.com/abelfodil) + +--- + +## 2021-07-24, v2.10.4 + +### Notable changes + +- Updated systemd template to use token when joining a cluster #138 + +--- + +## 2021-07-21, v2.10.3 + +### Notable changes + +- fix: typo #133 +- fix: restore clustering and avoid failure with jinja2_native=true #135 +- fix: do ignore etcd member count when uninstalling #136 + +### Contributors + +- [@Yaro](https://github.com/Yajo) + +--- + +## 2021-06-22, v2.10.2 + +### Notable changes + +- Role is now tested against RockyLinux + +--- + +## 2021-05-30, v2.10.1 + +### Notable changes + +- Case insensitive control node lookup #126 + +### Contributors + +- [@mrobinsn](https://github.com/mrobinsn) + +--- + +## 2021-05-27, v2.10.0 + +### Notable changes + +- Only deploy templates on primary controller #119 +- Allow control plane static pods #120 +- Add support for specifying URLs in templates #124 + +### Contributors + +- [@bjw-s](https://github.com/bjw-s) +- [@onedr0p](https://github.com/onedr0p) + +--- + +## 2021-05-14, v2.9.1 + + + +### Notable changes + + - Documentation, remove references to deprecated configuration techniques #115 + - Bugfix: Templating issue. + +--- + +## 2021-05-13, v2.9.0 + + + +### Notable changes + + - Feature: Support k3s private registry configuration #114 + +### Contributors + + - [@anjia0532](https://github.com/anjia0532) + +--- + +## 2021-05-06, v2.8.5 + +### Notable changes + + - Bugfix: Unmount CSI plugin folder to avoid data lost on uninstall #113 + +### Contributors + + - [@angelnu](https://github.com/angelnu) + +--- + +## 2021-05-01, v2.8.4 + +### Notable changes + + - Fixed issue with draining nodes #112 + +### Contributors + + - [@anjia0532](https://github.com/anjia0532) + +--- + +## 2021-04-18, v2.8.3 + +### Notable changes + + - Typo fix in README.md #110 + - Fixed check mode for cgroup test #111 + - Added check mode into molecule test sequence + - `inventory.yml` is now `blockinfile` + +### Contributors + + - [@bdronneau](https://github.com/bdronneau) + +--- + +## 2021-04-10, v2.8.2 + +### Notable changes + + - #105 - Added Ansible v2.9.16 support + - #102 - Pre-check for cgroup status + +### Known issues + + - As per README.md, you require `ansible` >= 2.9.16 + or `ansible-base` >= 2.10.4. See [#105(comment)](https://github.com/PyratLabs/ansible-role-k3s/issues/105#issuecomment-817182233) + +--- + +## 2021-03-22, v2.8.1 + +### Notable changes + + - #100 - Fixed typo in README.md + +### Contributors + + - [@mbwmbw1337](https://github.com/mbwmbw1337) + +--- + +## 2021-03-14, v2.8.0 + +Happy π day! + +### Notable changes + + - Updated GitHub Actions, resolved linting errors. + - Renamed `k3s_control_node_address` -> `k3s_registration_address` + +### Breaking changes + + - A task has been added to rename `k3s_control_node_address` to + `k3s_registration_address` for any users still using this variable name, + however this might still break something. + +--- + +## 2021-02-28, v2.7.1 + +### Notable changes + + - Bugfix, missing become on cluster token check. + +--- + +## 2021-02-27, v2.7.0 + +### Notable changes + + - Cluster init checks added. + - Tidy up of tasks, failed checks. + - Possible fix for #93 - force draining of nodes added. + +--- + +## 2021-02-27, v2.6.1 + +### Notable changes + + - Bugfix: Templating error for single control plane nodes using Etcd. + - Bugfix: a number of typos fixed. + +--- + +## 2021-02-16, v2.6.0 + +### Notable changes + + - Tidy up of `when` params and `assert` tasks to be more readable. + - Added feature to tweak K3S service dependencies. + - Updated documentation: + - Node labels and component arguments + - systemd config + - Use alternate CNI (Calico example) + +--- + +## 2021-01-31, v2.5.3 + +### Notable changes + + - Bugfix, missing update to minimum ansible version var #91. + +--- + +## 2021-01-30, v2.5.2 + +### Notable changes + + - Bugfix, missing `k3s_start_on_boot` to control `systemd.enabled` added. + +--- + +## 2021-01-30, v2.5.1 + +### Notable changes + + - Added uninstall task to remove hard-linked files #88 + - Fixed missing become for `systemd` operations tasks. #89 + - Added `k3s_start_on_boot` to control `systemd.enabled`. + +--- + +## 2021-01-24, v2.5.0 + +### Notable changes + + - Added support for Ansible >= 2.9.17 #83 + +--- + +## 2021-01-23, v2.4.3 + +### Notable changes + + - Bufgix: Installation hangs on "Check that all nodes to be ready" #84 + +--- + +## 2021-01-10, v2.4.2 + +### Notable changes + + - Bufgix: Docker check still failing on "false" + +--- + +## 2021-01-02, v2.4.1 + +### Notable changes + + - Fixed issue with armv6l (Raspberry Pi Zero W) + - Added path for private repositories config to directory creation list. + +--- + +## 2020-12-21, v2.4.0 + +### Notable changes + + - `k3s_config_dir` derived from `k3s_config_file`, reused throughout the role + to allow for easy removal of "Rancher" references #73. + - `k3s_token_location` has moved to be in `k3s_config_dir`. + - Tasks for creating directories now looped to caputure configuration from + `k3s_server` and `k3s_agent` and ensure directories exist before k3s + starts, see #75. + - Server token collected directly from token file, not symlinked file + (node-token). + - `k3s_runtime_config` defined in `vars/` for validation and overwritten in + tasks for control plane and workers. + - Removed unused references to GitHub API. + - `set_fact` and `command` tasks now use FQCN. + - Check of `ansible_version` in environment check. + - Introduction of target environment checks for #72. + - Fixed bug with non-default listening port not being passed to workers. + - Added ability to put documentation links into validation checks #76. + - Removed the requirement for `jmespath` on the Ansible controller. + - Fixed bug with issue data collection tasks. + +### Breaking changes + + - Ansible minimum version is hard set to v2.10.4 + - `k3s_token_location` has moved to be in `k3s_config_dir` so re-running the + role will create a duplicate file here. + +--- + +## 2020-12-19, v2.3.0 + +### Notable changes + + - Updated k3s uninstall scripts #74 + - Started moving Rancher references to `vars/` as per #73 + +--- + +## 2020-12-19, v2.2.2 + +### Notable changes + + - Fixed typos in documentation. + - Molecule testing pinned to v3.1 due to tests failing. + +--- + +## 2020-12-16, v2.2.1 + +### Notable changes + + - Re-working documentation + - Updated GitHub link, org changed from Rancher to k3s-io. + - Replace deprecated `play_hosts` variable. + +### Breaking changes + + - Moving git branch from `master` to `main`. + +--- + +## 2020-12-12, v2.2.0 + +### Notable changes + + - Use of FQCNs enforced, minimum Ansible version now v2.10 + - `k3s_etcd_datastore` no longer experimental after K3s version v1.19.5+k3s1 + - Docker marked as deprecated for K3s > v1.20.0+k3s1 + +### Breaking changes + + - Use of FQCNs enforced, minimum Ansible version now v2.10 + - Use of Docker requires `k3s_use_unsupported_config` to be `true` after + v1.20.0+k3s1 + +--- + +## 2020-12-05, v2.1.1 + +### Notable changes + + - Fixed link to documentation. + +--- + +## 2020-12-05, v2.1.0 + +### Notable changes + + - Deprecated configuration check built into validation steps. + - Removed duplicated tasks for single node cluster. + - Added documentation providing quickstart examples and common operations. + - Fixed data-dir configuration. + - Some tweaks to rootless. + - Fix draining and removing of nodes. + +### Breaking changes + + - `k3s_token_location` now points to a file location, not a directory. + - `k3s_systemd_unit_directory` renamed to `k3s_systemd_unit_dir` + - Removed `k3s_node_data_dir` as this is now configured with `data-dir` in + `k3s_server` and/or `k3s_agent`. + +### Known issues + + - Rootless is still broken, this is still not supported as a method for + running k3s using this role. + +--- + +## 2020-11-30, v2.0.2 + +### Notable changes + + - Updated issue template and information collection tasks. + +--- + +## 2020-11-30, v2.0.1 + +### Notable changes + + - Fixed a number of typos in the README.md + - Updated the meta/main.yml to put quotes around minimum Ansible version. + +--- + +## 2020-11-29, v2.0.0 + +### Notable changes + + - #64 - Initial release of v2.0.0 of + [ansible-role-k3s](https://github.com/PyratLabs/ansible-role-k3s). + - Minimum supported k3s version now: v1.19.1+k3s1 + - Minimum supported Ansible version now: v2.10.0 + - #62 - Remove all references to the word "master". + - #53 - Move to file-based configuration. + - Refactored to avoid duplication in code and make contribution easier. + - Validation checks moved to using variables defined in `vars/` + +### Breaking changes + +#### File based configuration + +Issue #53 + +With the release of v1.19.1+k3s1, this role has moved to file-based +configuration of k3s. This requires manuall translation of v1 configuration +variables into configuration file format. + +Please see: https://rancher.com/docs/k3s/latest/en/installation/install-options/#configuration-file + +#### Minimum supported k3s version + +As this role now relies on file-based configuration, the v2.x release of this +role will only support v1.19+ of k3s. If you are not in a position to update +k3s you will need to continue using the v1.x release of this role, which will +be supported until March 2021. + +#### Minimum supported ansible version + +This role now only supports Ansible v2.10+, this is because it has moved on to +using FQDNs, with the exception of `set_fact` tasks which have +[been broken](https://github.com/ansible/ansible/issues/72319) and the fixes +have [not yet been backported to v2.10](https://github.com/ansible/ansible/pull/71824). + +The use of FQDNs allows for custom modules to be introduced to override task +behavior. If this role requires a custom ansible module to be introduced then +this can be added as a dependency and targeted specifically by using the +correct FQDN. diff --git a/roles/xanmanning.k3s/CONTRIBUTING.md b/roles/xanmanning.k3s/CONTRIBUTING.md new file mode 100644 index 00000000..da7f6acc --- /dev/null +++ b/roles/xanmanning.k3s/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contribution Guidelines + +Thank you for taking time to contribute to this Ansible role. + +There are a number of ways that you can contribute to this project, not all of +them requiring you to be able to write code. Below is a list of suggested +contributions welcomed by the community: + + - Submit bug reports in GitHub issues + - Comment on bug reports with futher information or suggestions + - Suggest new features + - Create Pull Requests fixing bugs or adding new features + - Update and improve documentation + - Review the role on Ansible Galaxy + - Write a blog post reviewing the role + - Sponsor me. + +## Issue guidelines + +Issues are the best way to capture an bug in the role, or suggest new features. +This is due to issues being visible to the entire community and allows for +other contributors to pick up the work, so is a better communication medium +than email. + +A good bug issue will include as much information as possible about the +environment Ansible is running in, as well as the role configuration. If there +are any relevant pieces of documentation from upstream projects, this should +be included. + +New feature requests are also best captured in issues, these should include +as much relevant information as possible and if possible include a "user story" +(don't sweat if you don't know how to write one). If there are any relevant +pieces of documentation from upstream projects, this should be included. + +## Pull request guidelines + +PRs should only contain 1 issue fix at a time to limit the scope of testing +required. The smaller the scope of the PR, the easier it is for it to be +reviewed. + +PRs should include the keyword `Fixes` before an issue number if the PR will +completely close the issue. This is because automation will close the issue +once the PR is merged. + +PRs are preferred to be merged in as a single commit, so rebasing before +pushing is recommended, however this isn't a strict rule. diff --git a/roles/xanmanning.k3s/LICENSE.txt b/roles/xanmanning.k3s/LICENSE.txt new file mode 100644 index 00000000..af447bb9 --- /dev/null +++ b/roles/xanmanning.k3s/LICENSE.txt @@ -0,0 +1,30 @@ +BSD 3-Clause License + +Copyright (c) 2020, Xan Manning +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/roles/xanmanning.k3s/README.md b/roles/xanmanning.k3s/README.md new file mode 100644 index 00000000..ced9c9c9 --- /dev/null +++ b/roles/xanmanning.k3s/README.md @@ -0,0 +1,412 @@ +# Ansible Role: k3s (v3.x) + +Ansible role for installing [K3S](https://k3s.io/) ("Lightweight +Kubernetes") as either a standalone server or cluster. + +[![CI](https://github.com/PyratLabs/ansible-role-k3s/workflows/CI/badge.svg?event=push)](https://github.com/PyratLabs/ansible-role-k3s/actions?query=workflow%3ACI) + +## Help Wanted! + +Hi! :wave: [@xanmanning](https://github.com/xanmanning) is looking for a new +maintainer to work on this Ansible role. This is because I don't have as much +free time any more and I no longer write Ansible regularly as part of my day +job. If you're interested, get in touch. + +## Release notes + +Please see [Releases](https://github.com/PyratLabs/ansible-role-k3s/releases) +and [CHANGELOG.md](CHANGELOG.md). + +## Requirements + +The host you're running Ansible from requires the following Python dependencies: + + - `python >= 3.6.0` - [See Notes below](#important-note-about-python). + - `ansible >= 2.9.16` or `ansible-base >= 2.10.4` + +You can install dependencies using the requirements.txt file in this repository: +`pip3 install -r requirements.txt`. + +This role has been tested against the following Linux Distributions: + + - Alpine Linux + - Amazon Linux 2 + - Archlinux + - CentOS 8 + - Debian 11 + - Fedora 31 + - Fedora 32 + - Fedora 33 + - openSUSE Leap 15 + - RockyLinux 8 + - Ubuntu 20.04 LTS + +:warning: The v3 releases of this role only supports `k3s >= v1.19`, for +`k3s < v1.19` please consider updating or use the v1.x releases of this role. + +Before upgrading, see [CHANGELOG](CHANGELOG.md) for notifications of breaking +changes. + +## Role Variables + +Since K3s [v1.19.1+k3s1](https://github.com/k3s-io/k3s/releases/tag/v1.19.1%2Bk3s1) +you can now configure K3s using a +[configuration file](https://rancher.com/docs/k3s/latest/en/installation/install-options/#configuration-file) +rather than environment variables or command line arguments. The v2 release of +this role has moved to the configuration file method rather than populating a +systemd unit file with command-line arguments. There may be exceptions that are +defined in [Global/Cluster Variables](#globalcluster-variables), however you will +mostly be configuring k3s by configuration files using the `k3s_server` and +`k3s_agent` variables. + +See "_Server (Control Plane) Configuration_" and "_Agent (Worker) Configuraion_" +below. + +### Global/Cluster Variables + +Below are variables that are set against all of the play hosts for environment +consistency. These are generally cluster-level configuration. + +| Variable | Description | Default Value | +|--------------------------------------|--------------------------------------------------------------------------------------------|--------------------------------| +| `k3s_state` | State of k3s: installed, started, stopped, downloaded, uninstalled, validated. | installed | +| `k3s_release_version` | Use a specific version of k3s, eg. `v0.2.0`. Specify `false` for stable. | `false` | +| `k3s_airgap` | Boolean to enable air-gapped installations | `false` | +| `k3s_config_file` | Location of the k3s configuration file. | `/etc/rancher/k3s/config.yaml` | +| `k3s_build_cluster` | When multiple play hosts are available, attempt to cluster. Read notes below. | `true` | +| `k3s_registration_address` | Fixed registration address for nodes. IP or FQDN. | NULL | +| `k3s_github_url` | Set the GitHub URL to install k3s from. | https://github.com/k3s-io/k3s | +| `k3s_api_url` | URL for K3S updates API. | https://update.k3s.io | +| `k3s_install_dir` | Installation directory for k3s. | `/usr/local/bin` | +| `k3s_install_hard_links` | Install using hard links rather than symbolic links. | `false` | +| `k3s_server_config_yaml_d_files` | A flat list of templates to supplement the `k3s_server` configuration. | [] | +| `k3s_agent_config_yaml_d_files` | A flat list of templates to supplement the `k3s_agent` configuration. | [] | +| `k3s_server_manifests_urls` | A list of URLs to deploy on the primary control plane. Read notes below. | [] | +| `k3s_server_manifests_templates` | A flat list of templates to deploy on the primary control plane. | [] | +| `k3s_server_pod_manifests_urls` | A list of URLs for installing static pod manifests on the control plane. Read notes below. | [] | +| `k3s_server_pod_manifests_templates` | A flat list of templates for installing static pod manifests on the control plane. | [] | +| `k3s_use_experimental` | Allow the use of experimental features in k3s. | `false` | +| `k3s_use_unsupported_config` | Allow the use of unsupported configurations in k3s. | `false` | +| `k3s_etcd_datastore` | Enable etcd embedded datastore (read notes below). | `false` | +| `k3s_debug` | Enable debug logging on the k3s service. | `false` | +| `k3s_registries` | Registries configuration file content. | `{ mirrors: {}, configs:{} }` | + +### K3S Service Configuration + +The below variables change how and when the systemd service unit file for K3S +is run. Use this with caution, please refer to the [systemd documentation](https://www.freedesktop.org/software/systemd/man/systemd.unit.html#%5BUnit%5D%20Section%20Options) +for more information. + +| Variable | Description | Default Value | +|------------------------|----------------------------------------------------------------------|---------------| +| `k3s_start_on_boot` | Start k3s on boot. | `true` | +| `k3s_service_requires` | List of required systemd units to k3s service unit. | [] | +| `k3s_service_wants` | List of "wanted" systemd unit to k3s (weaker than "requires"). | []\* | +| `k3s_service_before` | Start k3s before a defined list of systemd units. | [] | +| `k3s_service_after` | Start k3s after a defined list of systemd units. | []\* | +| `k3s_service_env_vars` | Dictionary of environment variables to use within systemd unit file. | {} | +| `k3s_service_env_file` | Location on host of a environment file to include. | `false`\*\* | + +\* The systemd unit template **always** specifies `network-online.target` for +`wants` and `after`. + +\*\* The file must already exist on the target host, this role will not create +nor manage the file. You can manage this file outside of the role with +pre-tasks in your Ansible playbook. + +### Group/Host Variables + +Below are variables that are set against individual or groups of play hosts. +Typically you'd set these at group level for the control plane or worker nodes. + +| Variable | Description | Default Value | +|--------------------|-------------------------------------------------------------------|---------------------------------------------------| +| `k3s_control_node` | Specify if a host (or host group) are part of the control plane. | `false` (role will automatically delegate a node) | +| `k3s_server` | Server (control plane) configuration, see notes below. | `{}` | +| `k3s_agent` | Agent (worker) configuration, see notes below. | `{}` | + +#### Server (Control Plane) Configuration + +The control plane is configured with the `k3s_server` dict variable. Please +refer to the below documentation for configuration options: + +https://rancher.com/docs/k3s/latest/en/installation/install-options/server-config/ + +The `k3s_server` dictionary variable will contain flags from the above +(removing the `--` prefix). Below is an example: + +```yaml +k3s_server: + datastore-endpoint: postgres://postgres:verybadpass@database:5432/postgres?sslmode=disable + cluster-cidr: 172.20.0.0/16 + flannel-backend: 'none' # This needs to be in quotes + disable: + - traefik + - coredns +``` + +Alternatively, you can create a .yaml file and read it in to the `k3s_server` +variable as per the below example: + +```yaml +k3s_server: "{{ lookup('file', 'path/to/k3s_server.yml') | from_yaml }}" +``` + +Check out the [Documentation](documentation/README.md) for example +configuration. + +#### Agent (Worker) Configuration + +Workers are configured with the `k3s_agent` dict variable. Please refer to the +below documentation for configuration options: + +https://rancher.com/docs/k3s/latest/en/installation/install-options/agent-config + +The `k3s_agent` dictionary variable will contain flags from the above +(removing the `--` prefix). Below is an example: + +```yaml +k3s_agent: + with-node-id: true + node-label: + - "foo=bar" + - "hello=world" +``` + +Alternatively, you can create a .yaml file and read it in to the `k3s_agent` +variable as per the below example: + +```yaml +k3s_agent: "{{ lookup('file', 'path/to/k3s_agent.yml') | from_yaml }}" +``` + +Check out the [Documentation](documentation/README.md) for example +configuration. + +### Ansible Controller Configuration Variables + +The below variables are used to change the way the role executes in Ansible, +particularly with regards to privilege escalation. + +| Variable | Description | Default Value | +|------------------------|----------------------------------------------------------------|---------------| +| `k3s_skip_validation` | Skip all tasks that validate configuration. | `false` | +| `k3s_skip_env_checks` | Skip all tasks that check environment configuration. | `false` | +| `k3s_skip_post_checks` | Skip all tasks that check post execution state. | `false` | +| `k3s_become` | Escalate user privileges for tasks that need root permissions. | `false` | + +#### Important note about Python + +From v3 of this role, Python 3 is required on the target system as well as on +the Ansible controller. This is to ensure consistent behaviour for Ansible +tasks as Python 2 is now EOL. + +If target systems have both Python 2 and Python 3 installed, it is most likely +that Python 2 will be selected by default. To ensure Python 3 is used on a +target with both versions of Python, ensure `ansible_python_interpreter` is +set in your inventory. Below is an example inventory: + +```yaml +--- + +k3s_cluster: + hosts: + kube-0: + ansible_user: ansible + ansible_host: 10.10.9.2 + ansible_python_interpreter: /usr/bin/python3 + kube-1: + ansible_user: ansible + ansible_host: 10.10.9.3 + ansible_python_interpreter: /usr/bin/python3 + kube-2: + ansible_user: ansible + ansible_host: 10.10.9.4 + ansible_python_interpreter: /usr/bin/python3 +``` + +#### Important note about `k3s_release_version` + +If you do not set a `k3s_release_version` the latest version from the stable +channel of k3s will be installed. If you are developing against a specific +version of k3s you must ensure this is set in your Ansible configuration, eg: + +```yaml +k3s_release_version: v1.19.3+k3s1 +``` + +It is also possible to install specific K3s "Channels", below are some +examples for `k3s_release_version`: + +```yaml +k3s_release_version: false # defaults to 'stable' channel +k3s_release_version: stable # latest 'stable' release +k3s_release_version: testing # latest 'testing' release +k3s_release_version: v1.19 # latest 'v1.19' release +k3s_release_version: v1.19.3+k3s3 # specific release + +# Specific commit +# CAUTION - only used for testing - must be 40 characters +k3s_release_version: 48ed47c4a3e420fa71c18b2ec97f13dc0659778b +``` + +#### Important note about `k3s_install_hard_links` + +If you are using the [system-upgrade-controller](https://github.com/rancher/system-upgrade-controller) +you will need to use hard links rather than symbolic links as the controller +will not be able to follow symbolic links. This option has been added however +is not enabled by default to avoid breaking existing installations. + +To enable the use of hard links, ensure `k3s_install_hard_links` is set +to `true`. + +```yaml +k3s_install_hard_links: true +``` + +The result of this can be seen by running the following in `k3s_install_dir`: + +`ls -larthi | grep -E 'k3s|ctr|ctl' | grep -vE ".sh$" | sort` + +Symbolic Links: + +```text +[root@node1 bin]# ls -larthi | grep -E 'k3s|ctr|ctl' | grep -vE ".sh$" | sort +3277823 -rwxr-xr-x 1 root root 52M Jul 25 12:50 k3s-v1.18.4+k3s1 +3279565 lrwxrwxrwx 1 root root 31 Jul 25 12:52 k3s -> /usr/local/bin/k3s-v1.18.6+k3s1 +3279644 -rwxr-xr-x 1 root root 51M Jul 25 12:52 k3s-v1.18.6+k3s1 +3280079 lrwxrwxrwx 1 root root 31 Jul 25 12:52 ctr -> /usr/local/bin/k3s-v1.18.6+k3s1 +3280080 lrwxrwxrwx 1 root root 31 Jul 25 12:52 crictl -> /usr/local/bin/k3s-v1.18.6+k3s1 +3280081 lrwxrwxrwx 1 root root 31 Jul 25 12:52 kubectl -> /usr/local/bin/k3s-v1.18.6+k3s1 +``` + +Hard Links: + +```text +[root@node1 bin]# ls -larthi | grep -E 'k3s|ctr|ctl' | grep -vE ".sh$" | sort +3277823 -rwxr-xr-x 1 root root 52M Jul 25 12:50 k3s-v1.18.4+k3s1 +3279644 -rwxr-xr-x 5 root root 51M Jul 25 12:52 crictl +3279644 -rwxr-xr-x 5 root root 51M Jul 25 12:52 ctr +3279644 -rwxr-xr-x 5 root root 51M Jul 25 12:52 k3s +3279644 -rwxr-xr-x 5 root root 51M Jul 25 12:52 k3s-v1.18.6+k3s1 +3279644 -rwxr-xr-x 5 root root 51M Jul 25 12:52 kubectl +``` + +#### Important note about `k3s_build_cluster` + +If you set `k3s_build_cluster` to `false`, this role will install each play +host as a standalone node. An example of when you might use this would be +when building a large number of standalone IoT devices running K3s. Below is a +hypothetical situation where we are to deploy 25 Raspberry Pi devices, each a +standalone system and not a cluster of 25 nodes. To do this we'd use a playbook +similar to the below: + +```yaml +- hosts: k3s_nodes # eg. 25 RPi's defined in our inventory. + vars: + k3s_build_cluster: false + roles: + - xanmanning.k3s +``` + +#### Important note about `k3s_control_node` and High Availability (HA) + +By default only one host will be defined as a control node by Ansible, If you +do not set a host as a control node, this role will automatically delegate +the first play host as a control node. This is not suitable for use within +a Production workload. + +If multiple hosts have `k3s_control_node` set to `true`, you must also set +`datastore-endpoint` in `k3s_server` as the connection string to a MySQL or +PostgreSQL database, or external Etcd cluster else the play will fail. + +If using TLS, the CA, Certificate and Key need to already be available on +the play hosts. + +See: [High Availability with an External DB](https://rancher.com/docs/k3s/latest/en/installation/ha/) + +It is also possible, though not supported, to run a single K3s control node +with a `datastore-endpoint` defined. As this is not a typically supported +configuration you will need to set `k3s_use_unsupported_config` to `true`. + +Since K3s v1.19.1 it is possible to use an embedded Etcd as the backend +database, and this is done by setting `k3s_etcd_datastore` to `true`. +The best practice for Etcd is to define at least 3 members to ensure quorum is +established. In addition to this, an odd number of members is recommended to +ensure a majority in the event of a network partition. If you want to use 2 +members or an even number of members, please set `k3s_use_unsupported_config` +to `true`. + +#### Important note about `k3s_server_manifests_urls` and `k3s_server_pod_manifests_urls` + +To deploy server manifests and server pod manifests from URL, you need to +specify a `url` and optionally a `filename` (if none provided basename is used). Below is an example of how to deploy the +Tigera operator for Calico and kube-vip. + +```yaml +--- + +k3s_server_manifests_urls: + - url: https://docs.projectcalico.org/archive/v3.19/manifests/tigera-operator.yaml + filename: tigera-operator.yaml + +k3s_server_pod_manifests_urls: + - url: https://raw.githubusercontent.com/kube-vip/kube-vip/main/example/deploy/0.1.4.yaml + filename: kube-vip.yaml + +``` + +#### Important note about `k3s_airgap` + +When deploying k3s in an air gapped environment you should provide the `k3s` binary in `./files/`. The binary will not be downloaded from Github and will subsequently not be verified using the provided sha256 sum, nor able to verify the version that you are running. All risks and burdens associated are assumed by the user in this scenario. + +## Dependencies + +No dependencies on other roles. + +## Example Playbooks + +Example playbook, single control node running `testing` channel k3s: + +```yaml +- hosts: k3s_nodes + vars: + k3s_release_version: testing + roles: + - role: xanmanning.k3s +``` + +Example playbook, Highly Available with PostgreSQL database running the latest +stable release: + +```yaml +- hosts: k3s_nodes + vars: + k3s_registration_address: loadbalancer # Typically a load balancer. + k3s_server: + datastore-endpoint: "postgres://postgres:verybadpass@database:5432/postgres?sslmode=disable" + pre_tasks: + - name: Set each node to be a control node + ansible.builtin.set_fact: + k3s_control_node: true + when: inventory_hostname in ['node2', 'node3'] + roles: + - role: xanmanning.k3s +``` + +## License + +[BSD 3-clause](LICENSE.txt) + +## Contributors + +Contributions from the community are very welcome, but please read the +[contribution guidelines](CONTRIBUTING.md) before doing so, this will help +make things as streamlined as possible. + +Also, please check out the awesome +[list of contributors](https://github.com/PyratLabs/ansible-role-k3s/graphs/contributors). + +## Author Information + +[Xan Manning](https://xan.manning.io/) diff --git a/roles/xanmanning.k3s/defaults/main.yml b/roles/xanmanning.k3s/defaults/main.yml new file mode 100644 index 00000000..d0766d53 --- /dev/null +++ b/roles/xanmanning.k3s/defaults/main.yml @@ -0,0 +1,157 @@ +--- + +## +# Global/Cluster Configuration +## + +# k3s state, options: installed, started, stopped, restarted, uninstalled, validated +# (default: installed) +k3s_state: installed + +# Use a specific k3s version, if set to "false" we will get the latest +# k3s_release_version: v1.19.3 +k3s_release_version: false + +# Location of the k3s configuration file +k3s_config_file: "/etc/rancher/k3s/config.yaml" + +# Location of the k3s configuration directory +k3s_config_yaml_d_dir: "/etc/rancher/k3s/config.yaml.d" + +# When multiple ansible_play_hosts are present, attempt to cluster the nodes. +# Using false will create multiple standalone nodes. +# (default: true) +k3s_build_cluster: true + +# URL for GitHub project +k3s_github_url: https://github.com/k3s-io/k3s + +# URL for K3s updates API +k3s_api_url: https://update.k3s.io + +# Install K3s in Air Gapped scenarios +k3s_airgap: false + +# Skip all tasks that validate configuration +k3s_skip_validation: false + +# Skip all tasks that check environment configuration +k3s_skip_env_checks: false + +# Skip post-checks +k3s_skip_post_checks: false + +# Installation directory for k3s +k3s_install_dir: /usr/local/bin + +# Install using hard links rather than symbolic links +k3s_install_hard_links: false + +# A list of templates used for configuring the server. +k3s_server_config_yaml_d_files: [] + +# A list of templates used for configuring the agent. +k3s_agent_config_yaml_d_files: [] + +# A list of templates used for pre-configuring the cluster. +k3s_server_manifests_templates: [] + +# A list of URLs used for pre-configuring the cluster. +k3s_server_manifests_urls: [] +# - url: https://some/url/to/manifest.yml +# filename: manifest.yml + +# A list of templates used for installing static pod manifests on the control plane. +k3s_server_pod_manifests_templates: [] + +# A list of URLs used for installing static pod manifests on the control plane. +k3s_server_pod_manifests_urls: [] +# - url: https://some/url/to/manifest.yml +# filename: manifest.yml + +# Use experimental features in k3s? +k3s_use_experimental: false + +# Allow for unsupported configurations in k3s? +k3s_use_unsupported_config: false + +# Enable etcd embedded datastore +k3s_etcd_datastore: false + +## +# Systemd config +## + +# Start k3s on system boot +k3s_start_on_boot: true + +# List of required systemd units to k3s service unit. +k3s_service_requires: [] + +# List of "wanted" systemd unit to k3s (weaker than "requires"). +k3s_service_wants: [] + +# Start k3s before a defined list of systemd units. +k3s_service_before: [] + +# Start k3s after a defined list of systemd units. +k3s_service_after: [] + +# Dictionary of environment variables to use within systemd unit file +# Some examples below +k3s_service_env_vars: {} +# PATH: /opt/k3s/bin +# GOGC: 10 + +# Location on host of a environment file to include. This must already exist on +# the target as this role will not populate this file. +k3s_service_env_file: false + + +## +# Server Configuration +## + +k3s_server: {} +# k3s_server: +# listen-port: 6443 + +## +# Agent Configuration +## + +k3s_agent: {} +# k3s_agent: +# node-label: +# - "foo=bar" +# - "bish=bosh" + +## +# Ansible Controller configuration +## + +# Use become privileges? +k3s_become: false + +# Private registry configuration. +# Rancher k3s documentation: https://rancher.com/docs/k3s/latest/en/installation/private-registry/ +k3s_registries: + + mirrors: +# docker.io: +# endpoint: +# - "https://mycustomreg.com:5000" + configs: +# "mycustomreg:5000": +# auth: +# # this is the registry username +# username: xxxxxx +# # this is the registry password +# password: xxxxxx +# tls: +# # path to the cert file used in the registry +# cert_file: +# # path to the key file used in the registry +# key_file: +# # path to the ca file used in the registry +# ca_file: diff --git a/roles/xanmanning.k3s/documentation/README.md b/roles/xanmanning.k3s/documentation/README.md new file mode 100644 index 00000000..e00caed2 --- /dev/null +++ b/roles/xanmanning.k3s/documentation/README.md @@ -0,0 +1,44 @@ +# ansible-role-k3s + +This document describes a number of ways of consuming this Ansible role for use +in your own k3s deployments. It will not be able to cover every use case +scenario but will provide some common example configurations. + +## Requirements + +Before you start you will need an Ansible controller. This can either be your +workstation, or a dedicated system that you have access to. The instructions +in this documentation assume you are using `ansible` CLI, there are no +instructions available for Ansible Tower at this time. + +Follow the below guide to get Ansible installed. + +https://docs.ansible.com/ansible/latest/installation_guide/index.html + +## Quickstart + +Below are quickstart examples for a single node k3s server, a k3s cluster +with a single control node and HA k3s cluster. These represent the bare +minimum configuration. + + - [Single node k3s](quickstart-single-node.md) + - [Simple k3s cluster](quickstart-cluster.md) + - [HA k3s cluster using embedded etcd](quickstart-ha-cluster.md) + +## Example configurations and operations + +### Configuration + + - [Setting up 2-node HA control plane with external datastore](configuration/2-node-ha-ext-datastore.md) + - [Provision multiple standalone k3s nodes](configuration/multiple-standalone-k3s-nodes.md) + - [Set node labels and component arguments](configuration/node-labels-and-component-args.md) + - [Use an alternate CNI](configuration/use-an-alternate-cni.md) + - [IPv4/IPv6 Dual-Stack config](configuration/ipv4-ipv6-dual-stack.md) + - [Start K3S after another service](configuration/systemd-config.md) + +### Operations + + - [Stop/Start a cluster](operations/stop-start-cluster.md) + - [Updating k3s](operations/updating-k3s.md) + - [Extending a cluster](operations/extending-a-cluster.md) + - [Shrinking a cluster](operations/shrinking-a-cluster.md) diff --git a/roles/xanmanning.k3s/documentation/configuration/2-node-ha-ext-datastore.md b/roles/xanmanning.k3s/documentation/configuration/2-node-ha-ext-datastore.md new file mode 100644 index 00000000..764c7fbc --- /dev/null +++ b/roles/xanmanning.k3s/documentation/configuration/2-node-ha-ext-datastore.md @@ -0,0 +1,79 @@ +# 2 Node HA Control Plane with external database + +For this configuration we are deploying a highly available control plane +composed of two control nodes. This can be achieved with embedded etcd, however +etcd ideally has an odd number of nodes. + +The example below will use an external PostgreSQL datastore to store the +cluster state information. + +Main guide: https://rancher.com/docs/k3s/latest/en/installation/ha/ + +## Architecture + +```text + +-------------------+ + | Load Balancer/VIP | + +---------+---------+ + | + | + | + | + +------------+ | +------------+ + | | | | | ++--------+ control-01 +<-----+----->+ control-02 | +| | | | | +| +-----+------+ +------+-----+ +| | | +| +-------------+-------------+ +| | | | +| +------v----+ +-----v-----+ +----v------+ +| | | | | | | +| | worker-01 | | worker-02 | | worker-03 | +| | | | | | | +| +-----------+ +-----------+ +-----------+ +| +| +-------+ +-------+ +| | | | | ++-------------------> db-01 +--+ db-02 | + | | | | + +-------+ +-------+ +``` + +### Required Components + + - Load balancer + - 2 control plane nodes + - 1 or more worker nodes + - PostgreSQL Database (replicated, or Linux HA Cluster). + +## Configuration + +For your control nodes, you will need to instruct the control plane of the +PostgreSQL datastore endpoint and set `k3s_registration_address` to be the +hostname or IP of your load balancer or VIP. + +Below is the example for PostgreSQL, it is possible to use MySQL or an Etcd +cluster as well. Consult the below guide for using alternative datastore +endpoints. + +https://rancher.com/docs/k3s/latest/en/installation/datastore/#datastore-endpoint-format-and-functionality + +```yaml +--- + +k3s_server: + datastore-endpoint: postgres://postgres:verybadpass@database:5432/postgres?sslmode=disable + node-taint: + - "k3s-controlplane=true:NoExecute" +``` + +Your worker nodes need to know how to connect to the control plane, this is +defined by setting `k3s_registration_address` to the hostname or IP address of +the load balancer. + +```yaml +--- + +k3s_registration_address: control.examplek3s.com +``` diff --git a/roles/xanmanning.k3s/documentation/configuration/ipv4-ipv6-dual-stack.md b/roles/xanmanning.k3s/documentation/configuration/ipv4-ipv6-dual-stack.md new file mode 100644 index 00000000..fc80c8e5 --- /dev/null +++ b/roles/xanmanning.k3s/documentation/configuration/ipv4-ipv6-dual-stack.md @@ -0,0 +1,21 @@ +# IPv4 and IPv6 Dual-stack config + +If you need to run your K3S cluster with both IPv4 and IPv6 address ranges +you will need to configure the `k3s_server.cluster-cidr` and +`k3s_server.service-cidr` values specifying both ranges. + +:hand: if you are using `k3s<1.23` you will need to use a different CNI as +dual-stack support is not available in Flannel. + +Below is a noddy example: + +```yaml +--- + +k3s_server: + # Using Calico on k3s<1.23 so Flannel needs to be disabled. + flannel-backend: 'none' + # Format: ipv4/cidr,ipv6/cidr + cluster-cidr: 10.42.0.0/16,fc00:a0::/64 + service-cidr: 10.43.0.0/16,fc00:a1::/64 +``` diff --git a/roles/xanmanning.k3s/documentation/configuration/multiple-standalone-k3s-nodes.md b/roles/xanmanning.k3s/documentation/configuration/multiple-standalone-k3s-nodes.md new file mode 100644 index 00000000..3e9a08a6 --- /dev/null +++ b/roles/xanmanning.k3s/documentation/configuration/multiple-standalone-k3s-nodes.md @@ -0,0 +1,71 @@ +# Multiple standalone K3s nodes + +This is an example of when you might want to configure multiple standalone +k3s nodes simultaneously. For this we will assume a hypothetical situation +where we are configuring 25 Raspberry Pis to deploy to our shop floors. + +Each Rasperry Pi will be configured as a standalone IoT device hosting an +application that will push data to head office. + +## Architecture + +```text ++-------------+ +| | +| Node-01 +-+ +| | | ++--+----------+ +-+ + | | | + +--+---------+ +-+ + | | | + +--+--------+ | + | | Node-N + +----------+ + +``` + +## Configuration + +Below is our example inventory of 200 nodes (Truncated): + +```yaml +--- + +k3s_workers: + hosts: + kube-0: + ansible_user: ansible + ansible_host: 10.10.9.2 + ansible_python_interpreter: /usr/bin/python3 + kube-1: + ansible_user: ansible + ansible_host: 10.10.9.3 + ansible_python_interpreter: /usr/bin/python3 + kube-2: + ansible_user: ansible + ansible_host: 10.10.9.4 + ansible_python_interpreter: /usr/bin/python3 + + # ..... SNIP ..... + + kube-199: + ansible_user: ansible + ansible_host: 10.10.9.201 + ansible_python_interpreter: /usr/bin/python3 + kube-200: + ansible_user: ansible + ansible_host: 10.10.9.202 + ansible_python_interpreter: /usr/bin/python3 + +``` + +In our `group_vars/` (or as `vars:` in our playbook), we will need to set the +`k3s_build_cluster` variable to `false`. This will stop the role from +attempting to cluster all 200 nodes, instead it will install k3s across each +node as as 200 standalone servers. + +```yaml +--- + +k3s_build_cluster: false +``` diff --git a/roles/xanmanning.k3s/documentation/configuration/node-labels-and-component-args.md b/roles/xanmanning.k3s/documentation/configuration/node-labels-and-component-args.md new file mode 100644 index 00000000..3daef9b0 --- /dev/null +++ b/roles/xanmanning.k3s/documentation/configuration/node-labels-and-component-args.md @@ -0,0 +1,39 @@ +# Configure node labels and component arguments + +The following command line arguments can be specified multiple times with +`key=value` pairs: + + - `--kube-kubelet-arg` + - `--kube-proxy-arg` + - `--kube-apiserver-arg` + - `--kube-scheduler-arg` + - `--kube-controller-manager-arg` + - `--kube-cloud-controller-manager-arg` + - `--node-label` + - `--node-taint` + +In the config file, this is done by defining a list of values for each +command like argument, for example: + +```yaml +--- + +k3s_server: + # Set the plugins registry directory + kubelet-arg: + - "volume-plugin-dir=/var/lib/rancher/k3s/agent/kubelet/plugins_registry" + # Set the pod eviction timeout and node monitor grace period + kube-controller-manager-arg: + - "pod-eviction-timeout=2m" + - "node-monitor-grace-period=30s" + # Set API server feature gate + kube-apiserver-arg: + - "feature-gates=RemoveSelfLink=false" + # Laels to apply to a node + node-label: + - "NodeTier=development" + - "NodeLocation=eu-west-2a" + # Stop k3s control plane having workloads scheduled on them + node-taint: + - "k3s-controlplane=true:NoExecute" +``` diff --git a/roles/xanmanning.k3s/documentation/configuration/systemd-config.md b/roles/xanmanning.k3s/documentation/configuration/systemd-config.md new file mode 100644 index 00000000..3611f503 --- /dev/null +++ b/roles/xanmanning.k3s/documentation/configuration/systemd-config.md @@ -0,0 +1,19 @@ +# systemd config + +Below are examples to tweak how and when K3S starts up. + +## Wanted service units + +In this example, we're going to start K3S after Wireguard. Our example server +has a Wireguard connection `wg0`. We are using "wants" rather than "requires" +as it's a weaker requirement that Wireguard must be running. We then want +K3S to start after Wireguard has started. + +```yaml +--- + +k3s_service_wants: + - wg-quick@wg0.service +k3s_service_after: + - wg-quick@wg0.service +``` diff --git a/roles/xanmanning.k3s/documentation/configuration/use-an-alternate-cni.md b/roles/xanmanning.k3s/documentation/configuration/use-an-alternate-cni.md new file mode 100644 index 00000000..d67f3caf --- /dev/null +++ b/roles/xanmanning.k3s/documentation/configuration/use-an-alternate-cni.md @@ -0,0 +1,63 @@ +# Use an alternate CNI + +K3S ships with Flannel, however sometimes you want an different CNI such as +Calico, Canal or Weave Net. To do this you will need to disable Flannel with +`flannel-backend: "none"`, specify a `cluster-cidr` and add your CNI manifests +to the `k3s_server_manifests_templates`. + +## Calico example + +The below is based on the +[Calico quickstart documentation](https://docs.projectcalico.org/getting-started/kubernetes/quickstart). + +Steps: + + 1. Download `tigera-operator.yaml` to the manifests directory. + 1. Download `custom-resources.yaml` to the manifests directory. + 1. Choose a `cluster-cidr` (we are using 192.168.0.0/16) + 1. Set `k3s_server` and `k3s_server_manifest_templates` as per the below, + ensure the paths to manifests are correct for your project repo. + +```yaml +--- + +# K3S Server config, don't deploy flannel and set cluster pod CIDR. +k3s_server: + cluster-cidr: 192.168.0.0/16 + flannel-backend: "none" + +# Deploy the following k3s server templates. +k3s_server_manifests_templates: + - "manifests/calico/tigera-operator.yaml" + - "manifests/calico/custom-resources.yaml" +``` + +All nodes should come up as "Ready", below is a 3-node cluster: + +```text + $ kubectl get nodes -o wide -w +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME +kube-0 Ready control-plane,etcd,master 114s v1.20.2+k3s1 10.10.9.2 10.10.9.2 Ubuntu 20.04.1 LTS 5.4.0-56-generic containerd://1.4.3-k3s1 +kube-1 Ready control-plane,etcd,master 80s v1.20.2+k3s1 10.10.9.3 10.10.9.3 Ubuntu 20.04.1 LTS 5.4.0-56-generic containerd://1.4.3-k3s1 +kube-2 Ready control-plane,etcd,master 73s v1.20.2+k3s1 10.10.9.4 10.10.9.4 Ubuntu 20.04.1 LTS 5.4.0-56-generic containerd://1.4.3-k3s1 +``` + +Pods should be deployed with deployed within the CIDR specified in our config +file. + +```text +$ kubectl get pods -o wide -A +NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +calico-system calico-kube-controllers-cfb4ff54b-8rp8r 1/1 Running 0 5m4s 192.168.145.65 kube-0 +calico-system calico-node-2cm2m 1/1 Running 0 5m4s 10.10.9.2 kube-0 +calico-system calico-node-2s6lx 1/1 Running 0 4m42s 10.10.9.4 kube-2 +calico-system calico-node-zwqjz 1/1 Running 0 4m49s 10.10.9.3 kube-1 +calico-system calico-typha-7b6747d665-78swq 1/1 Running 0 3m5s 10.10.9.4 kube-2 +calico-system calico-typha-7b6747d665-8ff66 1/1 Running 0 3m5s 10.10.9.3 kube-1 +calico-system calico-typha-7b6747d665-hgplx 1/1 Running 0 5m5s 10.10.9.2 kube-0 +kube-system coredns-854c77959c-6qhgt 1/1 Running 0 5m20s 192.168.145.66 kube-0 +kube-system helm-install-traefik-4czr9 0/1 Completed 0 5m20s 192.168.145.67 kube-0 +kube-system metrics-server-86cbb8457f-qcxf5 1/1 Running 0 5m20s 192.168.145.68 kube-0 +kube-system traefik-6f9cbd9bd4-7h4rl 1/1 Running 0 2m50s 192.168.126.65 kube-1 +tigera-operator tigera-operator-b6c4bfdd9-29hhr 1/1 Running 0 5m20s 10.10.9.2 kube-0 +``` diff --git a/roles/xanmanning.k3s/documentation/operations/extending-a-cluster.md b/roles/xanmanning.k3s/documentation/operations/extending-a-cluster.md new file mode 100644 index 00000000..e10ffa7d --- /dev/null +++ b/roles/xanmanning.k3s/documentation/operations/extending-a-cluster.md @@ -0,0 +1,69 @@ +# Extending a cluster + +This document describes the method for extending an cluster with new worker +nodes. + +## Assumptions + +It is assumed that you have already deployed a k3s cluster using this role, +you have an appropriately configured inventory and playbook to create the +cluster. + +Below, our example inventory and playbook are as follows: + + - inventory: `inventory.yml` + - playbook: `cluster.yml` + +Currently your `inventory.yml` looks like this, it has two nodes defined, +`kube-0` (control node) and `kube-1` (worker node). + +```yaml +--- + +k3s_cluster: + hosts: + kube-0: + ansible_user: ansible + ansible_host: 10.10.9.2 + ansible_python_interpreter: /usr/bin/python3 + kube-1: + ansible_user: ansible + ansible_host: 10.10.9.3 + ansible_python_interpreter: /usr/bin/python3 +``` + +## Method + +We have our two nodes, one control, one worker. The goal is to extend this to +add capacity by adding a new worker node, `kube-2`. To do this we will add the +new node to our inventory. + +```yaml +--- + +k3s_cluster: + hosts: + kube-0: + ansible_user: ansible + ansible_host: 10.10.9.2 + ansible_python_interpreter: /usr/bin/python3 + kube-1: + ansible_user: ansible + ansible_host: 10.10.9.3 + ansible_python_interpreter: /usr/bin/python3 + kube-2: + ansible_user: ansible + ansible_host: 10.10.9.4 + ansible_python_interpreter: /usr/bin/python3 +``` + +Once the new node has been added, you can re-run the automation to join it to +the cluster. You should expect the majority of changes to the worker node being +introduced to the cluster. + +```text +PLAY RECAP ******************************************************************************************************* +kube-0 : ok=53 changed=1 unreachable=0 failed=0 skipped=30 rescued=0 ignored=0 +kube-1 : ok=40 changed=1 unreachable=0 failed=0 skipped=35 rescued=0 ignored=0 +kube-2 : ok=42 changed=10 unreachable=0 failed=0 skipped=35 rescued=0 ignored=0 +``` diff --git a/roles/xanmanning.k3s/documentation/operations/shrinking-a-cluster.md b/roles/xanmanning.k3s/documentation/operations/shrinking-a-cluster.md new file mode 100644 index 00000000..bf900b15 --- /dev/null +++ b/roles/xanmanning.k3s/documentation/operations/shrinking-a-cluster.md @@ -0,0 +1,74 @@ +# Shrinking a cluster + +This document describes the method for shrinking a cluster, by removing a +worker nodes. + +## Assumptions + +It is assumed that you have already deployed a k3s cluster using this role, +you have an appropriately configured inventory and playbook to create the +cluster. + +Below, our example inventory and playbook are as follows: + + - inventory: `inventory.yml` + - playbook: `cluster.yml` + +Currently your `inventory.yml` looks like this, it has three nodes defined, +`kube-0` (control node) and `kube-1`, `kube-2` (worker nodes). + +```yaml +--- + +k3s_cluster: + hosts: + kube-0: + ansible_user: ansible + ansible_host: 10.10.9.2 + ansible_python_interpreter: /usr/bin/python3 + kube-1: + ansible_user: ansible + ansible_host: 10.10.9.3 + ansible_python_interpreter: /usr/bin/python3 + kube-2: + ansible_user: ansible + ansible_host: 10.10.9.4 + ansible_python_interpreter: /usr/bin/python3 +``` + +## Method + +We have our three nodes, one control, two workers. The goal is to shrink this to +remove excess capacity by offboarding the worker node `kube-2`. To do this we +will set `kube-2` node to `k3s_state: uninstalled` in our inventory. + +```yaml +--- + +k3s_cluster: + hosts: + kube-0: + ansible_user: ansible + ansible_host: 10.10.9.2 + ansible_python_interpreter: /usr/bin/python3 + kube-1: + ansible_user: ansible + ansible_host: 10.10.9.3 + ansible_python_interpreter: /usr/bin/python3 + kube-2: + ansible_user: ansible + ansible_host: 10.10.9.4 + ansible_python_interpreter: /usr/bin/python3 + k3s_state: uninstalled +``` + +What you will typically see is changes to your control plane (`kube-0`) and the +node being removed (`kube-2`). The role will register the removal of the node +with the cluster by draining the node and removing it from the cluster. + +```text +PLAY RECAP ******************************************************************************************************* +kube-0 : ok=55 changed=2 unreachable=0 failed=0 skipped=28 rescued=0 ignored=0 +kube-1 : ok=40 changed=0 unreachable=0 failed=0 skipped=35 rescued=0 ignored=0 +kube-2 : ok=23 changed=2 unreachable=0 failed=0 skipped=17 rescued=0 ignored=1 +``` diff --git a/roles/xanmanning.k3s/documentation/operations/stop-start-cluster.md b/roles/xanmanning.k3s/documentation/operations/stop-start-cluster.md new file mode 100644 index 00000000..c321e00b --- /dev/null +++ b/roles/xanmanning.k3s/documentation/operations/stop-start-cluster.md @@ -0,0 +1,93 @@ +# Stopping and Starting a cluster + +This document describes the Ansible method for restarting a k3s cluster +deployed by this role. + +## Assumptions + +It is assumed that you have already deployed a k3s cluster using this role, +you have an appropriately configured inventory and playbook to create the +cluster. + +Below, our example inventory and playbook are as follows: + + - inventory: `inventory.yml` + - playbook: `cluster.yml` + +## Method + +### Start cluster + +You can start the cluster using either of the following commands: + + - Using the playbook: `ansible-playbook -i inventory.yml cluster.yml --become -e 'k3s_state=started'` + - Using an ad-hoc command: `ansible -i inventory.yml -m service -a 'name=k3s state=started' --become all` + +Below is example output, remember that Ansible is idempotent so re-running a +command may not necessarily change the state. + +**Playbook method output**: + +```text +PLAY RECAP ******************************************************************************************************* +kube-0 : ok=6 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +kube-1 : ok=6 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +kube-2 : ok=6 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +``` + +### Stop cluster + +You can stop the cluster using either of the following commands: + + - Using the playbook: `ansible-playbook -i inventory.yml cluster.yml --become -e 'k3s_state=stopped'` + - Using an ad-hoc command: `ansible -i inventory.yml -m service -a 'name=k3s state=stopped' --become all` + +Below is example output, remember that Ansible is idempotent so re-running a +command may not necessarily change the state. + +**Playbook method output**: + +```text +PLAY RECAP ******************************************************************************************************* +kube-0 : ok=6 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +kube-1 : ok=6 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +kube-2 : ok=6 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +``` + +### Restart cluster + +Just like the `service` module, you can also specify `restarted` as a state. +This will do `stop` followed by `start`. + + - Using the playbook: `ansible-playbook -i inventory.yml cluster.yml --become -e 'k3s_state=restarted'` + - Using an ad-hoc command: `ansible -i inventory.yml -m service -a 'name=k3s state=restarted' --become all` + +```text +PLAY RECAP ******************************************************************************************************* +kube-0 : ok=7 changed=1 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +kube-1 : ok=7 changed=1 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +kube-2 : ok=7 changed=1 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +``` + +## Tips + +You can limit the targets by adding the `-l` flag to your `ansible-playbook` +command, or simply target your ad-hoc commands. For example, in a 3 node +cluster (called `kube-0`, `kube-1` and `kube-2`) we can limit the restart to +`kube-1` and `kube-2` with the following: + + - Using the playbook: `ansible-playbook -i inventory.yml cluster.yml --become -e 'k3s_state=restarted' -l "kube-1,kube-2"` + - Using an ad-hoc command: `ansible -i inventory.yml -m service -a 'name=k3s state=restarted' --become "kube-1,kube-2"` + +```text +PLAY RECAP ******************************************************************************************************** +kube-1 : ok=7 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +kube-2 : ok=7 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +``` + +## FAQ + + 1. _Why might I use the `ansible-playbook` command over an ad-hoc command?_ + - The stop/start tasks will be aware of configuration. As the role + develops, there might be some pre-tasks added to change how a cluster + is stopped or started. diff --git a/roles/xanmanning.k3s/documentation/operations/updating-k3s.md b/roles/xanmanning.k3s/documentation/operations/updating-k3s.md new file mode 100644 index 00000000..54db37cb --- /dev/null +++ b/roles/xanmanning.k3s/documentation/operations/updating-k3s.md @@ -0,0 +1,52 @@ +# Updating k3s + +## Before you start! + +Ensure you back up your k3s cluster. This is particularly important if you use +an external datastore or embedded Etcd. Please refer to the below guide to +backing up your k3s datastore: + +https://rancher.com/docs/k3s/latest/en/backup-restore/ + +Also, check your volume backups are also working! + +## Proceedure + +### Updates using Ansible + +To update via Ansible, set `k3s_release_version` to the target version you wish +to go to. For example, from your `v1.19.3+k3s1` playbook: + +```yaml +--- +# BEFORE + +- name: Provision k3s cluster + hosts: k3s_cluster + vars: + k3s_release_version: v1.19.3+k3s1 + roles: + - name: xanmanning.k3s +``` + +Updating to `v1.20.2+k3s1`: + +```yaml +--- +# AFTER + +- name: Provision k3s cluster + hosts: k3s_cluster + vars: + k3s_release_version: v1.20.2+k3s1 + roles: + - name: xanmanning.k3s +``` + +### Automatic updates + +For automatic updates, consider installing Rancher's +[system-upgrade-controller](https://rancher.com/docs/k3s/latest/en/upgrades/automated/) + +**Please note**, to be able to update using the system-upgrade-controller you +will need to set `k3s_install_hard_links` to `true`. diff --git a/roles/xanmanning.k3s/documentation/quickstart-cluster.md b/roles/xanmanning.k3s/documentation/quickstart-cluster.md new file mode 100644 index 00000000..e623c099 --- /dev/null +++ b/roles/xanmanning.k3s/documentation/quickstart-cluster.md @@ -0,0 +1,147 @@ +# Quickstart: K3s cluster with a single control node + +This is the quickstart guide to creating your own k3s cluster with one control +plane node. This control plane node will also be a worker. + +:hand: This example requires your Ansible user to be able to connect to the +servers over SSH using key-based authentication. The user is also has an entry +in a sudoers file that allows privilege escalation without requiring a +password. + +To test this is the case, run the following check replacing `` +and ``. The expected output is `Works` + +`ssh @ 'sudo cat /etc/shadow >/dev/null && echo "Works"'` + +For example: + +```text +[ xmanning@dreadfort:~/git/kubernetes-playground ] (master) $ ssh ansible@kube-0 'sudo cat /etc/shadow >/dev/null && echo "Works"' +Works +[ xmanning@dreadfort:~/git/kubernetes-playground ] (master) $ +``` + +## Directory structure + +Our working directory will have the following files: + +```text +kubernetes-playground/ + |_ inventory.yml + |_ cluster.yml +``` + +## Inventory + +Here's a YAML based example inventory for our servers called `inventory.yml`: + +```yaml +--- + +k3s_cluster: + hosts: + kube-0: + ansible_user: ansible + ansible_host: 10.10.9.2 + ansible_python_interpreter: /usr/bin/python3 + kube-1: + ansible_user: ansible + ansible_host: 10.10.9.3 + ansible_python_interpreter: /usr/bin/python3 + kube-2: + ansible_user: ansible + ansible_host: 10.10.9.4 + ansible_python_interpreter: /usr/bin/python3 + +``` + +We can test this works with `ansible -i inventory.yml -m ping all`, expected +result: + +```text +kube-0 | SUCCESS => { + "changed": false, + "ping": "pong" +} +kube-1 | SUCCESS => { + "changed": false, + "ping": "pong" +} +kube-2 | SUCCESS => { + "changed": false, + "ping": "pong" +} + +``` + +## Playbook + +Here is our playbook for the k3s cluster (`cluster.yml`): + +```yaml +--- + +- name: Build a cluster with a single control node + hosts: k3s_cluster + vars: + k3s_become: true + roles: + - role: xanmanning.k3s +``` + +## Execution + +To execute the playbook against our inventory file, we will run the following +command: + +`ansible-playbook -i inventory.yml cluster.yml` + +The output we can expect is similar to the below, with no failed or unreachable +nodes. The default behavior of this role is to delegate the first play host as +the control node, so kube-0 will have more changed tasks than others: + +```text +PLAY RECAP ******************************************************************************************************* +kube-0 : ok=56 changed=11 unreachable=0 failed=0 skipped=28 rescued=0 ignored=0 +kube-1 : ok=43 changed=10 unreachable=0 failed=0 skipped=32 rescued=0 ignored=0 +kube-2 : ok=43 changed=10 unreachable=0 failed=0 skipped=32 rescued=0 ignored=0 +``` + +## Testing + +After logging into kube-0, we can test that k3s is running across the cluster, +that all nodes are ready and that everything is ready to execute our Kubernetes +workloads by running the following: + + - `sudo kubectl get nodes -o wide` + - `sudo kubectl get pods -o wide --all-namespaces` + +:hand: Note we are using `sudo` because we need to be root to access the +kube config for this node. This behavior can be changed with specifying +`write-kubeconfig-mode: '0644'` in `k3s_server`. + +**Get Nodes**: + +```text +ansible@kube-0:~$ sudo kubectl get nodes -o wide +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME +kube-0 Ready master 34s v1.19.4+k3s1 10.0.2.15 Ubuntu 20.04.1 LTS 5.4.0-56-generic containerd://1.4.1-k3s1 +kube-2 Ready 14s v1.19.4+k3s1 10.0.2.17 Ubuntu 20.04.1 LTS 5.4.0-56-generic containerd://1.4.1-k3s1 +kube-1 Ready 14s v1.19.4+k3s1 10.0.2.16 Ubuntu 20.04.1 LTS 5.4.0-56-generic containerd://1.4.1-k3s1 +ansible@kube-0:~$ +``` + +**Get Pods**: + +```text +ansible@kube-0:~$ sudo kubectl get pods -o wide --all-namespaces +NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +kube-system local-path-provisioner-7ff9579c6-72j8x 1/1 Running 0 55s 10.42.2.2 kube-1 +kube-system metrics-server-7b4f8b595-lkspj 1/1 Running 0 55s 10.42.1.2 kube-2 +kube-system helm-install-traefik-b6vnt 0/1 Completed 0 55s 10.42.0.3 kube-0 +kube-system coredns-66c464876b-llsh7 1/1 Running 0 55s 10.42.0.2 kube-0 +kube-system svclb-traefik-jrqg7 2/2 Running 0 27s 10.42.1.3 kube-2 +kube-system svclb-traefik-gh65q 2/2 Running 0 27s 10.42.0.4 kube-0 +kube-system svclb-traefik-5z7zp 2/2 Running 0 27s 10.42.2.3 kube-1 +kube-system traefik-5dd496474-l2k74 1/1 Running 0 27s 10.42.1.4 kube-2 +``` diff --git a/roles/xanmanning.k3s/documentation/quickstart-ha-cluster.md b/roles/xanmanning.k3s/documentation/quickstart-ha-cluster.md new file mode 100644 index 00000000..28e3b197 --- /dev/null +++ b/roles/xanmanning.k3s/documentation/quickstart-ha-cluster.md @@ -0,0 +1,154 @@ +# Quickstart: K3s cluster with a HA control plane using embedded etcd + +This is the quickstart guide to creating your own 3 node k3s cluster with a +highly available control plane using the embedded etcd datastore. +The control plane will all be workers as well. + +:hand: This example requires your Ansible user to be able to connect to the +servers over SSH using key-based authentication. The user is also has an entry +in a sudoers file that allows privilege escalation without requiring a +password. + +To test this is the case, run the following check replacing `` +and ``. The expected output is `Works` + +`ssh @ 'sudo cat /etc/shadow >/dev/null && echo "Works"'` + +For example: + +```text +[ xmanning@dreadfort:~/git/kubernetes-playground ] (master) $ ssh ansible@kube-0 'sudo cat /etc/shadow >/dev/null && echo "Works"' +Works +[ xmanning@dreadfort:~/git/kubernetes-playground ] (master) $ +``` + +## Directory structure + +Our working directory will have the following files: + +```text +kubernetes-playground/ + |_ inventory.yml + |_ ha_cluster.yml +``` + +## Inventory + +Here's a YAML based example inventory for our servers called `inventory.yml`: + +```yaml +--- + +# We're adding k3s_control_node to each host, this can be done in host_vars/ +# or group_vars/ as well - but for simplicity we are setting it here. +k3s_cluster: + hosts: + kube-0: + ansible_user: ansible + ansible_host: 10.10.9.2 + ansible_python_interpreter: /usr/bin/python3 + k3s_control_node: true + kube-1: + ansible_user: ansible + ansible_host: 10.10.9.3 + ansible_python_interpreter: /usr/bin/python3 + k3s_control_node: true + kube-2: + ansible_user: ansible + ansible_host: 10.10.9.4 + ansible_python_interpreter: /usr/bin/python3 + k3s_control_node: true + +``` + +We can test this works with `ansible -i inventory.yml -m ping all`, expected +result: + +```text +kube-0 | SUCCESS => { + "changed": false, + "ping": "pong" +} +kube-1 | SUCCESS => { + "changed": false, + "ping": "pong" +} +kube-2 | SUCCESS => { + "changed": false, + "ping": "pong" +} + +``` + +## Playbook + +Here is our playbook for the k3s cluster (`ha_cluster.yml`): + +```yaml +--- + +- name: Build a cluster with HA control plane + hosts: k3s_cluster + vars: + k3s_become: true + k3s_etcd_datastore: true + k3s_use_experimental: true # Note this is required for k3s < v1.19.5+k3s1 + roles: + - role: xanmanning.k3s +``` + +## Execution + +To execute the playbook against our inventory file, we will run the following +command: + +`ansible-playbook -i inventory.yml ha_cluster.yml` + +The output we can expect is similar to the below, with no failed or unreachable +nodes. The default behavior of this role is to delegate the first play host as +the primary control node, so kube-0 will have more changed tasks than others: + +```text +PLAY RECAP ******************************************************************************************************* +kube-0 : ok=53 changed=8 unreachable=0 failed=0 skipped=30 rescued=0 ignored=0 +kube-1 : ok=47 changed=10 unreachable=0 failed=0 skipped=28 rescued=0 ignored=0 +kube-2 : ok=47 changed=9 unreachable=0 failed=0 skipped=28 rescued=0 ignored=0 +``` + +## Testing + +After logging into any of the servers (it doesn't matter), we can test that k3s +is running across the cluster, that all nodes are ready and that everything is +ready to execute our Kubernetes workloads by running the following: + + - `sudo kubectl get nodes -o wide` + - `sudo kubectl get pods -o wide --all-namespaces` + +:hand: Note we are using `sudo` because we need to be root to access the +kube config for this node. This behavior can be changed with specifying +`write-kubeconfig-mode: '0644'` in `k3s_server`. + +**Get Nodes**: + +```text +ansible@kube-0:~$ sudo kubectl get nodes -o wide +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME +kube-0 Ready etcd,master 2m58s v1.19.4+k3s1 10.10.9.2 10.10.9.2 Ubuntu 20.04.1 LTS 5.4.0-56-generic containerd://1.4.1-k3s1 +kube-1 Ready etcd,master 2m22s v1.19.4+k3s1 10.10.9.3 10.10.9.3 Ubuntu 20.04.1 LTS 5.4.0-56-generic containerd://1.4.1-k3s1 +kube-2 Ready etcd,master 2m10s v1.19.4+k3s1 10.10.9.4 10.10.9.4 Ubuntu 20.04.1 LTS 5.4.0-56-generic containerd://1.4.1-k3s1 +``` + +**Get Pods**: + +```text +ansible@kube-0:~$ sudo kubectl get pods -o wide --all-namespaces +NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +kube-system coredns-66c464876b-rhgn6 1/1 Running 0 3m38s 10.42.0.2 kube-0 +kube-system helm-install-traefik-vwglv 0/1 Completed 0 3m39s 10.42.0.3 kube-0 +kube-system local-path-provisioner-7ff9579c6-d5xpb 1/1 Running 0 3m38s 10.42.0.5 kube-0 +kube-system metrics-server-7b4f8b595-nhbt8 1/1 Running 0 3m38s 10.42.0.4 kube-0 +kube-system svclb-traefik-9lzcq 2/2 Running 0 2m56s 10.42.1.2 kube-1 +kube-system svclb-traefik-vq487 2/2 Running 0 2m45s 10.42.2.2 kube-2 +kube-system svclb-traefik-wkwkk 2/2 Running 0 3m1s 10.42.0.7 kube-0 +kube-system traefik-5dd496474-lw6x8 1/1 Running 0 3m1s 10.42.0.6 kube-0 +``` diff --git a/roles/xanmanning.k3s/documentation/quickstart-single-node.md b/roles/xanmanning.k3s/documentation/quickstart-single-node.md new file mode 100644 index 00000000..a8dcf1fe --- /dev/null +++ b/roles/xanmanning.k3s/documentation/quickstart-single-node.md @@ -0,0 +1,121 @@ +# Quickstart: K3s single node + +This is the quickstart guide to creating your own single-node k3s "cluster". + +:hand: This example requires your Ansible user to be able to connect to the +server over SSH using key-based authentication. The user is also has an entry +in a sudoers file that allows privilege escalation without requiring a +password. + +To test this is the case, run the following check replacing `` +and ``. The expected output is `Works` + +`ssh @ 'sudo cat /etc/shadow >/dev/null && echo "Works"'` + +For example: + +```text +[ xmanning@dreadfort:~/git/kubernetes-playground ] (master) $ ssh ansible@kube-0 'sudo cat /etc/shadow >/dev/null && echo "Works"' +Works +[ xmanning@dreadfort:~/git/kubernetes-playground ] (master) $ +``` + +## Directory structure + +Our working directory will have the following files: + +```text +kubernetes-playground/ + |_ inventory.yml + |_ single_node.yml +``` + +## Inventory + +Here's a YAML based example inventory for our server called `inventory.yml`: + +```yaml +--- + +k3s_cluster: + hosts: + kube-0: + ansible_user: ansible + ansible_host: 10.10.9.2 + ansible_python_interpreter: /usr/bin/python3 + +``` + +We can test this works with `ansible -i inventory.yml -m ping all`, expected +result: + +```text +kube-0 | SUCCESS => { + "changed": false, + "ping": "pong" +} +``` + +## Playbook + +Here is our playbook for a single node k3s cluster (`single_node.yml`): + +```yaml +--- + +- name: Build a single node k3s cluster + hosts: kube-0 + vars: + k3s_become: true + roles: + - role: xanmanning.k3s +``` + +## Execution + +To execute the playbook against our inventory file, we will run the following +command: + +`ansible-playbook -i inventory.yml single_node.yml` + +The output we can expect is similar to the below, with no failed or unreachable +nodes: + +```text +PLAY RECAP ******************************************************************************************************* +kube-0 : ok=39 changed=8 unreachable=0 failed=0 skipped=39 rescued=0 ignored=0 +``` + +## Testing + +After logging into the server, we can test that k3s is running and that it is +ready to execute our Kubernetes workloads by running the following: + + - `sudo kubectl get nodes` + - `sudo kubectl get pods -o wide --all-namespaces` + +:hand: Note we are using `sudo` because we need to be root to access the +kube config for this node. This behavior can be changed with specifying +`write-kubeconfig-mode: '0644'` in `k3s_server`. + +**Get Nodes**: + +```text +ansible@kube-0:~$ sudo kubectl get nodes +NAME STATUS ROLES AGE VERSION +kube-0 Ready master 5m27s v1.19.4+k3s +ansible@kube-0:~$ +``` + +**Get Pods**: + +```text +ansible@kube-0:~$ sudo kubectl get pods --all-namespaces -o wide +NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +kube-system metrics-server-7b4f8b595-k692h 1/1 Running 0 9m38s 10.42.0.2 kube-0 +kube-system local-path-provisioner-7ff9579c6-5lgzb 1/1 Running 0 9m38s 10.42.0.3 kube-0 +kube-system coredns-66c464876b-xg42q 1/1 Running 0 9m38s 10.42.0.5 kube-0 +kube-system helm-install-traefik-tdpcs 0/1 Completed 0 9m38s 10.42.0.4 kube-0 +kube-system svclb-traefik-hk248 2/2 Running 0 9m4s 10.42.0.7 kube-0 +kube-system traefik-5dd496474-bf4kv 1/1 Running 0 9m4s 10.42.0.6 kube-0 +``` diff --git a/roles/xanmanning.k3s/handlers/main.yml b/roles/xanmanning.k3s/handlers/main.yml new file mode 100644 index 00000000..c0ef83a9 --- /dev/null +++ b/roles/xanmanning.k3s/handlers/main.yml @@ -0,0 +1,39 @@ +--- + +- name: Reload systemd + ansible.builtin.systemd: + daemon_reload: true + scope: "{{ k3s_systemd_context }}" + become: "{{ k3s_become }}" + +- name: Reload service + ansible.builtin.set_fact: + k3s_service_reloaded: true + become: "{{ k3s_become }}" + +- name: Restart k3s systemd + ansible.builtin.systemd: + name: k3s + state: restarted + scope: "{{ k3s_systemd_context }}" + enabled: "{{ k3s_start_on_boot }}" + retries: 3 + delay: 3 + register: k3s_systemd_restart_k3s + failed_when: + - k3s_systemd_restart_k3s is not success + - not ansible_check_mode + become: "{{ k3s_become }}" + +- name: Restart k3s service + ansible.builtin.service: + name: k3s + state: restarted + enabled: "{{ k3s_start_on_boot }}" + retries: 3 + delay: 3 + register: k3s_service_restart_k3s + failed_when: + - k3s_service_restart_k3s is not success + - not ansible_check_mode + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/meta/main.yml b/roles/xanmanning.k3s/meta/main.yml new file mode 100644 index 00000000..89958fb4 --- /dev/null +++ b/roles/xanmanning.k3s/meta/main.yml @@ -0,0 +1,86 @@ +--- + +galaxy_info: + role_name: k3s + namespace: xanmanning + author: Xan Manning + description: Ansible role for installing k3s as either a standalone server or HA cluster + company: Pyrat Ltd. + github_branch: main + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Some suggested licenses: + # - BSD (default) + # - MIT + # - GPLv2 + # - GPLv3 + # - Apache + # - CC-BY + license: BSD + + min_ansible_version: '2.9' + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # Optionally specify the branch Galaxy will use when accessing the GitHub + # repo for this role. During role install, if no tags are available, + # Galaxy will use this branch. During import Galaxy will access files on + # this branch. If Travis integration is configured, only notifications for this + # branch will be accepted. Otherwise, in all cases, the repo's default branch + # (usually main) will be used. + # github_branch: + + # + # platforms is a list of platforms, and each platform has a name and a list of versions. + # + platforms: + - name: Alpine + versions: + - all + - name: Archlinux + versions: + - all + - name: EL + versions: + - 7 + - 8 + - name: Amazon + - name: Fedora + versions: + - 29 + - 30 + - 31 + - name: Debian + versions: + - buster + - jessie + - stretch + - name: SLES + versions: + - 15 + - name: Ubuntu + versions: + - xenial + - bionic + + galaxy_tags: + - k3s + - k8s + - kubernetes + - containerd + - cluster + - lightweight + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] +# List your role dependencies here, one per line. Be sure to remove the '[]' above, +# if you add dependencies to this list. diff --git a/roles/xanmanning.k3s/molecule/autodeploy/converge.yml b/roles/xanmanning.k3s/molecule/autodeploy/converge.yml new file mode 100644 index 00000000..e4749288 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/autodeploy/converge.yml @@ -0,0 +1,28 @@ +--- +- name: Converge + hosts: node* + become: true + vars: + molecule_is_test: true + k3s_release_version: v1.22 + k3s_build_cluster: false + k3s_control_token: 55ba04e5-e17d-4535-9170-3e4245453f4d + k3s_install_dir: /opt/k3s/bin + k3s_config_file: /opt/k3s/etc/k3s_config.yaml + k3s_server: + data-dir: /var/lib/k3s-io + default-local-storage-path: /var/lib/k3s-io/local-storage + disable: + - metrics-server + - traefik + # k3s_agent: + # snapshotter: native + k3s_server_manifests_templates: + - "molecule/autodeploy/templates/00-ns-monitoring.yml.j2" + k3s_server_manifests_urls: + - url: https://raw.githubusercontent.com/metallb/metallb/v0.9.6/manifests/namespace.yaml + filename: 05-metallb-namespace.yml + k3s_service_env_vars: + K3S_TEST_VAR: "Hello world!" + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/autodeploy/molecule.yml b/roles/xanmanning.k3s/molecule/autodeploy/molecule.yml new file mode 100644 index 00000000..dce188f9 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/autodeploy/molecule.yml @@ -0,0 +1,60 @@ +--- + +dependency: + name: galaxy +driver: + name: docker +scenario: + test_sequence: + - dependency + - lint + - cleanup + - destroy + - syntax + - create + - prepare + - check + - converge + - idempotence + - side_effect + - verify + - cleanup + - destroy +lint: | + set -e + yamllint -s . + ansible-lint --exclude molecule/ --exclude meta/ +platforms: + - name: node1 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node2 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node3 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet +provisioner: + name: ansible + options: + verbose: true +verifier: + name: ansible diff --git a/roles/xanmanning.k3s/molecule/autodeploy/prepare.yml b/roles/xanmanning.k3s/molecule/autodeploy/prepare.yml new file mode 100644 index 00000000..01d84eb0 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/autodeploy/prepare.yml @@ -0,0 +1,26 @@ +--- +- name: Prepare + hosts: node* + become: true + tasks: + - name: Ensure apt cache is updated and iptables is installed + ansible.builtin.apt: + name: iptables + state: present + update_cache: true + when: ansible_pkg_mgr == 'apt' + + - name: Ensure install directory and configuration directory exists + ansible.builtin.file: + path: "/opt/k3s/{{ item }}" + state: directory + mode: 0755 + loop: + - bin + - etc + + - name: Ensure data directory exists + ansible.builtin.file: + path: "/var/lib/k3s-io" + state: directory + mode: 0755 diff --git a/roles/xanmanning.k3s/molecule/autodeploy/templates/00-ns-monitoring.yml.j2 b/roles/xanmanning.k3s/molecule/autodeploy/templates/00-ns-monitoring.yml.j2 new file mode 100644 index 00000000..d3252360 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/autodeploy/templates/00-ns-monitoring.yml.j2 @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: monitoring diff --git a/roles/xanmanning.k3s/molecule/autodeploy/verify.yml b/roles/xanmanning.k3s/molecule/autodeploy/verify.yml new file mode 100644 index 00000000..86afba4f --- /dev/null +++ b/roles/xanmanning.k3s/molecule/autodeploy/verify.yml @@ -0,0 +1,9 @@ +--- +# This is an example playbook to execute Ansible tests. + +- name: Verify + hosts: all + tasks: + - name: Example assertion + ansible.builtin.assert: + that: true diff --git a/roles/xanmanning.k3s/molecule/debug/converge.yml b/roles/xanmanning.k3s/molecule/debug/converge.yml new file mode 100644 index 00000000..ff910a0c --- /dev/null +++ b/roles/xanmanning.k3s/molecule/debug/converge.yml @@ -0,0 +1,14 @@ +--- +- name: Converge + hosts: all + become: true + vars: + pyratlabs_issue_controller_dump: true + k3s_agent: + snapshotter: native + pre_tasks: + - name: Ensure k3s_debug is set + ansible.builtin.set_fact: + k3s_debug: true + roles: + - xanmanning.k3s diff --git a/roles/xanmanning.k3s/molecule/debug/molecule.yml b/roles/xanmanning.k3s/molecule/debug/molecule.yml new file mode 100644 index 00000000..dce188f9 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/debug/molecule.yml @@ -0,0 +1,60 @@ +--- + +dependency: + name: galaxy +driver: + name: docker +scenario: + test_sequence: + - dependency + - lint + - cleanup + - destroy + - syntax + - create + - prepare + - check + - converge + - idempotence + - side_effect + - verify + - cleanup + - destroy +lint: | + set -e + yamllint -s . + ansible-lint --exclude molecule/ --exclude meta/ +platforms: + - name: node1 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node2 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node3 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet +provisioner: + name: ansible + options: + verbose: true +verifier: + name: ansible diff --git a/roles/xanmanning.k3s/molecule/debug/prepare.yml b/roles/xanmanning.k3s/molecule/debug/prepare.yml new file mode 100644 index 00000000..5ae730a3 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/debug/prepare.yml @@ -0,0 +1,10 @@ +--- +- name: Prepare + hosts: all + tasks: + - name: Ensure apt cache is updated and iptables is installed + ansible.builtin.apt: + name: iptables + state: present + update_cache: true + when: ansible_pkg_mgr == 'apt' diff --git a/roles/xanmanning.k3s/molecule/debug/verify.yml b/roles/xanmanning.k3s/molecule/debug/verify.yml new file mode 100644 index 00000000..86afba4f --- /dev/null +++ b/roles/xanmanning.k3s/molecule/debug/verify.yml @@ -0,0 +1,9 @@ +--- +# This is an example playbook to execute Ansible tests. + +- name: Verify + hosts: all + tasks: + - name: Example assertion + ansible.builtin.assert: + that: true diff --git a/roles/xanmanning.k3s/molecule/default/Dockerfile.j2 b/roles/xanmanning.k3s/molecule/default/Dockerfile.j2 new file mode 100644 index 00000000..1aeeaa5e --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/Dockerfile.j2 @@ -0,0 +1,26 @@ +# Molecule managed + +{% if item.registry is defined %} +FROM {{ item.registry.url }}/{{ item.image }} +{% else %} +FROM {{ item.image }} +{% endif %} + +RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python systemd sudo bash ca-certificates && apt-get clean; \ + elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python systemd sudo python-devel python*-dnf bash && dnf clean all; \ + elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python systemd sudo yum-plugin-ovl bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \ + elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python systemd sudo bash python-xml && zypper clean -a; \ + elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo systemd bash ca-certificates; \ + elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python systemd sudo bash ca-certificates && xbps-remove -O; fi + +RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \ + rm -f /lib/systemd/system/multi-user.target.wants/*; \ + rm -f /etc/systemd/system/*.wants/*; \ + rm -f /lib/systemd/system/local-fs.target.wants/*; \ + rm -f /lib/systemd/system/sockets.target.wants/*udev*; \ + rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \ + rm -f /lib/systemd/system/basic.target.wants/*; \ + rm -f /lib/systemd/system/anaconda.target.wants/*; + +VOLUME [“/sys/fs/cgroup”] +CMD [“/usr/sbin/init”] diff --git a/roles/xanmanning.k3s/molecule/default/INSTALL.rst b/roles/xanmanning.k3s/molecule/default/INSTALL.rst new file mode 100644 index 00000000..6a44bde9 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/INSTALL.rst @@ -0,0 +1,22 @@ +******* +Docker driver installation guide +******* + +Requirements +============ + +* Docker Engine + +Install +======= + +Please refer to the `Virtual environment`_ documentation for installation best +practices. If not using a virtual environment, please consider passing the +widely recommended `'--user' flag`_ when invoking ``pip``. + +.. _Virtual environment: https://virtualenv.pypa.io/en/latest/ +.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site + +.. code-block:: bash + + $ pip install 'molecule[docker]' diff --git a/roles/xanmanning.k3s/molecule/default/converge.yml b/roles/xanmanning.k3s/molecule/default/converge.yml new file mode 100644 index 00000000..d3affa3c --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/converge.yml @@ -0,0 +1,12 @@ +--- +- name: Converge + hosts: all + become: true + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" + vars: + molecule_is_test: true + k3s_install_hard_links: true + k3s_release_version: stable + k3s_agent: + snapshotter: native diff --git a/roles/xanmanning.k3s/molecule/default/molecule.yml b/roles/xanmanning.k3s/molecule/default/molecule.yml new file mode 100644 index 00000000..dce188f9 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/molecule.yml @@ -0,0 +1,60 @@ +--- + +dependency: + name: galaxy +driver: + name: docker +scenario: + test_sequence: + - dependency + - lint + - cleanup + - destroy + - syntax + - create + - prepare + - check + - converge + - idempotence + - side_effect + - verify + - cleanup + - destroy +lint: | + set -e + yamllint -s . + ansible-lint --exclude molecule/ --exclude meta/ +platforms: + - name: node1 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node2 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node3 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet +provisioner: + name: ansible + options: + verbose: true +verifier: + name: ansible diff --git a/roles/xanmanning.k3s/molecule/default/playbook-download.yml b/roles/xanmanning.k3s/molecule/default/playbook-download.yml new file mode 100644 index 00000000..4635022c --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/playbook-download.yml @@ -0,0 +1,9 @@ +--- +- name: Converge + hosts: all + become: true + vars: + molecule_is_test: true + k3s_state: downloaded + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/default/playbook-restart-cluster.yml b/roles/xanmanning.k3s/molecule/default/playbook-restart-cluster.yml new file mode 100644 index 00000000..33d06e85 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/playbook-restart-cluster.yml @@ -0,0 +1,9 @@ +--- +- name: Converge + hosts: all + become: true + vars: + molecule_is_test: true + k3s_state: restarted + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/default/playbook-rootless.yml b/roles/xanmanning.k3s/molecule/default/playbook-rootless.yml new file mode 100644 index 00000000..5c0af5bc --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/playbook-rootless.yml @@ -0,0 +1,15 @@ +--- +- name: Converge + hosts: node1 + become: true + become_user: k3suser + vars: + molecule_is_test: true + k3s_use_experimental: true + k3s_server: + rootless: true + k3s_agent: + rootless: true + k3s_install_dir: "/home/{{ ansible_user_id }}/bin" + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/default/playbook-standalone.yml b/roles/xanmanning.k3s/molecule/default/playbook-standalone.yml new file mode 100644 index 00000000..86938ed0 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/playbook-standalone.yml @@ -0,0 +1,9 @@ +--- +- name: Converge + hosts: all + become: true + vars: + molecule_is_test: true + k3s_build_cluster: false + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/default/playbook-start-cluster.yml b/roles/xanmanning.k3s/molecule/default/playbook-start-cluster.yml new file mode 100644 index 00000000..3c31263e --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/playbook-start-cluster.yml @@ -0,0 +1,9 @@ +--- +- name: Converge + hosts: all + become: true + vars: + molecule_is_test: true + k3s_state: started + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/default/playbook-stop-cluster.yml b/roles/xanmanning.k3s/molecule/default/playbook-stop-cluster.yml new file mode 100644 index 00000000..e736c126 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/playbook-stop-cluster.yml @@ -0,0 +1,9 @@ +--- +- name: Converge + hosts: all + become: true + vars: + molecule_is_test: true + k3s_state: stopped + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/default/playbook-uninstall-cluster.yml b/roles/xanmanning.k3s/molecule/default/playbook-uninstall-cluster.yml new file mode 100644 index 00000000..c84ec57d --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/playbook-uninstall-cluster.yml @@ -0,0 +1,9 @@ +--- +- name: Converge + hosts: all + become: true + vars: + molecule_is_test: true + k3s_state: uninstalled + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/default/prepare-rootless.yml b/roles/xanmanning.k3s/molecule/default/prepare-rootless.yml new file mode 100644 index 00000000..d02d3e5f --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/prepare-rootless.yml @@ -0,0 +1,23 @@ +--- +- name: Prepare + hosts: node1 + become: true + tasks: + - name: Ensure a user group exists + ansible.builtin.group: + name: user + state: present + + - name: Ensure a normal user exists + ansible.builtin.user: + name: k3suser + group: user + state: present + + - name: Ensure a normal user has bin directory + ansible.builtin.file: + path: /home/k3suser/bin + state: directory + owner: k3suser + group: user + mode: 0700 diff --git a/roles/xanmanning.k3s/molecule/default/prepare.yml b/roles/xanmanning.k3s/molecule/default/prepare.yml new file mode 100644 index 00000000..5ae730a3 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/prepare.yml @@ -0,0 +1,10 @@ +--- +- name: Prepare + hosts: all + tasks: + - name: Ensure apt cache is updated and iptables is installed + ansible.builtin.apt: + name: iptables + state: present + update_cache: true + when: ansible_pkg_mgr == 'apt' diff --git a/roles/xanmanning.k3s/molecule/default/tests/test_default.py b/roles/xanmanning.k3s/molecule/default/tests/test_default.py new file mode 100644 index 00000000..eedd64a1 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/default/tests/test_default.py @@ -0,0 +1,14 @@ +import os + +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') + + +def test_hosts_file(host): + f = host.file('/etc/hosts') + + assert f.exists + assert f.user == 'root' + assert f.group == 'root' diff --git a/roles/xanmanning.k3s/molecule/default/tests/test_default.pyc b/roles/xanmanning.k3s/molecule/default/tests/test_default.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28c2e2d42688dc5ec97e4aa0c0223353ad18569d GIT binary patch literal 1025 zcmdr~&2H2%5FRJnUs3jiP%k~_(#@gbh7eViszu9I+5!?Qp(st9)>*F|*^?DjyBFZ$ z;3aqo9-tjNoBqB5`5BMDjK7)rCgH!W&A-3HZ#gWlkk zB^iA>8GlU=kB14m^qG`7qcvE*2ejHDnQKbrE20Do2G^92f!zTN;77X+T_Mu|?A`;g z*MUb%klH$pT&9uTBpY+g;|p!kVvn{)YxL#%T`45TRf73<;UZp=yKgNwD6~q9(Z+{0 z4sku68{O2AE8XLc7F{!CL!rdcZIS@9q&%%g_ysIXIk>Sk^J7twVn#X z5V?tM&Q)fFj_2HnQV@dNqEMyD;;XBNp@WVoErj7YYIC_*JdNq-QkBj4^Uqz9M-(n$ zTJSP!Bo69J>{B-$^Ae^`unmIjIvHU7pwC{h$mvXD0w6thoZnSFXi!Ky$dnallBcFo zij;H&cOhO^ALiA661qj9`|as*dw7iyuI9pM<%!NYrtNsUQoUX+Hg=WW&*py|kQ>C? UnJ(zBdgp#cKbaQ+-E2F&1#loHZU6uP literal 0 HcmV?d00001 diff --git a/roles/xanmanning.k3s/molecule/highavailabilitydb/Dockerfile.j2 b/roles/xanmanning.k3s/molecule/highavailabilitydb/Dockerfile.j2 new file mode 100644 index 00000000..36166d64 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilitydb/Dockerfile.j2 @@ -0,0 +1,7 @@ +# Molecule managed + +{% if item.registry is defined %} +FROM {{ item.registry.url }}/{{ item.image }} +{% else %} +FROM {{ item.image }} +{% endif %} diff --git a/roles/xanmanning.k3s/molecule/highavailabilitydb/INSTALL.rst b/roles/xanmanning.k3s/molecule/highavailabilitydb/INSTALL.rst new file mode 100644 index 00000000..6a44bde9 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilitydb/INSTALL.rst @@ -0,0 +1,22 @@ +******* +Docker driver installation guide +******* + +Requirements +============ + +* Docker Engine + +Install +======= + +Please refer to the `Virtual environment`_ documentation for installation best +practices. If not using a virtual environment, please consider passing the +widely recommended `'--user' flag`_ when invoking ``pip``. + +.. _Virtual environment: https://virtualenv.pypa.io/en/latest/ +.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site + +.. code-block:: bash + + $ pip install 'molecule[docker]' diff --git a/roles/xanmanning.k3s/molecule/highavailabilitydb/converge.yml b/roles/xanmanning.k3s/molecule/highavailabilitydb/converge.yml new file mode 100644 index 00000000..b66a41c3 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilitydb/converge.yml @@ -0,0 +1,21 @@ +--- + +- name: Converge + hosts: node* + become: true + vars: + molecule_is_test: true + k3s_registration_address: loadbalancer + k3s_control_token: 55ba04e5-e17d-4535-9170-3e4245453f4d + k3s_server: + datastore-endpoint: "postgres://postgres:verybadpass@database:5432/postgres?sslmode=disable" + k3s_agent: + snapshotter: native + k3s_service_env_file: /tmp/k3s.env + pre_tasks: + - name: Set each node to be a control node + ansible.builtin.set_fact: + k3s_control_node: true + when: inventory_hostname in ['node2', 'node3'] + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/highavailabilitydb/haproxy-loadbalancer.conf.j2 b/roles/xanmanning.k3s/molecule/highavailabilitydb/haproxy-loadbalancer.conf.j2 new file mode 100644 index 00000000..78fe9d3b --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilitydb/haproxy-loadbalancer.conf.j2 @@ -0,0 +1,13 @@ +frontend loadbalancer + bind *:6443 + mode tcp + default_backend control_nodes + timeout client 1m + +backend control_nodes + mode tcp + balance roundrobin + server node2 node2:6443 + server node3 node3:6443 + timeout connect 30s + timeout server 30m diff --git a/roles/xanmanning.k3s/molecule/highavailabilitydb/molecule.yml b/roles/xanmanning.k3s/molecule/highavailabilitydb/molecule.yml new file mode 100644 index 00000000..7adbd77a --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilitydb/molecule.yml @@ -0,0 +1,73 @@ +--- + +dependency: + name: galaxy +driver: + name: docker +scenario: + test_sequence: + - dependency + - lint + - cleanup + - destroy + - syntax + - create + - prepare + - check + - converge + - idempotence + - side_effect + - verify + - cleanup + - destroy +lint: | + set -e + yamllint -s . + ansible-lint --exclude molecule/ --exclude meta/ +platforms: + - name: node1 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node2 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node3 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: database + image: postgres:11-alpine + pre_build_image: true + command: "postgres" + env: + POSTGRES_PASSWORD: "verybadpass" + networks: + - name: k3snet + - name: loadbalancer + image: geerlingguy/docker-rockylinux8-ansible:latest + pre_build_image: true + ports: + - "6443:6443" + networks: + - name: k3snet +provisioner: + name: ansible + options: + verbose: true diff --git a/roles/xanmanning.k3s/molecule/highavailabilitydb/prepare.yml b/roles/xanmanning.k3s/molecule/highavailabilitydb/prepare.yml new file mode 100644 index 00000000..d9f55013 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilitydb/prepare.yml @@ -0,0 +1,48 @@ +--- +- name: Prepare Load Balancer + hosts: loadbalancer + tasks: + - name: Ensure apt cache is updated + ansible.builtin.apt: + update_cache: true + when: ansible_pkg_mgr == 'apt' + + - name: Ensure HAProxy is installed + ansible.builtin.package: + name: haproxy + state: present + + - name: Ensure HAProxy config directory exists + ansible.builtin.file: + path: /usr/local/etc/haproxy + state: directory + mode: 0755 + + - name: Ensure HAProxy is configured + ansible.builtin.template: + src: haproxy-loadbalancer.conf.j2 + dest: /usr/local/etc/haproxy/haproxy.cfg + mode: 0644 + + - name: Ensure HAProxy service is started + ansible.builtin.command: + cmd: haproxy -D -f /usr/local/etc/haproxy/haproxy.cfg -p /var/run/haproxy.pid + args: + creates: /var/run/haproxy.pid + +- name: Prepare nodes + hosts: node* + tasks: + - name: Ensure apt cache is updated and iptables is installed + ansible.builtin.apt: + name: iptables + state: present + update_cache: true + when: ansible_pkg_mgr == 'apt' + + - name: Ensure environment file exists for k3s_service_env_file + ansible.builtin.lineinfile: + path: /tmp/k3s.env + line: "THISHOST={{ ansible_hostname }}" + mode: 0644 + create: true diff --git a/roles/xanmanning.k3s/molecule/highavailabilitydb/tests/test_default.py b/roles/xanmanning.k3s/molecule/highavailabilitydb/tests/test_default.py new file mode 100644 index 00000000..eedd64a1 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilitydb/tests/test_default.py @@ -0,0 +1,14 @@ +import os + +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') + + +def test_hosts_file(host): + f = host.file('/etc/hosts') + + assert f.exists + assert f.user == 'root' + assert f.group == 'root' diff --git a/roles/xanmanning.k3s/molecule/highavailabilitydb/tests/test_default.pyc b/roles/xanmanning.k3s/molecule/highavailabilitydb/tests/test_default.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28c2e2d42688dc5ec97e4aa0c0223353ad18569d GIT binary patch literal 1025 zcmdr~&2H2%5FRJnUs3jiP%k~_(#@gbh7eViszu9I+5!?Qp(st9)>*F|*^?DjyBFZ$ z;3aqo9-tjNoBqB5`5BMDjK7)rCgH!W&A-3HZ#gWlkk zB^iA>8GlU=kB14m^qG`7qcvE*2ejHDnQKbrE20Do2G^92f!zTN;77X+T_Mu|?A`;g z*MUb%klH$pT&9uTBpY+g;|p!kVvn{)YxL#%T`45TRf73<;UZp=yKgNwD6~q9(Z+{0 z4sku68{O2AE8XLc7F{!CL!rdcZIS@9q&%%g_ysIXIk>Sk^J7twVn#X z5V?tM&Q)fFj_2HnQV@dNqEMyD;;XBNp@WVoErj7YYIC_*JdNq-QkBj4^Uqz9M-(n$ zTJSP!Bo69J>{B-$^Ae^`unmIjIvHU7pwC{h$mvXD0w6thoZnSFXi!Ky$dnallBcFo zij;H&cOhO^ALiA661qj9`|as*dw7iyuI9pM<%!NYrtNsUQoUX+Hg=WW&*py|kQ>C? UnJ(zBdgp#cKbaQ+-E2F&1#loHZU6uP literal 0 HcmV?d00001 diff --git a/roles/xanmanning.k3s/molecule/highavailabilityetcd/converge.yml b/roles/xanmanning.k3s/molecule/highavailabilityetcd/converge.yml new file mode 100644 index 00000000..b227774b --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilityetcd/converge.yml @@ -0,0 +1,22 @@ +--- + +- name: Converge + hosts: node* + become: true + vars: + molecule_is_test: true + k3s_use_experimental: true + k3s_etcd_datastore: true + k3s_server: + secrets-encryption: true + k3s_agent: + node-ip: "{{ ansible_default_ipv4.address }}" + snapshotter: native + k3s_skip_validation: "{{ k3s_service_handler[ansible_service_mgr] == 'service' }}" + k3s_skip_post_checks: "{{ ansible_os_family | lower == 'redhat' }}" + pre_tasks: + - name: Set each node to be a control node + ansible.builtin.set_fact: + k3s_control_node: true + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/highavailabilityetcd/haproxy-loadbalancer.conf.j2 b/roles/xanmanning.k3s/molecule/highavailabilityetcd/haproxy-loadbalancer.conf.j2 new file mode 100644 index 00000000..78fe9d3b --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilityetcd/haproxy-loadbalancer.conf.j2 @@ -0,0 +1,13 @@ +frontend loadbalancer + bind *:6443 + mode tcp + default_backend control_nodes + timeout client 1m + +backend control_nodes + mode tcp + balance roundrobin + server node2 node2:6443 + server node3 node3:6443 + timeout connect 30s + timeout server 30m diff --git a/roles/xanmanning.k3s/molecule/highavailabilityetcd/molecule.yml b/roles/xanmanning.k3s/molecule/highavailabilityetcd/molecule.yml new file mode 100644 index 00000000..ca67deb4 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilityetcd/molecule.yml @@ -0,0 +1,65 @@ +--- + +dependency: + name: galaxy +driver: + name: docker +scenario: + test_sequence: + - dependency + - lint + - cleanup + - destroy + - syntax + - create + - prepare + - check + - converge + - idempotence + - side_effect + - verify + - cleanup + - destroy +lint: | + set -e + yamllint -s . + ansible-lint --exclude molecule/ --exclude meta/ +platforms: + - name: node1 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node2 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node3 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: loadbalancer + image: geerlingguy/docker-rockylinux8-ansible:latest + pre_build_image: true + ports: + - "6443:6443" + networks: + - name: k3snet +provisioner: + name: ansible + options: + verbose: true diff --git a/roles/xanmanning.k3s/molecule/highavailabilityetcd/prepare.yml b/roles/xanmanning.k3s/molecule/highavailabilityetcd/prepare.yml new file mode 100644 index 00000000..18b95ae2 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/highavailabilityetcd/prepare.yml @@ -0,0 +1,59 @@ +--- + +- name: Prepare all nodes + hosts: all + tasks: + - name: Ensure apt cache is updated + ansible.builtin.apt: + update_cache: true + when: ansible_pkg_mgr == 'apt' + + - name: Ensure sudo is installed + community.general.apk: + name: sudo + state: present + update_cache: true + when: ansible_pkg_mgr == 'apk' + +- name: Prepare Load Balancer + hosts: loadbalancer + tasks: + - name: Ensure HAProxy is installed + ansible.builtin.package: + name: haproxy + state: present + + - name: Ensure HAProxy config directory exists + ansible.builtin.file: + path: /usr/local/etc/haproxy + state: directory + mode: 0755 + + - name: Ensure HAProxy is configured + ansible.builtin.template: + src: haproxy-loadbalancer.conf.j2 + dest: /usr/local/etc/haproxy/haproxy.cfg + mode: 0644 + + - name: Ensure HAProxy service is started + ansible.builtin.command: + cmd: haproxy -D -f /usr/local/etc/haproxy/haproxy.cfg -p /var/run/haproxy.pid + args: + creates: /var/run/haproxy.pid + +- name: Prepare nodes + hosts: node* + tasks: + - name: Ensure apt cache is updated and iptables is installed + ansible.builtin.apt: + name: iptables + state: present + update_cache: true + when: ansible_pkg_mgr == 'apt' + + - name: Ensure iproute is installed + ansible.builtin.dnf: + name: iproute + state: present + update_cache: true + when: ansible_pkg_mgr == 'dnf' diff --git a/roles/xanmanning.k3s/molecule/nodeploy/.gitignore b/roles/xanmanning.k3s/molecule/nodeploy/.gitignore new file mode 100644 index 00000000..200dcfbd --- /dev/null +++ b/roles/xanmanning.k3s/molecule/nodeploy/.gitignore @@ -0,0 +1 @@ +files/* diff --git a/roles/xanmanning.k3s/molecule/nodeploy/converge.yml b/roles/xanmanning.k3s/molecule/nodeploy/converge.yml new file mode 100644 index 00000000..d016b35f --- /dev/null +++ b/roles/xanmanning.k3s/molecule/nodeploy/converge.yml @@ -0,0 +1,12 @@ +--- +- name: Converge + hosts: all + become: true + vars: + molecule_is_test: true + k3s_server: "{{ lookup('file', 'k3s_server.yml') | from_yaml }}" + k3s_agent: "{{ lookup('file', 'k3s_agent.yml') | from_yaml }}" + k3s_airgap: true + k3s_release_version: latest + roles: + - role: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" diff --git a/roles/xanmanning.k3s/molecule/nodeploy/k3s_agent.yml b/roles/xanmanning.k3s/molecule/nodeploy/k3s_agent.yml new file mode 100644 index 00000000..4ffc1ec9 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/nodeploy/k3s_agent.yml @@ -0,0 +1,9 @@ +--- + +node-label: + - "foo=bar" + - "hello=world" +kubelet-arg: + - "cloud-provider=external" + - "provider-id=azure" +snapshotter: native diff --git a/roles/xanmanning.k3s/molecule/nodeploy/k3s_server.yml b/roles/xanmanning.k3s/molecule/nodeploy/k3s_server.yml new file mode 100644 index 00000000..0bc74c20 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/nodeploy/k3s_server.yml @@ -0,0 +1,14 @@ +--- + +flannel-backend: 'none' +disable-scheduler: true +disable-cloud-controller: true +disable-network-policy: true +disable: + - coredns + - traefik + - servicelb + - local-storage + - metrics-server +node-taint: + - "k3s-controlplane=true:NoExecute" diff --git a/roles/xanmanning.k3s/molecule/nodeploy/molecule.yml b/roles/xanmanning.k3s/molecule/nodeploy/molecule.yml new file mode 100644 index 00000000..dce188f9 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/nodeploy/molecule.yml @@ -0,0 +1,60 @@ +--- + +dependency: + name: galaxy +driver: + name: docker +scenario: + test_sequence: + - dependency + - lint + - cleanup + - destroy + - syntax + - create + - prepare + - check + - converge + - idempotence + - side_effect + - verify + - cleanup + - destroy +lint: | + set -e + yamllint -s . + ansible-lint --exclude molecule/ --exclude meta/ +platforms: + - name: node1 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node2 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet + - name: node3 + image: ${MOLECULE_DISTRO:-"geerlingguy/docker-rockylinux8-ansible:latest"} + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + privileged: true + pre_build_image: ${MOLECULE_PREBUILT:-true} + networks: + - name: k3snet +provisioner: + name: ansible + options: + verbose: true +verifier: + name: ansible diff --git a/roles/xanmanning.k3s/molecule/nodeploy/prepare.yml b/roles/xanmanning.k3s/molecule/nodeploy/prepare.yml new file mode 100644 index 00000000..defa9299 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/nodeploy/prepare.yml @@ -0,0 +1,27 @@ +--- +- name: Prepare + hosts: all + tasks: + - name: Ensure apt cache is updated and iptables is installed + ansible.builtin.apt: + name: iptables + state: present + update_cache: true + when: ansible_pkg_mgr == 'apt' + + - name: Prepare air-gapped installation + delegate_to: localhost + run_once: true + block: + + - name: Ensure files directory exists + ansible.builtin.file: + path: ./files + state: directory + mode: 0750 + + - name: Ensure k3s is downloaded for air-gap installation + ansible.builtin.get_url: + url: https://github.com/k3s-io/k3s/releases/download/v1.22.5%2Bk3s1/k3s + dest: ./files/k3s + mode: 0755 diff --git a/roles/xanmanning.k3s/molecule/nodeploy/verify.yml b/roles/xanmanning.k3s/molecule/nodeploy/verify.yml new file mode 100644 index 00000000..86afba4f --- /dev/null +++ b/roles/xanmanning.k3s/molecule/nodeploy/verify.yml @@ -0,0 +1,9 @@ +--- +# This is an example playbook to execute Ansible tests. + +- name: Verify + hosts: all + tasks: + - name: Example assertion + ansible.builtin.assert: + that: true diff --git a/roles/xanmanning.k3s/molecule/requirements.txt b/roles/xanmanning.k3s/molecule/requirements.txt new file mode 100644 index 00000000..41787de0 --- /dev/null +++ b/roles/xanmanning.k3s/molecule/requirements.txt @@ -0,0 +1,6 @@ +-r ../requirements.txt + +molecule[docker]>=3.2 +docker>=4.3.1 +yamllint>=1.25.0 +ansible-lint>=4.3.5 diff --git a/roles/xanmanning.k3s/requirements.txt b/roles/xanmanning.k3s/requirements.txt new file mode 100644 index 00000000..96188f32 --- /dev/null +++ b/roles/xanmanning.k3s/requirements.txt @@ -0,0 +1 @@ +ansible>=2.10.7 diff --git a/roles/xanmanning.k3s/tasks/determine_systemd_context.yml b/roles/xanmanning.k3s/tasks/determine_systemd_context.yml new file mode 100644 index 00000000..5d4c4359 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/determine_systemd_context.yml @@ -0,0 +1,10 @@ +--- + +- name: Ensure systemd context is correct if we are running k3s rootless + ansible.builtin.set_fact: + k3s_systemd_context: user + k3s_systemd_unit_dir: "{{ ansible_user_dir }}/.config/systemd/user" + when: + - k3s_runtime_config is defined + - k3s_runtime_config.rootless is defined + - k3s_runtime_config.rootless diff --git a/roles/xanmanning.k3s/tasks/ensure_cluster.yml b/roles/xanmanning.k3s/tasks/ensure_cluster.yml new file mode 100644 index 00000000..8954b786 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_cluster.yml @@ -0,0 +1,106 @@ +--- + +- name: "Ensure cluster token is captured from {{ k3s_control_delegate }}" + ansible.builtin.slurp: + path: "{{ k3s_runtime_config['data-dir'] | default(k3s_data_dir) }}/server/token" + register: k3s_slurped_cluster_token + delegate_to: "{{ k3s_control_delegate }}" + when: + - k3s_control_token is not defined + - not ansible_check_mode + become: "{{ k3s_become }}" + +- name: Ensure cluster token is formatted correctly for use in templates + ansible.builtin.set_fact: + k3s_control_token_content: "{{ k3s_control_token | default(k3s_slurped_cluster_token.content | b64decode) }}" + when: + - k3s_control_token is not defined + - not ansible_check_mode + +- name: Ensure dummy cluster token is defined for ansible_check_mode + ansible.builtin.set_fact: + k3s_control_token_content: "{{ k3s_control_delegate | to_uuid }}" + check_mode: false + when: + - ansible_check_mode + +- name: Ensure the cluster token file location exists + ansible.builtin.file: + path: "{{ k3s_token_location | dirname }}" + state: directory + mode: 0755 + become: "{{ k3s_become }}" + +- name: Ensure k3s cluster token file is present + ansible.builtin.template: + src: cluster-token.j2 + dest: "{{ k3s_token_location }}" + mode: 0600 + become: "{{ k3s_become }}" + notify: + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + +- name: Ensure k3s service unit file is present + ansible.builtin.template: + src: k3s.service.j2 + dest: "{{ k3s_systemd_unit_dir }}/k3s.service" + mode: 0644 + become: "{{ k3s_become }}" + when: + - k3s_service_handler[ansible_service_mgr] == 'systemd' + notify: + - "Reload {{ k3s_service_handler[ansible_service_mgr] }}" + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + +- name: Ensure k3s service file is present + ansible.builtin.template: + src: k3s.openrc.j2 + dest: "{{ k3s_openrc_service_dir }}/k3s" + mode: 0744 + when: + - k3s_service_handler[ansible_service_mgr] == 'service' + notify: + - "Reload {{ k3s_service_handler[ansible_service_mgr] }}" + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + become: "{{ k3s_become }}" + +- name: Ensure k3s logrotate file is present + ansible.builtin.template: + src: k3s.logrotate.j2 + dest: "{{ k3s_logrotate_dir }}/k3s" + mode: 0640 + when: + - k3s_service_handler[ansible_service_mgr] == 'service' + notify: + - "Reload {{ k3s_service_handler[ansible_service_mgr] }}" + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + become: "{{ k3s_become }}" + +- name: Ensure k3s config file exists + ansible.builtin.template: + src: config.yaml.j2 + dest: "{{ k3s_config_file }}" + mode: 0644 + notify: + - "Reload {{ k3s_service_handler[ansible_service_mgr] }}" + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + become: "{{ k3s_become }}" + +- name: Ensure secondary controllers are started + include_tasks: ensure_control_plane_started_{{ ansible_service_mgr }}.yml + when: + - k3s_control_node + - not k3s_primary_control_node + +- import_tasks: post_checks_control_plane.yml + when: + - not k3s_skip_validation + - not k3s_skip_post_checks + +- name: Flush Handlers + ansible.builtin.meta: flush_handlers + +- import_tasks: post_checks_nodes.yml + when: + - not k3s_skip_validation + - not k3s_skip_post_checks diff --git a/roles/xanmanning.k3s/tasks/ensure_containerd_registries.yml b/roles/xanmanning.k3s/tasks/ensure_containerd_registries.yml new file mode 100644 index 00000000..12708157 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_containerd_registries.yml @@ -0,0 +1,11 @@ +--- + +- name: Ensure containerd registries file exists + ansible.builtin.template: + src: registries.yaml.j2 + dest: "{{ k3s_config_dir }}/registries.yaml" + mode: 0600 + notify: + - "Reload {{ k3s_service_handler[ansible_service_mgr] }}" + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/tasks/ensure_control_plane_started_openrc.yml b/roles/xanmanning.k3s/tasks/ensure_control_plane_started_openrc.yml new file mode 100644 index 00000000..fe9326d1 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_control_plane_started_openrc.yml @@ -0,0 +1,15 @@ +--- + +- name: Ensure k3s control plane server is started + ansible.builtin.service: + name: k3s + state: started + enabled: "{{ k3s_start_on_boot }}" + register: k3s_service_start_k3s + until: k3s_service_start_k3s is succeeded + retries: 3 + delay: 3 + failed_when: + - k3s_service_start_k3s is not succeeded + - not ansible_check_mode + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/tasks/ensure_control_plane_started_systemd.yml b/roles/xanmanning.k3s/tasks/ensure_control_plane_started_systemd.yml new file mode 100644 index 00000000..e2855d4e --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_control_plane_started_systemd.yml @@ -0,0 +1,16 @@ +--- + +- name: Ensure k3s control plane server is started + ansible.builtin.systemd: + name: k3s + state: started + enabled: "{{ k3s_start_on_boot }}" + scope: "{{ k3s_systemd_context }}" + register: k3s_systemd_start_k3s + until: k3s_systemd_start_k3s is succeeded + retries: 3 + delay: 3 + failed_when: + - k3s_systemd_start_k3s is not succeeded + - not ansible_check_mode + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/tasks/ensure_directories.yml b/roles/xanmanning.k3s/tasks/ensure_directories.yml new file mode 100644 index 00000000..21453b99 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_directories.yml @@ -0,0 +1,12 @@ +--- + +- name: Ensure {{ directory.name }} exists + ansible.builtin.file: + path: "{{ directory.path }}" + state: directory + mode: "{{ directory.mode | default(755) }}" + become: "{{ k3s_become }}" + when: + - directory.path is defined + - directory.path | length > 0 + - directory.path != omit diff --git a/roles/xanmanning.k3s/tasks/ensure_downloads.yml b/roles/xanmanning.k3s/tasks/ensure_downloads.yml new file mode 100644 index 00000000..4d12fecc --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_downloads.yml @@ -0,0 +1,51 @@ +--- + +- name: Ensure target host architecture information is set as a fact + ansible.builtin.set_fact: + k3s_arch: "{{ k3s_arch_lookup[ansible_architecture].arch }}" + k3s_arch_suffix: "{{ k3s_arch_lookup[ansible_architecture].suffix }}" + check_mode: false + +- name: Ensure URLs are set as facts for downloading binaries + ansible.builtin.set_fact: + k3s_binary_url: "{{ k3s_github_download_url }}/{{ k3s_release_version }}/k3s{{ k3s_arch_suffix }}" + k3s_hash_url: "{{ k3s_github_download_url }}/{{ k3s_release_version }}/sha256sum-{{ k3s_arch }}.txt" + check_mode: false + +- name: Override k3s_binary_url and k3s_hash_url facts for testing specific commit + ansible.builtin.set_fact: + k3s_binary_url: "https://storage.googleapis.com/k3s-ci-builds/k3s{{ k3s_arch_suffix }}-{{ k3s_release_version }}" + k3s_hash_url: "https://storage.googleapis.com/k3s-ci-builds/k3s{{ k3s_arch_suffix }}-{{ k3s_release_version }}.sha256sum" + when: + - k3s_release_version | regex_search("^[a-z0-9]{40}$") + check_mode: false + +- name: Ensure the k3s hashsum is downloaded + ansible.builtin.uri: + url: "{{ k3s_hash_url }}" + return_content: true + register: k3s_hash_sum_raw + check_mode: false + +- name: Ensure sha256sum is set from hashsum variable + ansible.builtin.set_fact: + k3s_hash_sum: "{{ (k3s_hash_sum_raw.content.split('\n') | + select('search', 'k3s' + k3s_arch_suffix) | + reject('search', 'images') | + first).split() | first }}" + changed_when: false + check_mode: false + +- name: Ensure installation directory exists + ansible.builtin.file: + path: "{{ k3s_install_dir }}" + state: directory + mode: 0755 + +- name: Ensure k3s binary is downloaded + ansible.builtin.get_url: + url: "{{ k3s_binary_url }}" + dest: "{{ k3s_install_dir }}/k3s-{{ k3s_release_version }}" + checksum: "sha256:{{ k3s_hash_sum }}" + mode: 0755 + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/tasks/ensure_drain_and_remove_nodes.yml b/roles/xanmanning.k3s/tasks/ensure_drain_and_remove_nodes.yml new file mode 100644 index 00000000..77dffbbe --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_drain_and_remove_nodes.yml @@ -0,0 +1,56 @@ +--- + +- name: Check if kubectl exists + ansible.builtin.stat: + path: "{{ k3s_install_dir }}/kubectl" + register: k3s_check_kubectl + become: "{{ k3s_become }}" + +- name: Clean up nodes that are in an uninstalled state + block: + + - name: Gather a list of nodes + ansible.builtin.command: + cmd: "{{ k3s_install_dir }}/kubectl get nodes" + changed_when: false + failed_when: false + delegate_to: "{{ k3s_control_delegate }}" + run_once: true + register: kubectl_get_nodes_result + become: "{{ k3s_become }}" + + - name: Ensure uninstalled nodes are drained + ansible.builtin.command: + cmd: >- + {{ k3s_install_dir }}/kubectl drain {{ item }} + --ignore-daemonsets + --{{ k3s_drain_command[ansible_version.string is version_compare('1.22', '>=')] }} + --force + delegate_to: "{{ k3s_control_delegate }}" + run_once: true + when: + - kubectl_get_nodes_result.stdout is defined + - item in kubectl_get_nodes_result.stdout + - hostvars[item].k3s_state is defined + - hostvars[item].k3s_state == 'uninstalled' + loop: "{{ ansible_play_hosts }}" + become: "{{ k3s_become }}" + + - name: Ensure uninstalled nodes are removed + ansible.builtin.command: + cmd: "{{ k3s_install_dir }}/kubectl delete node {{ item }}" + delegate_to: "{{ k3s_control_delegate }}" + run_once: true + when: + - kubectl_get_nodes_result.stdout is defined + - item in kubectl_get_nodes_result.stdout + - hostvars[item].k3s_state is defined + - hostvars[item].k3s_state == 'uninstalled' + loop: "{{ ansible_play_hosts }}" + become: "{{ k3s_become }}" + + when: + - k3s_check_kubectl.stat.exists is defined + - k3s_check_kubectl.stat.exists + - k3s_control_delegate is defined + - not ansible_check_mode diff --git a/roles/xanmanning.k3s/tasks/ensure_installed.yml b/roles/xanmanning.k3s/tasks/ensure_installed.yml new file mode 100644 index 00000000..fc19ea94 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_installed.yml @@ -0,0 +1,28 @@ +--- + +- include_tasks: ensure_directories.yml + loop: "{{ k3s_ensure_directories_exist }}" + loop_control: + loop_var: directory + +- include_tasks: ensure_installed_node.yml + when: + - ((k3s_control_node and k3s_controller_list | length == 1) + or (k3s_primary_control_node and k3s_controller_list | length > 1)) + - not ansible_check_mode + +- name: Flush Handlers + ansible.builtin.meta: flush_handlers + +- include_tasks: ensure_installed_node.yml + when: k3s_build_cluster + +- name: Determine if the systems are already clustered + ansible.builtin.stat: + path: "{{ k3s_token_location }}" + register: k3s_token_cluster_check + +- include_tasks: ensure_control_plane_started_{{ ansible_service_mgr }}.yml + when: (k3s_control_node and k3s_controller_list | length == 1) + or (k3s_primary_control_node and k3s_controller_list | length > 1) + or k3s_token_cluster_check.stat.exists diff --git a/roles/xanmanning.k3s/tasks/ensure_installed_node.yml b/roles/xanmanning.k3s/tasks/ensure_installed_node.yml new file mode 100644 index 00000000..df850513 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_installed_node.yml @@ -0,0 +1,103 @@ +--- + +- name: Ensure k3s is linked into the installation destination + ansible.builtin.file: + src: "{{ k3s_install_dir }}/k3s-{{ k3s_release_version }}" + dest: "{{ k3s_install_dir }}/{{ item }}" + state: "{{ 'hard' if k3s_install_hard_links else 'link' }}" + force: "{{ k3s_install_hard_links }}" + mode: 0755 + loop: + - k3s + - kubectl + - crictl + - ctr + when: not ansible_check_mode + notify: + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + become: "{{ k3s_become }}" + +- name: Ensure k3s config file exists + ansible.builtin.template: + src: config.yaml.j2 + dest: "{{ k3s_config_file }}" + mode: 0644 + notify: + - "Reload {{ k3s_service_handler[ansible_service_mgr] }}" + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + become: "{{ k3s_become }}" + +- name: Ensure cluster token is present when pre-defined + block: + - name: Ensure the cluster token file location exists + ansible.builtin.file: + path: "{{ k3s_token_location | dirname }}" + state: directory + mode: 0755 + become: "{{ k3s_become }}" + + - name: Ensure k3s cluster token file is present + ansible.builtin.template: + src: cluster-token.j2 + dest: "{{ k3s_token_location }}" + mode: 0600 + become: "{{ k3s_become }}" + notify: + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + when: k3s_control_token is defined + +- name: Ensure k3s service unit file is present + ansible.builtin.template: + src: k3s.service.j2 + dest: "{{ k3s_systemd_unit_dir }}/k3s.service" + mode: 0644 + when: + - k3s_service_handler[ansible_service_mgr] == 'systemd' + notify: + - "Reload {{ k3s_service_handler[ansible_service_mgr] }}" + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + become: "{{ k3s_become }}" + +- name: Ensure k3s service file is present + ansible.builtin.template: + src: k3s.openrc.j2 + dest: "{{ k3s_openrc_service_dir }}/k3s" + mode: 0744 + when: + - k3s_service_handler[ansible_service_mgr] == 'service' + notify: + - "Reload {{ k3s_service_handler[ansible_service_mgr] }}" + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + become: "{{ k3s_become }}" + +- name: Ensure k3s logrotate file is present + ansible.builtin.template: + src: k3s.logrotate.j2 + dest: "{{ k3s_logrotate_dir }}/k3s" + mode: 0640 + when: + - k3s_service_handler[ansible_service_mgr] == 'service' + notify: + - "Reload {{ k3s_service_handler[ansible_service_mgr] }}" + - "Restart k3s {{ k3s_service_handler[ansible_service_mgr] }}" + become: "{{ k3s_become }}" + +- name: Ensure k3s killall script is present + ansible.builtin.template: + src: k3s-killall.sh.j2 + dest: "/usr/local/bin/k3s-killall.sh" + mode: 0700 + become: "{{ k3s_become }}" + when: + - k3s_runtime_config is defined + - ("rootless" not in k3s_runtime_config or not k3s_runtime_config.rootless) + +- name: Ensure k3s uninstall script is present + ansible.builtin.template: + src: k3s-uninstall.sh.j2 + dest: "/usr/local/bin/k3s-uninstall.sh" + mode: 0700 + become: "{{ k3s_become }}" + when: + - k3s_runtime_config is defined + - ("rootless" not in k3s_runtime_config or not k3s_runtime_config.rootless) diff --git a/roles/xanmanning.k3s/tasks/ensure_k3s_auto_deploy.yml b/roles/xanmanning.k3s/tasks/ensure_k3s_auto_deploy.yml new file mode 100644 index 00000000..ac638d82 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_k3s_auto_deploy.yml @@ -0,0 +1,62 @@ +--- + +- name: Ensure that the manifests directory exists + ansible.builtin.file: + state: directory + path: "{{ k3s_server_manifests_dir }}" + mode: 0755 + when: >- + k3s_server_manifests_templates | length > 0 + or k3s_server_manifests_urls | length > 0 + become: "{{ k3s_become }}" + +- name: Ensure that the pod-manifests directory exists + ansible.builtin.file: + state: directory + path: "{{ k3s_server_pod_manifests_dir }}" + mode: 0755 + when: >- + k3s_server_pod_manifests_templates | length > 0 + or k3s_server_pod_manifests_urls | length > 0 + become: "{{ k3s_become }}" + +# https://rancher.com/docs/k3s/latest/en/advanced/#auto-deploying-manifests +- name: Ensure auto-deploying manifests are copied to the primary controller + ansible.builtin.template: + src: "{{ item }}" + dest: "{{ k3s_server_manifests_dir }}/{{ item | basename | replace('.j2', '') }}" + mode: 0644 + loop: "{{ k3s_server_manifests_templates }}" + become: "{{ k3s_become }}" + when: + - k3s_server_manifests_templates | length > 0 + +- name: Ensure auto-deploying manifests are downloaded to the primary controller + ansible.builtin.get_url: + url: "{{ item.url }}" + dest: "{{ k3s_server_manifests_dir }}/{{ item.filename | default(item.url | basename) }}" + mode: 0644 + loop: "{{ k3s_server_manifests_urls }}" + become: "{{ k3s_become }}" + when: + - not ansible_check_mode + - k3s_server_manifests_urls | length > 0 + +# https://github.com/k3s-io/k3s/pull/1691 +- name: Ensure static pod manifests are copied to controllers + ansible.builtin.template: + src: "{{ item }}" + dest: "{{ k3s_server_pod_manifests_dir }}/{{ item | basename | replace('.j2', '') }}" + mode: 0644 + loop: "{{ k3s_server_pod_manifests_templates }}" + become: "{{ k3s_become }}" + +# https://rancher.com/docs/k3s/latest/en/advanced/#auto-deploying-manifests +- name: Ensure auto-deploying manifests are downloaded to the primary controller + ansible.builtin.get_url: + url: "{{ item.url }}" + dest: "{{ k3s_server_pod_manifests_dir }}/{{ item.filename | default(item.url | basename) }}" + mode: 0644 + loop: "{{ k3s_server_pod_manifests_urls }}" + become: "{{ k3s_become }}" + when: not ansible_check_mode diff --git a/roles/xanmanning.k3s/tasks/ensure_k3s_config_files.yml b/roles/xanmanning.k3s/tasks/ensure_k3s_config_files.yml new file mode 100644 index 00000000..78cb90aa --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_k3s_config_files.yml @@ -0,0 +1,31 @@ +--- + +- name: Ensure that the config.yaml.d directory exists + ansible.builtin.file: + state: directory + path: "{{ k3s_config_yaml_d_dir }}" + mode: 0755 + when: >- + k3s_server_config_yaml_d_files | length > 0 + or k3s_agent_config_yaml_d_files | length > 0 + become: "{{ k3s_become }}" + +# https://github.com/k3s-io/k3s/pull/3162 +- name: Ensure configuration files are copied to controllers + ansible.builtin.template: + src: "{{ item }}" + dest: "{{ k3s_config_yaml_d_dir }}/{{ item | basename | replace('.j2', '') }}" + mode: 0644 + loop: "{{ k3s_server_config_yaml_d_files }}" + become: "{{ k3s_become }}" + when: k3s_control_node + +# https://github.com/k3s-io/k3s/pull/3162 +- name: Ensure configuration files are copied to agents + ansible.builtin.template: + src: "{{ item }}" + dest: "{{ k3s_config_yaml_d_dir }}/{{ item | basename | replace('.j2', '') }}" + mode: 0644 + loop: "{{ k3s_agent_config_yaml_d_files }}" + become: "{{ k3s_become }}" + when: not k3s_control_node diff --git a/roles/xanmanning.k3s/tasks/ensure_pre_configuration.yml b/roles/xanmanning.k3s/tasks/ensure_pre_configuration.yml new file mode 100644 index 00000000..1d5bc063 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_pre_configuration.yml @@ -0,0 +1,129 @@ +--- + +- name: Ensure k3s_build_cluster is false if running against a single node. + ansible.builtin.set_fact: + k3s_build_cluster: false + when: + - ansible_play_hosts | length < 2 + - k3s_registration_address is not defined + +- name: Ensure k3s control node fact is set + ansible.builtin.set_fact: + k3s_control_node: "{{ not k3s_build_cluster }}" + when: k3s_control_node is not defined + +- name: Ensure k3s primary control node fact is set + ansible.builtin.set_fact: + k3s_primary_control_node: "{{ not k3s_build_cluster }}" + when: k3s_primary_control_node is not defined + +- name: Ensure k3s control plane port is captured + ansible.builtin.set_fact: + k3s_control_plane_port: "{{ k3s_runtime_config['https-listen-port'] | default(6443) }}" + delegate_to: k3s_primary_control_node + +- name: Ensure k3s node IP is configured when node-ip is defined + ansible.builtin.set_fact: + k3s_node_ip: "{{ k3s_runtime_config['node-ip'] }}" + when: + - k3s_runtime_config['node-ip'] is defined + +- name: Ensure a count of control nodes is generated from ansible_play_hosts + ansible.builtin.set_fact: + k3s_controller_list: "{{ k3s_controller_list + [item] }}" + when: + - hostvars[item].k3s_control_node is defined + - hostvars[item].k3s_control_node + loop: "{{ ansible_play_hosts }}" + +- name: Ensure a k3s control node is defined if none are found in ansible_play_hosts + block: + + - name: Set the control host + ansible.builtin.set_fact: + k3s_control_node: true + when: inventory_hostname == ansible_play_hosts[0] + + - name: Ensure a count of control nodes is generated + ansible.builtin.set_fact: + k3s_controller_list: "{{ k3s_controller_list + [item] }}" + when: + - hostvars[item].k3s_control_node is defined + - hostvars[item].k3s_control_node + loop: "{{ ansible_play_hosts }}" + + when: + - k3s_controller_list | length < 1 + - k3s_build_cluster is defined + - k3s_build_cluster + +- name: Ensure a primary k3s control node is defined if multiple are found in ansible_play_hosts + ansible.builtin.set_fact: + k3s_primary_control_node: true + when: + - k3s_controller_list is defined + - inventory_hostname == k3s_controller_list[0] + - k3s_build_cluster is defined + - k3s_build_cluster + +- name: Ensure ansible_host is mapped to inventory_hostname + ansible.builtin.blockinfile: + path: /tmp/inventory.txt + block: | + {% for host in ansible_play_hosts %} + {% filter replace('\n', ' ') %} + {{ host }} + @@@ + {{ hostvars[host].ansible_host | default(hostvars[host].ansible_fqdn) | string }} + @@@ + C_{{ hostvars[host].k3s_control_node | string }} + @@@ + P_{{ hostvars[host].k3s_primary_control_node | default(False) | string }} + {% endfilter %} + @@@ END:{{ host }} + {% endfor %} + create: true + mode: 0600 + check_mode: false + when: k3s_control_node is defined + +- name: Delegate an initializing control plane node + block: + - name: Lookup control node from file + ansible.builtin.command: + cmd: "grep -i '{{ 'P_True' if (k3s_controller_list | length > 1) else 'C_True' }}' /tmp/inventory.txt" + changed_when: false + check_mode: false + register: k3s_control_delegate_raw + + - name: Ensure control node is delegated for obtaining a cluster token + ansible.builtin.set_fact: + k3s_control_delegate: "{{ k3s_control_delegate_raw.stdout.split(' @@@ ')[0] }}" + check_mode: false + when: k3s_control_delegate is not defined + + - name: Ensure the node registration address is defined from k3s_control_node_address + ansible.builtin.set_fact: + k3s_registration_address: "{{ k3s_control_node_address }}" + check_mode: false + when: k3s_control_node_address is defined + + - name: Ensure the node registration address is defined from node-ip + ansible.builtin.set_fact: + k3s_registration_address: "{{ hostvars[k3s_control_delegate].k3s_node_ip }}" + check_mode: false + when: + - k3s_registration_address is not defined + - k3s_control_node_address is not defined + - hostvars[k3s_control_delegate].k3s_node_ip is defined + + - name: Ensure the node registration address is defined + ansible.builtin.set_fact: + k3s_registration_address: "{{ hostvars[k3s_control_delegate].ansible_host | default(hostvars[k3s_control_delegate].ansible_fqdn) }}" + check_mode: false + when: + - k3s_registration_address is not defined + - k3s_control_node_address is not defined + + when: k3s_registration_address is not defined + or k3s_control_delegate is not defined diff --git a/roles/xanmanning.k3s/tasks/ensure_started.yml b/roles/xanmanning.k3s/tasks/ensure_started.yml new file mode 100644 index 00000000..c4b934a3 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_started.yml @@ -0,0 +1,20 @@ +--- + +- name: Ensure k3s service is started + ansible.builtin.systemd: + name: k3s + state: started + enabled: "{{ k3s_start_on_boot }}" + when: k3s_non_root is not defined or not k3s_non_root + become: "{{ k3s_become }}" + +- name: Ensure k3s service is started + ansible.builtin.systemd: + name: k3s + state: started + enabled: "{{ k3s_start_on_boot }}" + scope: user + when: + - k3s_non_root is defined + - k3s_non_root + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/tasks/ensure_stopped.yml b/roles/xanmanning.k3s/tasks/ensure_stopped.yml new file mode 100644 index 00000000..7ba4b943 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_stopped.yml @@ -0,0 +1,20 @@ +--- + +- name: Ensure k3s service is stopped + ansible.builtin.systemd: + name: k3s + state: stopped + enabled: "{{ k3s_start_on_boot }}" + when: k3s_non_root is not defined or not k3s_non_root + become: "{{ k3s_become }}" + +- name: Ensure k3s service is started + ansible.builtin.systemd: + name: k3s + state: stopped + enabled: "{{ k3s_start_on_boot }}" + scope: user + when: + - k3s_non_root is defined + - k3s_non_root + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/tasks/ensure_uninstalled.yml b/roles/xanmanning.k3s/tasks/ensure_uninstalled.yml new file mode 100644 index 00000000..b944afda --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_uninstalled.yml @@ -0,0 +1,42 @@ +--- + +- name: Check to see if k3s-killall.sh exits + ansible.builtin.stat: + path: /usr/local/bin/k3s-killall.sh + register: check_k3s_killall_script + +- name: Check to see if k3s-uninstall.sh exits + ansible.builtin.stat: + path: /usr/local/bin/k3s-uninstall.sh + register: check_k3s_uninstall_script + +- name: Run k3s-killall.sh + ansible.builtin.command: + cmd: /usr/local/bin/k3s-killall.sh + register: k3s_killall + changed_when: k3s_killall.rc == 0 + when: check_k3s_killall_script.stat.exists + become: "{{ k3s_become }}" + +- name: Run k3s-uninstall.sh + ansible.builtin.command: + cmd: /usr/local/bin/k3s-uninstall.sh + args: + removes: /usr/local/bin/k3s-uninstall.sh + register: k3s_uninstall + changed_when: k3s_uninstall.rc == 0 + when: check_k3s_uninstall_script.stat.exists + become: "{{ k3s_become }}" + +- name: Ensure hard links are removed + ansible.builtin.file: + path: "{{ k3s_install_dir }}/{{ item }}" + state: absent + loop: + - kubectl + - crictl + - ctr + when: + - k3s_install_hard_links + - not ansible_check_mode + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/tasks/ensure_uploads.yml b/roles/xanmanning.k3s/tasks/ensure_uploads.yml new file mode 100644 index 00000000..a2f8a782 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/ensure_uploads.yml @@ -0,0 +1,15 @@ +--- + +- name: Ensure installation directory exists + ansible.builtin.file: + path: "{{ k3s_install_dir }}" + state: directory + mode: 0755 + +- name: Ensure k3s binary is copied from controller to target host + ansible.builtin.copy: + src: k3s + # TODO: allow airgap to bypass version post-fix + dest: "{{ k3s_install_dir }}/k3s-{{ k3s_release_version }}" + mode: 0755 + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/tasks/main.yml b/roles/xanmanning.k3s/tasks/main.yml new file mode 100644 index 00000000..d6a2bccb --- /dev/null +++ b/roles/xanmanning.k3s/tasks/main.yml @@ -0,0 +1,5 @@ +--- + +- import_tasks: pre_checks.yml + +- include_tasks: state_{{ (k3s_state | lower) | default('installed') }}.yml diff --git a/roles/xanmanning.k3s/tasks/post_checks_control_plane.yml b/roles/xanmanning.k3s/tasks/post_checks_control_plane.yml new file mode 100644 index 00000000..a1a242a2 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/post_checks_control_plane.yml @@ -0,0 +1,10 @@ +--- + +- name: Check that the control plane is available to accept connections + ansible.builtin.wait_for: + port: "{{ k3s_runtime_config['https-listen-port'] | default('6443') }}" + host: "{{ k3s_runtime_config['bind-address'] | default('127.0.0.1') }}" + delay: 5 + sleep: 5 + timeout: 300 + when: k3s_control_node diff --git a/roles/xanmanning.k3s/tasks/post_checks_nodes.yml b/roles/xanmanning.k3s/tasks/post_checks_nodes.yml new file mode 100644 index 00000000..5a874851 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/post_checks_nodes.yml @@ -0,0 +1,21 @@ +--- + +- name: Check that all nodes to be ready + ansible.builtin.command: + cmd: "{{ k3s_install_dir }}/kubectl get nodes" + changed_when: false + failed_when: >- + kubectl_get_nodes_result.stdout.find("was refused") != -1 or + kubectl_get_nodes_result.stdout.find("ServiceUnavailable") != -1 + register: kubectl_get_nodes_result + until: + - kubectl_get_nodes_result.rc == 0 + - kubectl_get_nodes_result.stdout.find("NotReady") == -1 + retries: 30 + delay: 20 + when: + - k3s_control_node + - ("flannel-backend" not in k3s_runtime_config + or k3s_runtime_config["flannel-backend"] != "none") + - not ansible_check_mode + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/tasks/post_checks_uninstalled.yml b/roles/xanmanning.k3s/tasks/post_checks_uninstalled.yml new file mode 100644 index 00000000..8b125fb9 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/post_checks_uninstalled.yml @@ -0,0 +1,41 @@ +--- + +- name: Check that k3s is not running + ansible.builtin.command: + cmd: pgrep k3s + failed_when: + - check_k3s_process.rc == 0 + - not ansible_check_mode + changed_when: false + register: check_k3s_process + +- name: Fail if k3s binaries have not been removed + ansible.builtin.stat: + path: "{{ k3s_install_dir }}/{{ item }}" + register: check_k3s_binaries_removed + failed_when: check_k3s_binaries_removed.stat.exists + loop: + - k3s + - kubectl + - crictl + - ctr + +- name: Check k3s-killall.sh is removed + ansible.builtin.stat: + path: /usr/local/bin/k3s-killall.sh + register: check_k3s_killall + +- name: Fail if k3s-killall.sh script still exists + ansible.builtin.fail: + msg: k3s-killall.sh still exists, uninstall script failed. Please investigate. + when: check_k3s_killall.stat.exists + +- name: Check k3s-uninstall.sh is removed + ansible.builtin.stat: + path: /usr/local/bin/k3s-uninstall.sh + register: check_k3s_uninstall + +- name: Fail if k3s-uninstall.sh script still exists + ansible.builtin.fail: + msg: k3s-uninstall.sh is still exists, uninstall script failed. Please investigate. + when: check_k3s_uninstall.stat.exists diff --git a/roles/xanmanning.k3s/tasks/pre_checks.yml b/roles/xanmanning.k3s/tasks/pre_checks.yml new file mode 100644 index 00000000..6ecb8fc1 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/pre_checks.yml @@ -0,0 +1,123 @@ +--- + +- name: Check that k3s_state is a supported value + ansible.builtin.assert: + that: + - k3s_state in k3s_valid_states + fail_msg: "k3s_state not valid. Check README.md for details." + success_msg: "k3s_state is valid." + when: k3s_state is defined + +- name: Check that Ansible v{{ ansible_version.string }} is supported by this role + ansible.builtin.assert: + that: + - ansible_version.string is version_compare(k3s_ansible_min_version, '>=') + fail_msg: >- + Ansible v{{ ansible_version.string }} is not supported by this role. + Please install >= v{{ k3s_ansible_min_version }}. + success_msg: "Ansible v{{ ansible_version.string }} is supported." + become: false + delegate_to: localhost + run_once: true + when: + - not k3s_skip_validation + - not k3s_skip_env_checks + +- name: Check that Python v{{ ansible_python_version }} is supported by this role + ansible.builtin.assert: + that: + - ansible_python_version is version_compare(k3s_python_min_version, '>=') + fail_msg: >- + Python v{{ ansible_python_version }} is not supported by this role. + Please install >= v{{ k3s_python_min_version }}. + success_msg: "Python v{{ ansible_python_version }} is supported." + become: false + delegate_to: localhost + run_once: true + when: + - not k3s_skip_validation + - not k3s_skip_env_checks + +- name: Check that the target init system is supported by this role + ansible.builtin.assert: + that: + - ansible_service_mgr in k3s_supported_init + fail_msg: >- + {{ ansible_service_mgr }} is not supported by this role. + Supported init systems: {{ k3s_supported_init | join(', ') }} + success_msg: "{{ ansible_service_mgr }} is supported" + when: + - not k3s_skip_validation + - not k3s_skip_env_checks + +- name: Determing if {{ ansible_service_mgr }} is actually openrc + ansible.builtin.stat: + path: /sbin/openrc-run + register: k3s_check_openrc_run + when: + - k3s_service_handler[ansible_service_mgr] == 'service' + - not k3s_skip_validation + - not k3s_skip_env_checks + +- name: Check that {{ ansible_service_mgr }} is actually openrc + ansible.builtin.assert: + that: + - k3s_check_openrc_run.stat.exists + fail_msg: >- + openrc was not found, cannot install to {{ ansible_service_mgr }} + success_msg: "openrc found" + when: + - k3s_service_handler[ansible_service_mgr] == 'service' + - not k3s_skip_validation + - not k3s_skip_env_checks + +- include_tasks: pre_checks_version.yml + when: + - (k3s_release_version is not defined + or not k3s_release_version + or k3s_release_version is not regex('\\+k3s[1-9]$')) + - not k3s_airgap + +- include_tasks: pre_checks_cgroups.yml + loop: "{{ k3s_cgroup_subsys }}" + loop_control: + loop_var: cgroup + when: + - not k3s_skip_validation + - not k3s_skip_env_checks + +- include_tasks: pre_checks_packages.yml + loop: "{{ k3s_check_packages[k3s_os_distribution_version] }}" + loop_control: + loop_var: package + when: + - not k3s_skip_validation + - not k3s_skip_env_checks + - k3s_check_packages[k3s_os_distribution_version] is defined + +- include_tasks: pre_checks_issue_data.yml + when: + - pyratlabs_issue_controller_dump is defined + - pyratlabs_issue_controller_dump + +- import_tasks: pre_checks_variables.yml + when: + - not k3s_skip_validation + +- import_tasks: pre_checks_experimental_variables.yml + when: + - not k3s_skip_validation + +- import_tasks: pre_checks_unsupported_rootless.yml + when: + - k3s_runtime_config.rootless is defined + - k3s_runtime_config.rootless + - not k3s_skip_validation + +- import_tasks: ensure_pre_configuration.yml + +- import_tasks: pre_checks_control_node_count.yml + when: + - k3s_build_cluster is defined + - k3s_build_cluster + - not k3s_skip_validation diff --git a/roles/xanmanning.k3s/tasks/pre_checks_cgroups.yml b/roles/xanmanning.k3s/tasks/pre_checks_cgroups.yml new file mode 100644 index 00000000..a75a90cb --- /dev/null +++ b/roles/xanmanning.k3s/tasks/pre_checks_cgroups.yml @@ -0,0 +1,17 @@ +--- + +- name: Check if {{ cgroup.name }} cgroup is enabled + ansible.builtin.command: + cmd: 'grep -E "^{{ cgroup.name }}\s+.*\s+1$" /proc/cgroups' + failed_when: false + changed_when: false + check_mode: false + register: k3s_check_cgroup_option + +- name: Fail if {{ cgroup.name }} cgroup is not enabled + ansible.builtin.assert: + that: + - k3s_check_cgroup_option.rc == 0 + fail_msg: | + {{ cgroup.name }} cgroup disabled. {{ cgroup.documentation }} + success_msg: "{{ cgroup.name }} cgroup enabled." diff --git a/roles/xanmanning.k3s/tasks/pre_checks_cluster.yml b/roles/xanmanning.k3s/tasks/pre_checks_cluster.yml new file mode 100644 index 00000000..f9435d1f --- /dev/null +++ b/roles/xanmanning.k3s/tasks/pre_checks_cluster.yml @@ -0,0 +1,19 @@ +--- + +- name: Check that the initial control plane server is available to accept connections + ansible.builtin.wait_for: + port: "{{ k3s_runtime_config['https-listen-port'] | default('6443') }}" + host: "{{ k3s_runtime_config['bind-address'] | default('127.0.0.1') }}" + delay: 5 + sleep: 5 + timeout: 300 + +- name: Check that cluster-token exists + ansible.builtin.stat: + path: "{{ k3s_runtime_config['data-dir'] | default(k3s_data_dir) }}/server/token" + register: k3s_check_cluster_token + check_mode: false + failed_when: + - not k3s_check_cluster_token.stat.exists + - not ansible_check_mode + become: "{{ k3s_become }}" diff --git a/roles/xanmanning.k3s/tasks/pre_checks_control_node_count.yml b/roles/xanmanning.k3s/tasks/pre_checks_control_node_count.yml new file mode 100644 index 00000000..386e65e4 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/pre_checks_control_node_count.yml @@ -0,0 +1,46 @@ +--- + +- name: Check the conditions when a single controller is defined + ansible.builtin.assert: + that: + - (k3s_controller_list | length == 1) + - ("datastore-endpoint" not in k3s_runtime_config or not k3s_runtime_config['datastore-endpoint']) + - (k3s_etcd_datastore is not defined or not k3s_etcd_datastore) + success_msg: "Control plane configuration is valid." + fail_msg: >- + Control plane configuration is invalid. + Please see notes about k3s_control_node and HA in README.md. + when: + - k3s_controller_list | length == 1 + - not k3s_use_unsupported_config + - k3s_control_node + +- name: Check the conditions when multiple controllers are defined + ansible.builtin.assert: + that: + - (k3s_controller_list | length >= 2) + - (("datastore-endpoint" in k3s_runtime_config and k3s_runtime_config['datastore-endpoint']) + or (k3s_etcd_datastore is defined and k3s_etcd_datastore)) + success_msg: "Control plane configuration is valid." + fail_msg: >- + Control plane configuration is invalid. Please see notes about + k3s_control_node and HA in README.md. + when: + - k3s_controller_list | length >= 2 + - k3s_control_node + +- name: Check the conditions when embedded etcd is defined + ansible.builtin.assert: + that: + - (k3s_controller_list | length >= 3) + - (((k3s_controller_list | length) % 2) == 1) + success_msg: "Control plane configuration is valid." + fail_msg: >- + Etcd should have a minimum of 3 defined members and the number of + members should be odd. Please see notes about HA in README.md + when: + - k3s_etcd_datastore is defined + - k3s_etcd_datastore + - not k3s_use_unsupported_config + - k3s_control_node + - k3s_state != 'uninstalled' diff --git a/roles/xanmanning.k3s/tasks/pre_checks_experimental_variables.yml b/roles/xanmanning.k3s/tasks/pre_checks_experimental_variables.yml new file mode 100644 index 00000000..15f731c8 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/pre_checks_experimental_variables.yml @@ -0,0 +1,31 @@ +--- + +- name: Check if embedded etcd datastore is enabled and marked as experimental + ansible.builtin.assert: + that: + - k3s_use_experimental is defined and k3s_use_experimental + success_msg: "Experimental variables are defined and enabled." + fail_msg: >- + Experimental variable k3s_etcd_datastore has been configured. + If you want to use this ensure you set k3s_use_experimental, + when: + - k3s_etcd_datastore is defined + - k3s_etcd_datastore + - (k3s_release_version | replace('v', '')) is version_compare("1.19.5", '<') + +- name: Check if any experimental variables are configure and if they are enabled with k3s_use_experimental + ansible.builtin.assert: + that: + - k3s_use_experimental is defined and k3s_use_experimental + success_msg: "Experimental variables are defined and enabled." + fail_msg: >- + Experimental variable {{ item.setting }} has been configured. + If you want to use this ensure you set k3s_use_experimental. + {% if item.document is defined %} + Documentation: {{ item.documentation }} + {% endif %} + loop: "{{ k3s_experimental_config }}" + when: + - (item.setting in k3s_runtime_config and k3s_runtime_config[item.setting]) + - ((item.until is not defined) or + (item.until is defined and (k3s_release_version | replace('v', '')) is version_compare(item.until, '<'))) diff --git a/roles/xanmanning.k3s/tasks/pre_checks_issue_data.yml b/roles/xanmanning.k3s/tasks/pre_checks_issue_data.yml new file mode 100644 index 00000000..60421ba4 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/pre_checks_issue_data.yml @@ -0,0 +1,87 @@ +--- + +- name: Ensure facts are gathered + ansible.builtin.setup: + +- name: Ensure Ansible version is captured + ansible.builtin.command: + cmd: ansible --version + failed_when: false + changed_when: false + register: check_ansible_version + delegate_to: localhost + run_once: true + become: false + +- name: Ensure Ansible config is captured + ansible.builtin.command: + cmd: ansible-config dump --only-changed + failed_when: false + changed_when: false + register: check_ansible_config + delegate_to: localhost + run_once: true + become: false + +- name: Ensure a list of roles is captured + ansible.builtin.command: + cmd: ansible-galaxy role list + failed_when: false + changed_when: false + register: check_ansible_roles + delegate_to: localhost + run_once: true + become: false + +- name: Ensure facts are written to disk + ansible.builtin.copy: + dest: "{{ playbook_dir }}/pyratlabs-issue-dump.txt" + content: | + # Begin ANSIBLE VERSION + {{ check_ansible_version.stdout }} + # End ANSIBLE VERSION + + # Begin ANSIBLE CONFIG + {{ check_ansible_config.stdout }} + # End ANSIBLE CONFIG + + # Begin ANSIBLE ROLES + {{ check_ansible_roles.stdout }} + # End ANSIBLE ROLES + + # Begin PLAY HOSTS + {{ ansible_play_hosts | to_json }} + # End PLAY HOSTS + + # Begin K3S ROLE CONFIG + {% for host in ansible_play_hosts %} + ## {{ host }} + {% for config_key in hostvars[host] %} + {% if config_key | regex_search('^k3s_') %} + {{ config_key }}: {{ hostvars[host][config_key] | to_json }} + {% endif %} + {% endfor %} + + {% endfor %} + # End K3S ROLE CONFIG + + # Begin K3S RUNTIME CONFIG + {% for host in ansible_play_hosts %} + ## {{ host }} + {% if hostvars[host].k3s_runtime_config is defined %} + {{ hostvars[host].k3s_runtime_config }} + {% endif %} + {% endfor %} + # End K3S RUNTIME CONFIG + mode: 0600 + delegate_to: localhost + run_once: true + become: false + +- name: Fail the play + ansible.builtin.fail: + msg: >- + Please include the output of + {{ playbook_dir }}/pyratlabs-issue-dump.txt in your bug report. + delegate_to: localhost + run_once: true diff --git a/roles/xanmanning.k3s/tasks/pre_checks_packages.yml b/roles/xanmanning.k3s/tasks/pre_checks_packages.yml new file mode 100644 index 00000000..006a510f --- /dev/null +++ b/roles/xanmanning.k3s/tasks/pre_checks_packages.yml @@ -0,0 +1,26 @@ +--- + +- name: Check that {{ package.name }} is installed + ansible.builtin.command: + cmd: "which {{ package.name }}" + changed_when: false + failed_when: false + register: check_k3s_required_package + become: "{{ k3s_become }}" + +- name: Test that checks for {{ package.name }} passed + ansible.builtin.assert: + that: + - check_k3s_required_package.rc == 0 + success_msg: "Found required package: {{ package.name }}." + fail_msg: >- + Could not find package: {{ package.name }}. + {% if package.documentation is defined %} + Documentation: {{ package.documentation }} + {% endif %} + when: + - check_k3s_required_package.rc is defined + - (package.until is not defined + or (k3s_release_version | replace('v', '')) is version_compare(package.until, '<')) + - (package.from is not defined + or (k3s_release_version | replace('v', '')) is version_compare(package.from, '>=')) diff --git a/roles/xanmanning.k3s/tasks/pre_checks_unsupported_rootless.yml b/roles/xanmanning.k3s/tasks/pre_checks_unsupported_rootless.yml new file mode 100644 index 00000000..625ee252 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/pre_checks_unsupported_rootless.yml @@ -0,0 +1,63 @@ +--- + +- name: Check if newuidmap is available + ansible.builtin.command: + cmd: which newuidmap + failed_when: false + changed_when: false + register: k3s_check_newuidmap_installed + +- name: Check if /proc/sys/kernel/unprivileged_userns_clone exists + ansible.builtin.stat: + path: /proc/sys/kernel/unprivileged_userns_clone + register: k3s_check_unprivileged_userns_exists + +- name: Get the value of /proc/sys/kernel/unprivileged_userns_clone + ansible.builtin.slurp: + src: /proc/sys/kernel/unprivileged_userns_clone + register: k3s_get_unprivileged_userns_clone + when: k3s_check_unprivileged_userns_exists.stat.exists + +- name: Set the value of k3s_get_unprivileged_userns_clone + ansible.builtin.set_fact: + k3s_get_unprivileged_userns_clone: + content: "MQo=" + when: not k3s_check_unprivileged_userns_exists.stat.exists + +- name: Get the value of /proc/sys/user/max_user_namespaces + ansible.builtin.slurp: + src: /proc/sys/user/max_user_namespaces + register: k3s_get_max_user_namespaces + +- name: Get the contents of /etc/subuid + ansible.builtin.slurp: + src: /etc/subuid + register: k3s_get_subuid + +- name: Get the contents of /etc/subgid + ansible.builtin.slurp: + src: /etc/subgid + register: k3s_get_subgid + +- name: Get current user subuid and subgid values + ansible.builtin.set_fact: + k3s_current_user_subuid: "{{ (k3s_get_subuid['content'] | b64decode).split('\n') + | select('search', ansible_user_id) | first | default('UserNotFound:0:0') }}" + k3s_current_user_subgid: "{{ (k3s_get_subgid['content'] | b64decode).split('\n') + | select('search', ansible_user_id) | first | default('UserNotFound:0:0') }}" + +- name: Check user namespaces kernel parameters are adequate + ansible.builtin.assert: + that: + - k3s_get_unprivileged_userns_clone['content'] | b64decode | int == 1 + - ((k3s_get_max_user_namespaces['content'] | b64decode | int >= 28633) or (k3s_os_family != "redhat")) + - k3s_current_user_subuid != "UserNotFound:0:0" + - k3s_current_user_subgid != "UserNotFound:0:0" + - k3s_current_user_subuid.split(':')[2] | int >= 65536 + - k3s_current_user_subgid.split(':')[2] | int >= 65536 + - ansible_env['XDG_RUNTIME_DIR'] is defined + - k3s_check_newuidmap_installed.rc == 0 + success_msg: All kernel parameters passed + fail_msg: >- + Kernel parameters are not set correctly, please check + https://github.com/rootless-containers/rootlesskit diff --git a/roles/xanmanning.k3s/tasks/pre_checks_variables.yml b/roles/xanmanning.k3s/tasks/pre_checks_variables.yml new file mode 100644 index 00000000..29743ef0 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/pre_checks_variables.yml @@ -0,0 +1,57 @@ +--- + +- name: "Check that k3s_release_version >= {{ k3s_min_version }}" + ansible.builtin.assert: + that: + - (k3s_release_version | replace('v', '')) is version_compare(k3s_min_version, '>=') + success_msg: "{{ k3s_release_version }} is supported by this role." + fail_msg: "{{ k3s_release_version }} is not supported by this role, please use xanmanning.k3s v1.x." + when: not k3s_airgap + +- name: Check configuration in k3s_server and k3s_agent that needs alternate configuration + ansible.builtin.assert: + that: + - (item.setting not in k3s_runtime_config) + success_msg: "{{ item.setting }} not found in server/agent config" + fail_msg: >- + {{ item.setting }} found in server/agent config. + Please set {{ item.correction }} to use this option. + {% if item.document is defined %} + Documentation: {{ item.documentation }} + {% endif %} + loop: "{{ k3s_config_exclude }}" + +- name: Check configuration in k3s_server and k3s_agent for deprecated configuration + ansible.builtin.assert: + that: + - (item.setting not in k3s_runtime_config) + or (not k3s_runtime_config[item.setting]) + success_msg: "{{ item.setting }} not found in server/agent config" + fail_msg: >- + {{ item.setting }} found in server/agent config. + Please set {{ item.correction }} to use this option. + {% if item.document is defined %} + Documentation: {{ item.documentation }} + {% endif %} + loop: "{{ k3s_deprecated_config }}" + when: + - not k3s_airgap + - (item.when is not defined + or (item.when is defined and (k3s_release_version | replace('v', '')) is version_compare(item.when, '>='))) + - not k3s_use_unsupported_config + +- name: Check configuration in k3s_server and k3s_agent against release version + ansible.builtin.assert: + that: + - (k3s_release_version | replace('v', '')) is version_compare(item.version, '>=') + success_msg: "{{ item.setting }} is supported by {{ k3s_release_version }}" + fail_msg: >- + {{ item.setting }} is not supported in {{ k3s_release_version }}. + Please update to v{{ item.version }} to use this option. + {% if item.document is defined %} + Documentation: {{ item.documentation }} + {% endif %} + loop: "{{ k3s_config_version_check }}" + when: + - k3s_config_version_check is defined + - item.setting in k3s_runtime_config diff --git a/roles/xanmanning.k3s/tasks/pre_checks_version.yml b/roles/xanmanning.k3s/tasks/pre_checks_version.yml new file mode 100644 index 00000000..9969c54b --- /dev/null +++ b/roles/xanmanning.k3s/tasks/pre_checks_version.yml @@ -0,0 +1,32 @@ +--- + +- name: Ensure k3s_release_version is set to default if false + ansible.builtin.set_fact: + k3s_release_version: "{{ k3s_release_channel }}" + check_mode: false + when: + - k3s_release_version is defined + - not k3s_release_version + +- name: Ensure the default release channel is set + ansible.builtin.set_fact: + k3s_release_channel: "{{ k3s_release_version | default('stable') }}" + check_mode: false + +- name: Get the latest release version from k3s.io + ansible.builtin.uri: + url: "{{ k3s_api_releases }}" + return_content: true + body_format: json + register: k3s_latest_release + no_log: true + check_mode: false + +- name: Ensure the release version is set as a fact + ansible.builtin.set_fact: + k3s_release_version: "{{ item.latest }}" + loop: "{{ k3s_latest_release.json.data }}" + check_mode: false + when: + - item.name == k3s_release_channel + - item.type == "channel" diff --git a/roles/xanmanning.k3s/tasks/state_downloaded.yml b/roles/xanmanning.k3s/tasks/state_downloaded.yml new file mode 100644 index 00000000..0fa4aa6e --- /dev/null +++ b/roles/xanmanning.k3s/tasks/state_downloaded.yml @@ -0,0 +1,12 @@ +--- + +- import_tasks: pre_checks_version.yml + when: + - k3s_release_version is not defined or not k3s_release_version + - not k3s_airgap + +- import_tasks: ensure_downloads.yml + when: not k3s_airgap + +- import_tasks: ensure_uploads.yml + when: k3s_airgap diff --git a/roles/xanmanning.k3s/tasks/state_installed.yml b/roles/xanmanning.k3s/tasks/state_installed.yml new file mode 100644 index 00000000..72164a1c --- /dev/null +++ b/roles/xanmanning.k3s/tasks/state_installed.yml @@ -0,0 +1,38 @@ +--- + +- import_tasks: ensure_drain_and_remove_nodes.yml + +- import_tasks: determine_systemd_context.yml + +- name: Flush Handlers + ansible.builtin.meta: flush_handlers + +- import_tasks: ensure_downloads.yml + when: not k3s_airgap + +- import_tasks: ensure_uploads.yml + when: k3s_airgap + +- import_tasks: ensure_k3s_auto_deploy.yml + when: + - k3s_primary_control_node + +- import_tasks: ensure_k3s_config_files.yml + +- import_tasks: ensure_installed.yml + +- include_tasks: ensure_containerd_registries.yml + when: + - k3s_registries is defined + - ('rootless' not in k3s_runtime_config or not k3s_runtime_config.rootless) + +- include_tasks: pre_checks_cluster.yml + when: + - k3s_control_delegate is defined + - k3s_control_delegate == inventory_hostname + +- import_tasks: ensure_cluster.yml + when: + - k3s_build_cluster is defined + - k3s_build_cluster + - k3s_registration_address is defined diff --git a/roles/xanmanning.k3s/tasks/state_restarted.yml b/roles/xanmanning.k3s/tasks/state_restarted.yml new file mode 100644 index 00000000..aad31866 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/state_restarted.yml @@ -0,0 +1,5 @@ +--- + +- import_tasks: ensure_stopped.yml + +- import_tasks: ensure_started.yml diff --git a/roles/xanmanning.k3s/tasks/state_started.yml b/roles/xanmanning.k3s/tasks/state_started.yml new file mode 100644 index 00000000..c9784196 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/state_started.yml @@ -0,0 +1,3 @@ +--- + +- import_tasks: ensure_started.yml diff --git a/roles/xanmanning.k3s/tasks/state_stopped.yml b/roles/xanmanning.k3s/tasks/state_stopped.yml new file mode 100644 index 00000000..d8ddc3a0 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/state_stopped.yml @@ -0,0 +1,3 @@ +--- + +- import_tasks: ensure_stopped.yml diff --git a/roles/xanmanning.k3s/tasks/state_uninstalled.yml b/roles/xanmanning.k3s/tasks/state_uninstalled.yml new file mode 100644 index 00000000..7d15d891 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/state_uninstalled.yml @@ -0,0 +1,12 @@ +--- + +- import_tasks: ensure_pre_configuration.yml + +- import_tasks: ensure_drain_and_remove_nodes.yml + +- import_tasks: ensure_uninstalled.yml + +- import_tasks: post_checks_uninstalled.yml + when: + - not k3s_skip_validation + - not k3s_skip_post_checks diff --git a/roles/xanmanning.k3s/tasks/state_validated.yml b/roles/xanmanning.k3s/tasks/state_validated.yml new file mode 100644 index 00000000..b831ee18 --- /dev/null +++ b/roles/xanmanning.k3s/tasks/state_validated.yml @@ -0,0 +1,5 @@ +--- + +- import_tasks: post_checks_control_plane.yml + +- import_tasks: post_checks_nodes.yml diff --git a/roles/xanmanning.k3s/templates/cluster-token.j2 b/roles/xanmanning.k3s/templates/cluster-token.j2 new file mode 100644 index 00000000..6ad94898 --- /dev/null +++ b/roles/xanmanning.k3s/templates/cluster-token.j2 @@ -0,0 +1 @@ +{{ k3s_control_token_content | default(k3s_control_token | default(k3s_control_delegate | to_uuid)) }} diff --git a/roles/xanmanning.k3s/templates/config.yaml.j2 b/roles/xanmanning.k3s/templates/config.yaml.j2 new file mode 100644 index 00000000..3a174992 --- /dev/null +++ b/roles/xanmanning.k3s/templates/config.yaml.j2 @@ -0,0 +1,8 @@ +--- + +{% if k3s_etcd_datastore and (k3s_control_node is defined and k3s_control_node) and (k3s_primary_control_node is defined and k3s_primary_control_node) %} +cluster-init: true +{% endif %} +{% if k3s_runtime_config is defined and k3s_runtime_config | length > 0 %} +{{ k3s_runtime_config | to_nice_yaml(indent=2) }} +{% endif %} diff --git a/roles/xanmanning.k3s/templates/k3s-killall.sh.j2 b/roles/xanmanning.k3s/templates/k3s-killall.sh.j2 new file mode 100644 index 00000000..fa2769dc --- /dev/null +++ b/roles/xanmanning.k3s/templates/k3s-killall.sh.j2 @@ -0,0 +1,70 @@ +#!/bin/sh + +[ $(id -u) -eq 0 ] || exec sudo $0 $@ + +for bin in {{ k3s_runtime_config['data-dir'] | default(k3s_data_dir) }}/data/**/bin/; do + [ -d $bin ] && export PATH=$PATH:$bin:$bin/aux +done + +set -x + +for service in {{ k3s_systemd_unit_dir }}/k3s*.service; do + [ -s $service ] && systemctl stop $(basename $service) +done + +for service in /etc/init.d/k3s*; do + [ -x $service ] && $service stop +done + +pschildren() { + ps -e -o ppid= -o pid= | \ + sed -e 's/^\s*//g; s/\s\s*/\t/g;' | \ + grep -w "^$1" | \ + cut -f2 +} + +pstree() { + for pid in $@; do + echo $pid + for child in $(pschildren $pid); do + pstree $child + done + done +} + +killtree() { + kill -9 $( + { set +x; } 2>/dev/null; + pstree $@; + set -x; + ) 2>/dev/null +} + +getshims() { + ps -e -o pid= -o args= | sed -e 's/^ *//; s/\s\s*/\t/;' | grep -w 'k3s/data/[^/]*/bin/containerd-shim' | cut -f1 +} + +killtree $({ set +x; } 2>/dev/null; getshims; set -x) + +do_unmount_and_remove() { + awk -v path="$1" '$2 ~ ("^" path) { print $2 }' /proc/self/mounts | sort -r | xargs -r -t -n 1 sh -c 'umount "$0" && rm -rf "$0"' +} + +do_unmount_and_remove '/run/k3s' +do_unmount_and_remove '{{ k3s_runtime_config['data-dir'] | default(k3s_data_dir) }}' +do_unmount_and_remove '/var/lib/kubelet/pods' +do_unmount_and_remove '/var/lib/kubelet/plugins' +do_unmount_and_remove '/run/netns/cni-' + +# Remove CNI namespaces +ip netns show 2>/dev/null | grep cni- | xargs -r -t -n 1 ip netns delete + +# Delete network interface(s) that match 'master cni0' +ip link show 2>/dev/null | grep 'master cni0' | while read ignore iface ignore; do + iface=${iface%%@*} + [ -z "$iface" ] || ip link delete $iface +done +ip link delete cni0 +ip link delete flannel.1 +rm -rf /var/lib/cni/ +iptables-save | grep -v KUBE- | grep -v CNI- | iptables-restore diff --git a/roles/xanmanning.k3s/templates/k3s-uninstall.sh.j2 b/roles/xanmanning.k3s/templates/k3s-uninstall.sh.j2 new file mode 100644 index 00000000..9aa5678b --- /dev/null +++ b/roles/xanmanning.k3s/templates/k3s-uninstall.sh.j2 @@ -0,0 +1,50 @@ +#!/bin/sh +set -x +[ $(id -u) -eq 0 ] || exec sudo $0 $@ + +/usr/local/bin/k3s-killall.sh + +if which systemctl; then + systemctl disable k3s + systemctl reset-failed k3s + systemctl daemon-reload +fi +if which rc-update; then + rc-update delete k3s default +fi + +rm -f {{ k3s_systemd_unit_dir }}/k3s.service + +remove_uninstall() { + rm -f /usr/local/bin/k3s-uninstall.sh +} +trap remove_uninstall EXIT + +if (ls {{ k3s_systemd_unit_dir }}/k3s*.service || ls /etc/init.d/k3s*) >/dev/null 2>&1; then + set +x; echo 'Additional k3s services installed, skipping uninstall of k3s'; set -x + exit +fi + +for cmd in kubectl crictl ctr; do + if [ -L {{ k3s_install_dir }}/$cmd ]; then + rm -f {{ k3s_install_dir }}/$cmd + fi +done + +for bin in {{ k3s_install_dir }}/k3s*; do + if [ -f "${bin}" ]; then + rm -f "${bin}" + fi +done + +rm -rf {{ k3s_config_dir }} +rm -rf /run/k3s +rm -rf /run/flannel +rm -rf {{ k3s_runtime_config['data-dir'] | default(k3s_data_dir) }} +rm -rf /var/lib/kubelet +rm -f /usr/local/bin/k3s-killall.sh + +if type yum >/dev/null 2>&1; then + yum remove -y k3s-selinux + rm -f /etc/yum.repos.d/rancher-k3s-common*.repo +fi diff --git a/roles/xanmanning.k3s/templates/k3s.logrotate.j2 b/roles/xanmanning.k3s/templates/k3s.logrotate.j2 new file mode 100644 index 00000000..36687045 --- /dev/null +++ b/roles/xanmanning.k3s/templates/k3s.logrotate.j2 @@ -0,0 +1,5 @@ +/var/log/k3s.log { + missingok + notifempty + copytruncate +} diff --git a/roles/xanmanning.k3s/templates/k3s.openrc.j2 b/roles/xanmanning.k3s/templates/k3s.openrc.j2 new file mode 100644 index 00000000..98aa22a1 --- /dev/null +++ b/roles/xanmanning.k3s/templates/k3s.openrc.j2 @@ -0,0 +1,47 @@ +#!/sbin/openrc-run + +depend() { + after network-online + want cgroups +} + +start_pre() { + rm -f /tmp/k3s.* +} + +supervisor=supervise-daemon +name="k3s" +command="{{ k3s_install_dir }}/k3s" +command_args="{% filter regex_replace('\s+', ' ') %} +{% filter replace('\n', ' ') %} +{% if k3s_debug is defined and k3s_debug %} + --debug +{% endif %} +{% if k3s_control_node %} + server + {% if (k3s_etcd_datastore is defined and k3s_etcd_datastore) and (k3s_primary_control_node is not defined or not k3s_primary_control_node) and k3s_controller_list | length > 1 %} + --server https://{{ k3s_registration_address }}:{{ k3s_control_plane_port | default(6443) | string }} + {% endif %} + {% if k3s_server is defined %} + --config {{ k3s_config_file }} + {% endif %} + {% if not k3s_primary_control_node or k3s_control_token is defined %} + --token-file {{ k3s_token_location }} + {% endif %} +{% else %} + agent + --server https://{{ k3s_registration_address }}:{{ k3s_control_plane_port | default(6443) | string }} + --token-file {{ k3s_token_location }} + {% if k3s_agent is defined %} + --config {{ k3s_config_file }} + {% endif %} +{% endif %} >>/var/log/k3s.log 2>&1" +{% endfilter %} +{% endfilter %} + +output_log="/var/log/k3s.log" +error_log="/var/log/k3s.log" + +pidfile="/var/run/k3s.pid" +respawn_delay=5 +respawn_max=0 diff --git a/roles/xanmanning.k3s/templates/k3s.service.j2 b/roles/xanmanning.k3s/templates/k3s.service.j2 new file mode 100644 index 00000000..fdab38fb --- /dev/null +++ b/roles/xanmanning.k3s/templates/k3s.service.j2 @@ -0,0 +1,70 @@ +[Unit] +Description=Lightweight Kubernetes +Documentation=https://k3s.io +{% for requires_unit in k3s_service_requires %} +Requires={{ requires_unit }} +{% endfor %} +Wants=network-online.target +{% for wants_unit in k3s_service_wants %} +Wants={{ wants_unit }} +{% endfor %} +{% for before_unit in k3s_service_before %} +Before={{ before_unit }} +{% endfor %} +After=network-online.target +{% for after_unit in k3s_service_after %} +After={{ after_unit }} +{% endfor %} + +[Service] +Type={{ 'notify' if k3s_control_node else 'exec' }} +{% if k3s_service_env_vars is defined and k3s_service_env_vars is iterable %} +{% for env_var in k3s_service_env_vars %} +Environment={{ env_var }}="{{ k3s_service_env_vars[env_var] }}" +{% endfor %} +{% endif %} +{% if k3s_service_env_file is defined and k3s_service_env_file %} +EnvironmentFile={{ k3s_service_env_file }} +{% endif %} +ExecStartPre=-/sbin/modprobe br_netfilter +ExecStartPre=-/sbin/modprobe overlay +{% filter regex_replace('\s+', ' ') %} +{% filter replace('\n', ' ') %} +ExecStart={{ k3s_install_dir }}/k3s +{% if k3s_debug is defined and k3s_debug %} + --debug +{% endif %} +{% if k3s_control_node %} + server + {% if (k3s_etcd_datastore is defined and k3s_etcd_datastore) and (k3s_primary_control_node is not defined or not k3s_primary_control_node) and k3s_controller_list | length > 1 %} + --server https://{{ k3s_registration_address }}:{{ k3s_control_plane_port | default(6443) | string }} + {% endif %} + {% if k3s_server is defined %} + --config {{ k3s_config_file }} + {% endif %} + {% if not k3s_primary_control_node or k3s_control_token is defined %} + --token-file {{ k3s_token_location }} + {% endif %} +{% else %} + agent + --server https://{{ k3s_registration_address }}:{{ k3s_control_plane_port | default(6443) | string }} + --token-file {{ k3s_token_location }} + {% if k3s_agent is defined %} + --config {{ k3s_config_file }} + {% endif %} +{% endif %} +{% endfilter %} +{% endfilter %} + +KillMode=process +Delegate=yes +LimitNOFILE=1048576 +LimitNPROC=infinity +LimitCORE=infinity +TasksMax=infinity +TimeoutStartSec=0 +Restart=always +RestartSec=5s + +[Install] +WantedBy=multi-user.target diff --git a/roles/xanmanning.k3s/templates/registries.yaml.j2 b/roles/xanmanning.k3s/templates/registries.yaml.j2 new file mode 100644 index 00000000..4adbf13a --- /dev/null +++ b/roles/xanmanning.k3s/templates/registries.yaml.j2 @@ -0,0 +1,2 @@ +--- +{{ k3s_registries | to_nice_yaml }} \ No newline at end of file diff --git a/roles/xanmanning.k3s/vars/main.yml b/roles/xanmanning.k3s/vars/main.yml new file mode 100644 index 00000000..bc08dfce --- /dev/null +++ b/roles/xanmanning.k3s/vars/main.yml @@ -0,0 +1,206 @@ +--- + +# Minimum supported versions +k3s_min_version: 1.19.3 +k3s_ansible_min_version: 2.9.16 +k3s_python_min_version: 3.6 + +# Valid states for this role +k3s_valid_states: + - installed + - started + - stopped + - restarted + - downloaded + - uninstalled + - validated + +# Supported init systems +k3s_supported_init: + - systemd + - openrc + +# Map ansible fact gathering architecture to a release name and suffix in github. +k3s_arch_lookup: + amd64: + arch: amd64 + suffix: "" + x86_64: + arch: amd64 + suffix: "" + arm64: + arch: arm64 + suffix: "-arm64" + aarch64: + arch: arm64 + suffix: "-arm64" + arm: + arch: arm + suffix: "-armhf" + arm6l: + arch: arm + suffix: "-armhf" + armv6l: + arch: arm + suffix: "-armhf" + arm7: + arch: arm + suffix: "-armhf" + armv7l: + arch: arm + suffix: "-armhf" + armhf: + arch: arm + suffix: "-armhf" + +# Always default to stable channel, this will change with k3s_release_version +k3s_release_channel: stable + +# K3s updates API +k3s_api_releases: "{{ k3s_api_url }}/v1-release/channels" +# Download location for releases +k3s_github_download_url: "{{ k3s_github_url }}/releases/download" + +# Generate a runtime config dictionary +k3s_runtime_config: "{{ + (k3s_control_node is defined and k3s_control_node) + | ternary((k3s_server | default({})) | combine(k3s_agent | default({})), + (k3s_agent | default({})), + (k3s_server | default({})) | combine(k3s_agent | default({}))) +}}" + +# Determine if a cluster should be built +k3s_conf_build_cluster: "{{ + not ((ansible_play_hosts_all | length < 2) + and k3s_registration_address is not defined) +}}" + +# Empty array for counting the number of control plane nodes +k3s_controller_list: [] + +# Control plane port default +k3s_control_plane_port: "{{ k3s_runtime_config['https-listen-port'] | default(6443) }}" + +# Default to the "system" systemd context, this will be "user" when running rootless +k3s_systemd_context: system + +# Directory for systemd unit files to be installed. As this role doesn't use package +# management, this should live in /etc/systemd, not /lib/systemd +k3s_systemd_unit_dir: "/etc/systemd/{{ k3s_systemd_context }}" + +# Directory for installing openrc service file +k3s_openrc_service_dir: /etc/init.d + +# Directory for installing logrotate config +k3s_logrotate_dir: /etc/logrotate.d + +# Service handler +k3s_service_handler: + systemd: systemd + openrc: service + +# Data directory location for k3s +k3s_data_dir: "{{ k3s_runtime_config['data-dir'] | default('/var/lib/rancher/k3s') }}" + +# Config directory location for k3s +k3s_config_dir: "{{ k3s_config_file | dirname }}" + +# Directory for gathering the k3s token for clustering. I don't see this changing. +k3s_token_location: "{{ k3s_config_dir }}/cluster-token" + +# Path for additional Kubernetes Manifests +# https://rancher.com/docs/k3s/latest/en/advanced/#auto-deploying-manifests +k3s_server_manifests_dir: "{{ k3s_data_dir }}/server/manifests" + +# Path for static pod manifests that are deployed on the control plane +# https://github.com/k3s-io/k3s/pull/1691 +k3s_server_pod_manifests_dir: "{{ k3s_data_dir }}/agent/pod-manifests" + +# OS formatted strings +k3s_os_distribution: "{{ ansible_distribution | replace(' ', '-') | lower }}" +k3s_os_version: "{{ ansible_distribution_version | replace([' ', '.'], '-') | lower }}" +k3s_os_distribution_version: "{{ k3s_os_distribution }}-{{ k3s_os_version }}" +k3s_os_family: "{{ ansible_os_family | replace(' ', '-') | lower }}" + +# Packages that we need to check are installed +k3s_check_packages: + debian-11: + - name: iptables-legacy + from: 1.19.2 + until: 1.22.3 + documentation: https://rancher.com/docs/k3s/latest/en/advanced/#enabling-legacy-iptables-on-raspbian-buster +# - name: dummy +# from: 1.19.2 +# until: 1.21.0 +# documentation: https://example.com + +# Directories that we need to ensure exist +k3s_ensure_directories_exist: + - name: Config directory + path: "{{ k3s_config_dir }}" + - name: Config.yaml.d directory + path: "{{ k3s_config_yaml_d_dir }}" + - name: Systemd unit file directory + path: "{{ k3s_systemd_unit_dir }}" + - name: Data directory + path: "{{ k3s_data_dir }}" + - name: Default local storage path + path: "{{ k3s_runtime_config['default-local-storage-path'] | default(omit) }}" + - name: Private registry config file + path: "{{ (k3s_runtime_config['private-registry'] | default(omit)) | dirname }}" + +# Config items that should not appear in k3s_server or k3s_agent +k3s_config_exclude: + - setting: server + correction: k3s_registration_address + - setting: cluster-init + correction: k3s_etcd_datastore + - setting: token + correction: k3s_control_token + - setting: token-file + correction: k3s_token_location + +# Config items and the versions that they were introduced +k3s_config_version_check: + - setting: etcd-s3-bucket + version: 1.20.6 # Prior to this there was very buggy support! + +# Config items that should be marked as experimental +k3s_experimental_config: + - setting: selinux + until: 1.19.4 + - setting: rootless + - setting: secrets-encryption + - setting: agent-token + - setting: agent-token-file + - setting: cluster-reset + until: 1.19.5 + +# Config items that should be marked as deprecated +k3s_deprecated_config: + - setting: no-flannel + correction: "flannel-backend: 'none'" + # when: 0.10.2 # Example + - setting: cluster-secret + correction: token + - setting: no-deploy + correction: "disable: VALUE" + - setting: docker + correction: "docker: false" + when: 1.20.0 + +# cgroup checks +k3s_cgroup_subsys: + - name: memory + documentation: | + If you are running on a Raspberry Pi, see: + https://rancher.com/docs/k3s/latest/en/advanced/#enabling-cgroups-for-raspbian-buster + - name: cpuset + documentation: | + If you are running Alpine Linux, see: + https://rancher.com/docs/k3s/latest/en/advanced/#additional-preparation-for-alpine-linux-setup + +# Drain command +k3s_drain_command: + true: delete-emptydir-data + false: delete-local-data