diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..5bcef00 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,6 @@ +--- +warn_list: + - experimental + - var-naming + - no-changed-when + - inline-env-var diff --git a/.codespellignore b/.codespellignore new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/default-bare.yml b/.github/workflows/default-bare.yml new file mode 100644 index 0000000..286e914 --- /dev/null +++ b/.github/workflows/default-bare.yml @@ -0,0 +1,94 @@ +--- +name: default-bare + +on: + push: + pull_request: + workflow_dispatch: + +permissions: {} + +jobs: + build: + permissions: + contents: read + runs-on: ${{ matrix.distribution }}-${{ matrix.version }} + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + max-parallel: 4 + matrix: + include: + - distribution: ubuntu + version: '22.04' + experimental: false + - distribution: ubuntu + version: '20.04' + experimental: false + env: + ANSIBLE_CALLBACKS_ENABLED: profile_tasks + ANSIBLE_ROLE: cyverse_ansible.ansible_jupyterhub_docker + + steps: + - uses: actions/checkout@v3 + with: + path: ${{ env.ANSIBLE_ROLE }} + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip3 install ansible-lint flake8 yamllint + which ansible + pip3 install ansible + pip3 show ansible + ansible --version + cd $GITHUB_WORKSPACE/$ANSIBLE_ROLE + { echo '[defaults]'; echo 'callbacks_enabled = profile_tasks, timer'; echo 'inventory = hosts.ini'; echo 'roles_path = ../'; echo 'ansible_python_interpreter: /usr/bin/python3'; } >> ansible.cfg + - name: Galaxy dependencies + run: | + cd $GITHUB_WORKSPACE/$ANSIBLE_ROLE && ansible-galaxy install --timeout 120 --verbose -r molecule/default/requirements.yml -p ../ + continue-on-error: true + - name: run test + run: | + cd $GITHUB_WORKSPACE/$ANSIBLE_ROLE && ansible-playbook --connection=local --become -vvv molecule/default/converge.yml + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + - name: idempotency run + run: | + cd $GITHUB_WORKSPACE/$ANSIBLE_ROLE && ansible-playbook --connection=local --become -vvv molecule/default/converge.yml | tee /tmp/idempotency.log | grep -q 'changed=0.*failed=0' && (echo 'Idempotence test: pass' && exit 0) || (echo 'Idempotence test: fail' && cat /tmp/idempotency.log && exit 0) + - name: On failure + run: | + cd $GITHUB_WORKSPACE/$ANSIBLE_ROLE + ansible --connection=local -m setup localhost + if: env.WORKFLOW_CONCLUSION == 'failure' # notify only if failure + - name: After script - files + run: | + set -x + cat /etc/rancher/k3s/config.yaml + cat /opt/jupyterhub/config.yaml + if: ${{ always() }} + continue-on-error: true + - name: After script - network + run: | + set -x + sudo ss -tunap | grep LISTEN + if: ${{ always() }} + continue-on-error: true + - name: After script - process + run: | + set -x + ps aux + if: ${{ always() }} + continue-on-error: true + - name: After script - k8 + run: | + set -x + export KUBECONFIG=/etc/rancher/k3s/k3s.yaml + kubectl get pods --all-namespaces + helm ls --all-namespaces + if: ${{ always() }} + continue-on-error: true diff --git a/.github/workflows/galaxy.yml b/.github/workflows/galaxy.yml new file mode 100644 index 0000000..d5e8a58 --- /dev/null +++ b/.github/workflows/galaxy.yml @@ -0,0 +1,26 @@ +--- +name: Ansible Galaxy release + +on: + release: + types: [created, edited, published, released] + push: + tags: + - '*' + +permissions: {} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + with: + path: cyverse_ansible.ansible_jupyterhub_docker + - name: galaxy + uses: robertdebock/galaxy-action@1.2.1 + with: + galaxy_api_key: ${{ secrets.galaxy_api_key }} + path: cyverse_ansible.ansible_jupyterhub_docker + git_branch: master diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..8319eba --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,60 @@ +--- +name: lint + +on: + push: + pull_request: + workflow_dispatch: + +permissions: {} + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 4 + env: + ANSIBLE_CALLBACKS_ENABLED: profile_tasks + ANSIBLE_EXTRA_VARS: "" + ANSIBLE_ROLE: cyverse_ansible.ansible_jupyterhub_docker + + steps: + - uses: actions/checkout@v3 + with: + path: ${{ env.ANSIBLE_ROLE }} + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip3 install --pre ansible-lint flake8 yamllint + which ansible + pip3 install ansible + pip3 show ansible + ansible --version + cd $GITHUB_WORKSPACE/$ANSIBLE_ROLE + [ -f molecule/default/requirements.yml ] && ansible-galaxy install -r molecule/default/requirements.yml -p ../ + { echo '[defaults]'; echo 'callbacks_enabled = profile_tasks, timer'; echo 'roles_path = ../'; echo 'ansible_python_interpreter: /usr/bin/python3'; } >> ansible.cfg + - name: Environment + run: | + pwd + env + find . -ls + - uses: codespell-project/actions-codespell@master + with: + ignore_words_file: ${{ env.ANSIBLE_ROLE }}/.codespellignore + skip: .git + path: ${{ env.ANSIBLE_ROLE }} + if: ${{ always() }} + - name: yamllint + run: | + cd $GITHUB_WORKSPACE/$ANSIBLE_ROLE && yamllint . + if: ${{ always() }} + - name: ansible-lint + run: | + cd $GITHUB_WORKSPACE/$ANSIBLE_ROLE && ansible-lint + if: ${{ always() }} diff --git a/.travis.yml b/.travis.yml index 36bbf62..121cc49 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ sudo: false addons: apt: packages: - - python-pip + - python-pip install: # Install ansible @@ -26,4 +26,4 @@ script: - ansible-playbook tests/test.yml -i tests/inventory --syntax-check notifications: - webhooks: https://galaxy.ansible.com/api/v1/notifications/ \ No newline at end of file + webhooks: https://galaxy.ansible.com/api/v1/notifications/ diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..ff775c4 --- /dev/null +++ b/.yamllint @@ -0,0 +1,28 @@ +--- +# 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 + empty-lines: + max: 3 + level: error + hyphens: + level: error + key-duplicates: enable + line-length: disable + new-line-at-end-of-file: disable + new-lines: + type: unix + truthy: disable diff --git a/defaults/main.yml b/defaults/main.yml index cd7a470..9784b0a 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,3 +1,4 @@ +--- JH_NAMESPACE: default JH_AUTH_CLASS: none JH_CONFIG_DIR: "/opt/jupyterhub" diff --git a/handlers/main.yml b/handlers/main.yml index 804dd15..3252061 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -1,11 +1,11 @@ # because we use ansible-pull and include_role (dynamic includes), handlers do not work -#- name: restart apache2 -# systemd: -# name: apache2 -# state: restarted +# - name: restart apache2 +# systemd: +# name: apache2 +# state: restarted -#- name: restart jupyterhub -# systemd: -# name: jupyterhub -# state: restarted +# - name: restart jupyterhub +# systemd: +# name: jupyterhub +# state: restarted diff --git a/hosts.ini b/hosts.ini new file mode 100644 index 0000000..0d32f8f --- /dev/null +++ b/hosts.ini @@ -0,0 +1,11 @@ +[k3s_masters] + localhost + +[k3s_agents] + +[k3s_cluster:children] + k3s_masters + k3s_agents + +[jupyterhubgroup] + localhost diff --git a/meta/main.yml b/meta/main.yml index 6deef0a..11058e0 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,7 +1,10 @@ +--- galaxy_info: - author: Edwin Skidmore + author: cyverse_ansible + role_name: ansible_jupyterhub_docker description: This role will install jupyterhub with CyVerse auth integration. Jupyterhub is configured to use dockerspawner company: CyVerse + standalone: true # If the issue tracker for your role is not on github, uncomment the # next line and provide a value @@ -16,7 +19,7 @@ galaxy_info: # - CC-BY license: BSD - min_ansible_version: 2.3 + min_ansible_version: '2.3' # If this a Container Enabled role, provide the minimum Ansible Container version. # min_ansible_container_version: @@ -27,27 +30,27 @@ galaxy_info: # 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 master) will be used. - #github_branch: + # github_branch: # # platforms is a list of platforms, and each platform has a name and a list of versions. # platforms: - # - name: Fedora - # versions: - # - all - # - 25 - # - name: SomePlatform - # versions: - # - all - # - 1.0 - # - 7 - # - 99.99 - - name: Ubuntu - versions: - - xenial + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + - name: Ubuntu + versions: + - xenial - galaxy_tags: + galaxy_tags: - jupyter - jupyterhub - cyverse @@ -59,7 +62,7 @@ galaxy_info: # NOTE: A tag is limited to a single word comprised of alphanumeric characters. # Maximum 20 tags per role. -#dependencies: [docker] +# dependencies: [docker] dependencies: [] - # List your role dependencies here, one per line. Be sure to remove the '[]' above, - # if you add dependencies to this list. +# List your role dependencies here, one per line. Be sure to remove the '[]' above, +# if you add dependencies to this list. diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml new file mode 100644 index 0000000..ad0f3dc --- /dev/null +++ b/molecule/default/converge.yml @@ -0,0 +1,38 @@ +--- + +- name: Converge + hosts: all + environment: + http_proxy: "{{ lookup('env', 'http_proxy') }}" + https_proxy: "{{ lookup('env', 'https_proxy') }}" + no_proxy: "{{ lookup('env', 'no_proxy') }}" + remote_user: root + pre_tasks: + - name: Ubuntu | Install python3 + ansible.builtin.raw: test -e /usr/bin/python3 || (apt -y update && apt install -y python3-minimal) + register: python3 + changed_when: "'installed' in python3.stdout" + when: (ansible_distribution == "Ubuntu" and ansible_distribution_major_version | int >= 16) + - name: RedHat | Install python3 + ansible.builtin.raw: test -e /usr/bin/python3 || (yum install -y python3) + register: python3 + changed_when: "'installed' in python3.stdout" + when: (ansible_os_family == "RedHat" and ansible_distribution_major_version | int >= 8) + - name: Gather Facts + ansible.builtin.setup: + when: (ansible_distribution == "Ubuntu" and ansible_distribution_major_version | int >= 16) + - name: Ubuntu Bionic+, Redhat 8+ | Enforce python3 for ansible + ansible.builtin.set_fact: + ansible_python_interpreter: /usr/bin/python3 + when: > + (ansible_distribution == "Ubuntu" and ansible_distribution_major_version | int >= 16) or + (ansible_os_family == "RedHat" and ansible_distribution_major_version | int >= 8) + - name: Debian | Update cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + when: ansible_os_family == 'Debian' + roles: + - geerlingguy.docker + - cyverse_ansible.ansible_k3s + - cyverse_ansible.ansible_jupyterhub_docker diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml new file mode 100644 index 0000000..672e22b --- /dev/null +++ b/molecule/default/molecule.yml @@ -0,0 +1,24 @@ +--- +dependency: + name: galaxy + enabled: False +driver: + name: docker +platforms: + - name: instance + image: ${MOLECULE_DISTRO:-ubuntu:20.04} + # env: + # http_proxy: ${http_proxy} + # https_proxy: ${https_proxy} + # no_proxy: ${no_proxy} + groups: + - jupyterhubgroup +provisioner: + name: ansible + config_options: + defaults: + verbosity: 2 +scenario: + name: default +verifier: + name: ansible diff --git a/molecule/default/requirements.yml b/molecule/default/requirements.yml new file mode 100644 index 0000000..320668a --- /dev/null +++ b/molecule/default/requirements.yml @@ -0,0 +1,11 @@ +--- + +collections: + - ansible.posix + +roles: + - name: geerlingguy.docker + # - name: cyverse-ansible.ansible_k3s + - src: https://github.com/juju4/ansible-k3s/ + version: devel-ci2 + name: cyverse_ansible.ansible_k3s diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml new file mode 100644 index 0000000..c9d6e0a --- /dev/null +++ b/molecule/default/verify.yml @@ -0,0 +1,55 @@ +--- + +- name: Verify + hosts: jupyterhubgroup + vars: + config: /opt/jupyterhub/config.yaml + ports: + # web? + - { h: 0.0.0.0, p: 443 } + url: https://localhost + is_container: false + pre_tasks: + - name: Debug | var ansible_virtualization_type + ansible.builtin.debug: + var: ansible_virtualization_type + - name: Set fact is_container + ansible.builtin.set_fact: + is_container: true + when: > + (ansible_virtualization_type is defined and + (ansible_virtualization_type == "docker" or ansible_virtualization_type == "containerd") + ) + tasks: + - name: Gather package facts + ansible.builtin.package_facts: + manager: auto + - name: Validate that needed packages are present + ansible.builtin.assert: + that: ansible_facts.packages['python3'] + + - name: Check config file + ansible.builtin.stat: + path: "{{ config }}" + register: cfg1 + - name: Validate configuration file is present + ansible.builtin.assert: + that: cfg1.stat.exists and cfg1.stat.size != 0 + + - name: Check all processes + ansible.builtin.command: ps aux + changed_when: false + register: psa + - name: Debug | ps aux output + ansible.builtin.debug: + var: psa + verbosity: 1 + + - name: Ensure ports are listening + ansible.builtin.wait_for: + host: "{{ item.h }}" + port: "{{ item.p }}" + timeout: 10 + with_items: "{{ ports }}" + when: + - not is_container|bool diff --git a/tasks/main.yml b/tasks/main.yml index 0f6dc8f..96a1ce7 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,84 +1,94 @@ --- - name: FAIL; check oauth2 variables when an oauth2 provider is selected - fail: + ansible.builtin.fail: msg: "Auth class = {{ JH_AUTH_CLASS }} but one of JH_OAUTH2_CLIENT_ID, JH_OAUTH2_CLIENT_SECRET, or JH_OAUTH2_CALLBACK_URL is not defined " when: JH_AUTH_CLASS == "github" and (JH_OAUTH2_CLIENT_ID is not defined or JH_OAUTH2_CLIENT_ID == "" or JH_OAUTH2_CLIENT_SECRET is not defined or JH_OAUTH2_CLIENT_SECRET == "" or JH_OAUTH2_CALLBACK_URL is not defined or JH_OAUTH2_CALLBACK_URL is not defined == "") - name: FAIL; check dummy password - fail: + ansible.builtin.fail: msg: "JH_AUTH_CLASS is set to dummy but JH_DUMMY_PASS is not defined" when: JH_AUTH_CLASS == "dummy" and JH_DUMMY_PASS is not defined - name: FILE; mkdir {{ JH_CONFIG_DIR }} - file: + ansible.builtin.file: path: "{{ JH_CONFIG_DIR }}" state: directory mode: 0755 - name: TEMPLATE; install config.yaml to {{ JH_CONFIG_DIR }} - template: + ansible.builtin.template: src: config.yaml.j2 dest: "{{ JH_CONFIG_DIR }}/config.yaml" + mode: 0600 - name: SHELL; locate k3s binary - shell: command -v k3s + ansible.builtin.command: + cmd: which k3s register: k3s_found ignore_errors: true -- block: - - name: SET_FACT; set jh_k3s_kubeconfig if k3s is found on host - set_fact: - jh_k3s_kubeconfig: "KUBECONFIG=/etc/rancher/k3s/k3s.yaml" - - - name: FILE; create {{ansible_env.HOME}}/.kube directory if necessary - file: - path: "{{ansible_env.HOME}}/.kube" - state: directory - - - name: FILE; if k3s, link the k3s.yaml into {{ansible_env.HOME}}/.kube/config - file: - src: /etc/rancher/k3s/k3s.yaml - dest: "{{ansible_env.HOME}}/.kube/config" - state: link +- name: K3s present when: k3s_found is success + block: + - name: SET_FACT; set jh_k3s_kubeconfig if k3s is found on host + ansible.builtin.set_fact: + jh_k3s_kubeconfig: "KUBECONFIG=/etc/rancher/k3s/k3s.yaml" + + - name: FILE; create HOME .kube directory if necessary + ansible.builtin.file: + path: "{{ ansible_env.HOME }}/.kube" + state: directory + mode: 0600 + + - name: FILE; if k3s, link the k3s.yaml into HOME/.kube/config + ansible.builtin.file: + src: /etc/rancher/k3s/k3s.yaml + dest: "{{ ansible_env.HOME }}/.kube/config" + state: link # for now, we'll assign the master nodes as the one with all the jupyterhub core components - name: SHELL; kubectl label master nodes hub.jupyter.org/node-purpose=core - shell: "kubectl label nodes --overwrite --selector='node-role.kubernetes.io/master=true' hub.jupyter.org/node-purpose=core" + ansible.builtin.command: "kubectl label nodes --overwrite --selector='node-role.kubernetes.io/master=true' hub.jupyter.org/node-purpose=core" # and we'll assign worker nodes as one with all the jupyter notebooks - name: SHELL; kubectl label worker nodes hub.jupyter.org/node-purpose=user - shell: "kubectl label nodes --overwrite --selector='!node-role.kubernetes.io/master' hub.jupyter.org/node-purpose=user" + ansible.builtin.command: "kubectl label nodes --overwrite --selector='!node-role.kubernetes.io/master' hub.jupyter.org/node-purpose=user" -- block: - # - name: SHELL; kubectl label master nodes hub.jupyter.org/dedicated=user:NoSchedule - # shell: "kubectl taint nodes --overwrite --selector='node-role.kubernetes.io/master=true' hub.jupyter.org/dedicated=user:NoSchedule" +- name: Single User Exclude Master + when: JH_SINGLEUSER_EXCLUDE_MASTER|bool + block: + # - name: SHELL; kubectl label master nodes hub.jupyter.org/dedicated=user:NoSchedule + # shell: "kubectl taint nodes --overwrite --selector='node-role.kubernetes.io/master=true' hub.jupyter.org/dedicated=user:NoSchedule" - # - name: SHELL; kubectl label worker nodes hub.jupyter.org/dedicated=user:NoSchedule - # shell: "kubectl taint nodes --overwrite --selector='!node-role.kubernetes.io/master' hub.jupyter.org/dedicated=core:NoSchedule" - - name: SHELL; kubectl label master nodes hub.jupyter.org/dedicated=core:NoSchedule - shell: "kubectl taint nodes --overwrite --selector='node-role.kubernetes.io/master=true' hub.jupyter.org/dedicated=core:NoSchedule" + # - name: SHELL; kubectl label worker nodes hub.jupyter.org/dedicated=user:NoSchedule + # shell: "kubectl taint nodes --overwrite --selector='!node-role.kubernetes.io/master' hub.jupyter.org/dedicated=core:NoSchedule" + - name: SHELL; kubectl label master nodes hub.jupyter.org/dedicated=core:NoSchedule + ansible.builtin.command: "kubectl taint nodes --overwrite --selector='node-role.kubernetes.io/master=true' hub.jupyter.org/dedicated=core:NoSchedule" - - name: SHELL; kubectl label worker nodes hub.jupyter.org/dedicated=user:NoSchedule - shell: "kubectl taint nodes --overwrite --selector='!node-role.kubernetes.io/master' hub.jupyter.org/dedicated=user:NoSchedule" - when: JH_SINGLEUSER_EXCLUDE_MASTER|bool + - name: SHELL; kubectl label worker nodes hub.jupyter.org/dedicated=user:NoSchedule + ansible.builtin.command: "kubectl taint nodes --overwrite --selector='!node-role.kubernetes.io/master' hub.jupyter.org/dedicated=user:NoSchedule" - name: SHELL; locate helm binary - shell: command -v helm + ansible.builtin.command: + cmd: which helm register: helm_found ignore_errors: true - name: SHELL; install helm, if not found - shell: - cmd: curl https://raw.githubusercontent.com/helm/helm/HEAD/scripts/get-helm-3 | bash + ansible.builtin.shell: + cmd: | + set -o pipefail + curl https://raw.githubusercontent.com/helm/helm/HEAD/scripts/get-helm-3 | bash when: helm_found is failed - name: SHELL; Add jupyterhub chart repo - shell: + ansible.builtin.shell: cmd: helm repo add jupyterhub https://jupyterhub.github.io/helm-chart/ ; helm repo update - name: SHELL; install/Upgrade helm chart - shell: - cmd: "{{ jh_k3s_kubeconfig | default('') }} helm upgrade --cleanup-on-fail --install jupyterhub jupyterhub/jupyterhub --version={{ JH_CHART_VERSION }} --namespace {{ JH_NAMESPACE }} --create-namespace --values {{ JH_CONFIG_DIR }}/config.yaml --timeout 20m" + ansible.builtin.command: + cmd: "helm upgrade --cleanup-on-fail --install jupyterhub jupyterhub/jupyterhub --version={{ JH_CHART_VERSION }} --namespace {{ JH_NAMESPACE }} --create-namespace --values {{ JH_CONFIG_DIR }}/config.yaml --timeout 20m" + environment: + KUBECONFIG: "{% if k3s_found is success %}/etc/rancher/k3s/k3s.yaml{% else %}{% endif %}" async: 1200 diff --git a/tests/test.yml b/tests/test.yml index 55f9fbe..59d1fc6 100644 --- a/tests/test.yml +++ b/tests/test.yml @@ -1,5 +1,6 @@ --- -- hosts: localhost +- name: Test play + hosts: localhost remote_user: root roles: - - ansible-jupyterhub \ No newline at end of file + - cyverse_ansible.ansible_jupyterhub_docker