From fcfb3434b6320b59abc6f9c7b04a6b073132771e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Franti=C5=A1ek=20Dvo=C5=99=C3=A1k?= <valtri@civ.zcu.cz>
Date: Thu, 22 May 2025 13:40:24 +0000
Subject: [PATCH] Sonatype Nexus repository deployment
* use Hashicorp Vault
* document used secrets in playbook
* two repositories: private for notebooks and public for binder
* required 2 vCPU, recommended 4 vCPU
* hack due to kubectl wait problem
* increased timeouts for bigger images
---
common/playbooks/repository-nexus.yaml | 186 ++++++++++++++++++
.../playbooks/subtasks/nexus-repository.yaml | 23 +++
common/playbooks/subtasks/nexus-role.yaml | 23 +++
common/playbooks/subtasks/nexus-user.yaml | 27 +++
common/playbooks/templates/nexus.yaml | 149 ++++++++++++++
.../playbooks/templates/nexus/blobstore.yaml | 4 +
common/playbooks/templates/nexus/realms.yaml | 3 +
.../templates/nexus/repository-binder.yaml | 12 ++
.../templates/nexus/repository-notebooks.yaml | 11 ++
.../templates/nexus/role-anonymous.yaml | 10 +
.../templates/nexus/role-binder.yaml | 10 +
.../templates/nexus/role-notebooks-read.yaml | 7 +
.../templates/nexus/role-notebooks-write.yaml | 10 +
.../templates/nexus/user-binder.yaml | 9 +
.../nexus/user-notebooks-reader.yaml | 9 +
.../nexus/user-notebooks-writer.yaml | 9 +
16 files changed, 502 insertions(+)
create mode 100644 common/playbooks/repository-nexus.yaml
create mode 100644 common/playbooks/subtasks/nexus-repository.yaml
create mode 100644 common/playbooks/subtasks/nexus-role.yaml
create mode 100644 common/playbooks/subtasks/nexus-user.yaml
create mode 100644 common/playbooks/templates/nexus.yaml
create mode 100644 common/playbooks/templates/nexus/blobstore.yaml
create mode 100644 common/playbooks/templates/nexus/realms.yaml
create mode 100644 common/playbooks/templates/nexus/repository-binder.yaml
create mode 100644 common/playbooks/templates/nexus/repository-notebooks.yaml
create mode 100644 common/playbooks/templates/nexus/role-anonymous.yaml
create mode 100644 common/playbooks/templates/nexus/role-binder.yaml
create mode 100644 common/playbooks/templates/nexus/role-notebooks-read.yaml
create mode 100644 common/playbooks/templates/nexus/role-notebooks-write.yaml
create mode 100644 common/playbooks/templates/nexus/user-binder.yaml
create mode 100644 common/playbooks/templates/nexus/user-notebooks-reader.yaml
create mode 100644 common/playbooks/templates/nexus/user-notebooks-writer.yaml
diff --git a/common/playbooks/repository-nexus.yaml b/common/playbooks/repository-nexus.yaml
new file mode 100644
index 0000000..d3f40c1
--- /dev/null
+++ b/common/playbooks/repository-nexus.yaml
@@ -0,0 +1,186 @@
+---
+# Secrets in "/nexus" (user passwords):
+#
+# * admin
+# * binder
+# * notebooks-reader
+# * notebooks-writer
+#
+# (the list is dynamic based on templates/nexus/user-*.yaml)
+#
+- name: Sonatype Nexus Deployment
+ hosts: master[0]
+ vars:
+ nexus_url: "https://{{ nexus_hostname }}/service/rest/v1"
+ nexus_blobstore_name: default
+ nexus_blobstore_type: file
+ nexus_repository_binder_name: binder
+ nexus_repository_notebooks_name: notebooks
+ nexus_repository_port: 8082
+ secrets: "{{ lookup('community.hashi_vault.hashi_vault', (vault_mount_point, 'nexus') | join('/'), token_validate=false) }}"
+ become: true
+ tasks:
+ - name: Debug Secrets
+ debug:
+ msg: "{{ item.key }} = {{ item.value }}"
+ loop: "{{ secrets | dict2items }}"
+ - name: Create Nexus configuration file on master
+ vars:
+ name: nexus
+ template:
+ src: templates/nexus.yaml
+ dest: /tmp/nexus.yaml
+ mode: 0600
+ - name: Deploy/update Nexus instance
+ command: kubectl apply -f /tmp/nexus.yaml
+ environment:
+ KUBECONFIG: /etc/kubernetes/admin.conf
+ PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
+ changed_when: true
+ when: true
+ - name: Wait for Nexus pod ready
+ command: kubectl wait pod --all --namespace nexus --for condition=ready --timeout=5m
+ environment:
+ KUBECONFIG: /etc/kubernetes/admin.conf
+ changed_when: false
+ when: true
+ # workaround problem with kubectl wait
+ retries: 1
+ - name: Wait for Nexus REST API
+ uri:
+ url: "{{ nexus_url }}/status"
+ status_code: 200
+ method: GET
+ register: _result
+ until: _result.status == 200
+ retries: 120
+ delay: 15
+ - name: Check the admin password
+ uri:
+ url: "{{ nexus_url }}/status"
+ force_basic_auth: true
+ method: HEAD
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ status_code: 200, 401
+ register: nexus_admin_password_check
+ - name: Admin password setup
+ when:
+ - nexus_admin_password_check.status == 401
+ block:
+ - name: Get initial admin password
+ shell: 'kubectl exec -it -n nexus $(kubectl get pod -n nexus -l app=sonatype-nexus -o name) -- cat /nexus-data/admin.password'
+ register: nexus_admin_password_initial
+ changed_when: false
+ environment:
+ KUBECONFIG: /etc/kubernetes/admin.conf
+ - name: Set the admin password
+ uri:
+ url: "{{ nexus_url }}/security/users/admin/change-password"
+ force_basic_auth: true
+ headers:
+ Content-Type: text/plain
+ method: PUT
+ user: 'admin'
+ password: "{{ nexus_admin_password_initial.stdout }}"
+ body: "{{ secrets['admin'] }}"
+ body_format: raw
+ status_code: [200, 204]
+ rescue:
+ - name: Admin Password Setup Fail
+ fail:
+ msg: "Failed admin password setup"
+ - name: Check blobstore
+ uri:
+ url: "{{ nexus_url }}/blobstores/{{ nexus_blobstore_type }}/{{ nexus_blobstore_name }}"
+ force_basic_auth: true
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ # XXX: workaround REST API bug for S3 (Nexus 3.33.0-01)
+ status_code: [200, 400, 404, 500]
+ register: nexus_blobstore_check
+ # XXX: REST API bug II - needs to be created manually
+ - name: Create blobstore
+ when: &blobstore_changed
+ - nexus_blobstore_check.status == 404 or nexus_blobstore_check.status == 400
+ uri:
+ url: "{{ nexus_url }}/blobstores/{{ nexus_blobstore_type }}"
+ force_basic_auth: true
+ method: POST
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ body: "{{ lookup('template', 'templates/nexus/blobstore.yaml') | from_yaml }}"
+ body_format: json
+ status_code: [200, 201]
+ changed_when: *blobstore_changed
+ - name: Check repository
+ uri:
+ url: "{{ nexus_url }}/repositories/docker/hosted/{{ nexus_repository_binder_name }}"
+ force_basic_auth: true
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ status_code: [200, 404]
+ register: nexus_repository_check
+ - name: Delete original repositories
+ when: &repositories_deleted
+ - nexus_repository_check.status == 404
+ uri:
+ url: "{{ nexus_url }}/repositories/{{ item }}"
+ force_basic_auth: true
+ method: DELETE
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ status_code: [200, 204, 404]
+ register: _result
+ loop:
+ - maven-central
+ - maven-public
+ - maven-releases
+ - maven-snapshots
+ - nuget-group
+ - nuget-hosted
+ - nuget.org-proxy
+ changed_when: _result.status == 200 or _result.status == 204
+ - name: Create repositories
+ include_tasks: subtasks/nexus-repository.yaml
+ loop:
+ - name: "{{ nexus_repository_binder_name }}"
+ type: docker/hosted
+ - name: "{{ nexus_repository_notebooks_name }}"
+ type: docker/hosted
+ - name: Create roles
+ include_tasks: subtasks/nexus-role.yaml
+ vars:
+ rolename: "{{ item | basename | splitext | first | regex_replace('^role-', '') }}"
+ with_fileglob:
+ - "templates/nexus/role-*.yaml"
+ - name: Create users
+ include_tasks: subtasks/nexus-user.yaml
+ vars:
+ username: "{{ item | basename | splitext | first | regex_replace('^user-', '') }}"
+ with_fileglob:
+ - "templates/nexus/user-*.yaml"
+ - name: Check security realms
+ uri:
+ url: "{{ nexus_url }}/security/realms/active"
+ force_basic_auth: true
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ return_content: true
+ register: nexus_realms_check
+ - name: Update securty realms
+ when: &realms_changed
+ - '"DockerToken" not in nexus_realms_check.content'
+ uri:
+ url: "{{ nexus_url }}/security/realms/active"
+ force_basic_auth: true
+ headers:
+ accept: application/json
+ Content-Type: application/json
+ method: PUT
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ body: "{{ lookup('template', 'templates/nexus/realms.yaml') | from_yaml }}"
+ body_format: json
+ status_code: [200, 204]
+ changed_when: *realms_changed
diff --git a/common/playbooks/subtasks/nexus-repository.yaml b/common/playbooks/subtasks/nexus-repository.yaml
new file mode 100644
index 0000000..4787166
--- /dev/null
+++ b/common/playbooks/subtasks/nexus-repository.yaml
@@ -0,0 +1,23 @@
+---
+- name: Check repository {{ item.name }}
+ uri:
+ url: "{{ nexus_url }}/repositories/{{ item.type }}/{{ item.name }}"
+ force_basic_auth: true
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ status_code: [200, 404]
+ register: nexus_repository_check
+
+- name: Create repository {{ item.name }}
+ when: &repository_created
+ - nexus_repository_check.status == 404
+ uri:
+ url: "{{ nexus_url }}/repositories/{{ item.type }}"
+ force_basic_auth: true
+ method: POST
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ body: "{{ lookup('template', 'templates/nexus/repository-' + item.name + '.yaml') | from_yaml }}"
+ body_format: json
+ status_code: [200, 201]
+ changed_when: *repository_created
diff --git a/common/playbooks/subtasks/nexus-role.yaml b/common/playbooks/subtasks/nexus-role.yaml
new file mode 100644
index 0000000..498c498
--- /dev/null
+++ b/common/playbooks/subtasks/nexus-role.yaml
@@ -0,0 +1,23 @@
+---
+- name: Check role {{ rolename }}
+ uri:
+ url: "{{ nexus_url }}/security/roles/{{ rolename }}"
+ force_basic_auth: true
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ status_code: [200, 404]
+ register: nexus_role_check
+
+- name: Create role {{ rolename }}
+ when: &role_created
+ - nexus_role_check.status == 404
+ uri:
+ url: "{{ nexus_url }}/security/roles"
+ force_basic_auth: true
+ method: POST
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ body: "{{ lookup('template', item) | from_yaml }}"
+ body_format: json
+ status_code: [200, 201]
+ changed_when: *role_created
diff --git a/common/playbooks/subtasks/nexus-user.yaml b/common/playbooks/subtasks/nexus-user.yaml
new file mode 100644
index 0000000..0d3a7a1
--- /dev/null
+++ b/common/playbooks/subtasks/nexus-user.yaml
@@ -0,0 +1,27 @@
+---
+- name: Check user {{ username }}
+ uri:
+ url: "{{ nexus_url }}/security/users?userId={{ username }}"
+ force_basic_auth: true
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ return_content: true
+ status_code: [200, 404]
+ register: nexus_user_check
+
+- name: Create user {{ username }}
+ when: &user_created
+ - username not in nexus_user_check.content
+ uri:
+ url: "{{ nexus_url }}/security/users"
+ force_basic_auth: true
+ headers:
+ accept: application/json
+ Content-Type: application/json
+ method: POST
+ user: 'admin'
+ password: "{{ secrets['admin'] }}"
+ body: "{{ lookup('template', item) | from_yaml }}"
+ body_format: json
+ status_code: [200, 201]
+ changed_when: *user_created
diff --git a/common/playbooks/templates/nexus.yaml b/common/playbooks/templates/nexus.yaml
new file mode 100644
index 0000000..6208be2
--- /dev/null
+++ b/common/playbooks/templates/nexus.yaml
@@ -0,0 +1,149 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: {{ name }}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: nexus-pvc
+ namespace: {{ name }}
+ labels:
+ app: sonatype-nexus
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 500Gi
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nexus
+ namespace: {{ name }}
+ labels:
+ app: sonatype-nexus
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: sonatype-nexus
+ template:
+ metadata:
+ labels:
+ app: sonatype-nexus
+ spec:
+ containers:
+ - image: sonatype/nexus3
+ imagePullPolicy: Always
+ name: nexus
+ ports:
+ - containerPort: 8081
+ - containerPort: {{ nexus_repository_port }}
+ - containerPort: {{ nexus_repository_port + 1 }}
+ resources:
+ requests:
+ cpu: 2
+ limits:
+ cpu: 4
+ volumeMounts:
+ - mountPath: /nexus-data
+ name: nexus-data-volume
+ volumes:
+ - name: nexus-data-volume
+ persistentVolumeClaim:
+ claimName: nexus-pvc
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: nexus
+ namespace: {{ name }}
+spec:
+ ports:
+ - port: 80
+ targetPort: 8081
+ protocol: TCP
+ name: http
+ - port: 5000
+ targetPort: {{ nexus_repository_port }}
+ protocol: TCP
+ name: docker-container-notebooks
+ - port: 5001
+ targetPort: {{ nexus_repository_port + 1 }}
+ protocol: TCP
+ name: docker-repository
+ selector:
+ app: sonatype-nexus
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: nexus-ingress
+ namespace: nexus
+ annotations:
+ kubernetes.io/ingress.class: "nginx"
+ kubernetes.io/tls-acme: "true"
+ ingress.kubernetes.io/proxy-body-size: 100m
+ nginx.ingress.kubernetes.io/proxy-connect-timeout: "15"
+ nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
+ nginx.ingress.kubernetes.io/proxy-next-upstream-timeout: "3600"
+ nginx.ingress.kubernetes.io/proxy-request-buffering: "on"
+spec:
+ tls:
+ - hosts:
+ - {{ nexus_hostname }}
+ - {{ registry_binder_hostname }}
+ - {{ registry_notebooks_hostname }}
+ secretName: acme-tls-{{ name }}
+ rules:
+ - host: {{ nexus_hostname }}
+ http:
+ paths:
+ - backend:
+ service:
+ name: nexus
+ port:
+ number: 80
+ path: /
+ pathType: Prefix
+ - host: {{ registry_binder_hostname }}
+ http:
+ paths:
+ - backend:
+ service:
+ name: nexus
+ port:
+ number: 5000
+ path: /
+ pathType: Prefix
+ - host: {{ registry_notebooks_hostname }}
+ http:
+ paths:
+ - backend:
+ service:
+ name: nexus
+ port:
+ number: 5001
+ path: /
+ pathType: Prefix
+# direct access without nginx layer and SSL (for debugging)
+# ---
+# apiVersion: v1
+# kind: Service
+# metadata:
+# name: nexus-repository-direct
+# namespace: {{ name }}
+# spec:
+# type: NodePort
+# selector:
+# app: sonatype-nexus
+# ports:
+# - port: 5002
+# targetPort: {{ nexus_repository_port + 1 }}
+# protocol: TCP
+# nodePort: 31444
+# externalIPs: {{ groups['ingress'] }}
diff --git a/common/playbooks/templates/nexus/blobstore.yaml b/common/playbooks/templates/nexus/blobstore.yaml
new file mode 100644
index 0000000..5c46365
--- /dev/null
+++ b/common/playbooks/templates/nexus/blobstore.yaml
@@ -0,0 +1,4 @@
+---
+name: {{ nexus_blobstore_name }}
+
+path: default
diff --git a/common/playbooks/templates/nexus/realms.yaml b/common/playbooks/templates/nexus/realms.yaml
new file mode 100644
index 0000000..c49f88f
--- /dev/null
+++ b/common/playbooks/templates/nexus/realms.yaml
@@ -0,0 +1,3 @@
+---
+- NexusAuthenticatingRealm
+- DockerToken
diff --git a/common/playbooks/templates/nexus/repository-binder.yaml b/common/playbooks/templates/nexus/repository-binder.yaml
new file mode 100644
index 0000000..5cf07bd
--- /dev/null
+++ b/common/playbooks/templates/nexus/repository-binder.yaml
@@ -0,0 +1,12 @@
+---
+name: {{ nexus_repository_binder_name }}
+online: true
+storage:
+ blobStoreName: {{ nexus_blobstore_name }}
+ strictContentTypeValidation: true
+ writePolicy: allow
+docker:
+ v1Enabled: false
+ # basic-auth worked only with binder 0.2.0-n577.h14cc6c7 + jupyterhub 0.11.1
+ forceBasicAuth: false
+ httpPort: {{ nexus_repository_port }}
diff --git a/common/playbooks/templates/nexus/repository-notebooks.yaml b/common/playbooks/templates/nexus/repository-notebooks.yaml
new file mode 100644
index 0000000..7447e5f
--- /dev/null
+++ b/common/playbooks/templates/nexus/repository-notebooks.yaml
@@ -0,0 +1,11 @@
+---
+name: {{ nexus_repository_notebooks_name }}
+online: true
+storage:
+ blobStoreName: {{ nexus_blobstore_name }}
+ strictContentTypeValidation: true
+ writePolicy: allow
+docker:
+ v1Enabled: false
+ forceBasicAuth: true
+ httpPort: {{ nexus_repository_port + 1 }}
diff --git a/common/playbooks/templates/nexus/role-anonymous.yaml b/common/playbooks/templates/nexus/role-anonymous.yaml
new file mode 100644
index 0000000..f88aaf0
--- /dev/null
+++ b/common/playbooks/templates/nexus/role-anonymous.yaml
@@ -0,0 +1,10 @@
+---
+id: anonymous
+name: anonymous
+description: Anonymous Role for Jupyter Binder repository manager
+# only explicit repository read roles to avoid access to the internal repository
+privileges:
+ - nx-healthcheck-read
+ - nx-repository-view-docker-{{ nexus_repository_binder_name }}-browse
+ - nx-repository-view-docker-{{ nexus_repository_binder_name }}-read
+ - nx-search-read
diff --git a/common/playbooks/templates/nexus/role-binder.yaml b/common/playbooks/templates/nexus/role-binder.yaml
new file mode 100644
index 0000000..977d887
--- /dev/null
+++ b/common/playbooks/templates/nexus/role-binder.yaml
@@ -0,0 +1,10 @@
+---
+id: binder
+name: binder
+description: Jupyter Binder
+privileges:
+ - nx-repository-view-docker-{{ nexus_repository_binder_name }}-add
+ - nx-repository-view-docker-{{ nexus_repository_binder_name }}-edit
+ - nx-repository-view-docker-{{ nexus_repository_binder_name }}-read
+roles:
+ - anonymous
diff --git a/common/playbooks/templates/nexus/role-notebooks-read.yaml b/common/playbooks/templates/nexus/role-notebooks-read.yaml
new file mode 100644
index 0000000..13a2e7b
--- /dev/null
+++ b/common/playbooks/templates/nexus/role-notebooks-read.yaml
@@ -0,0 +1,7 @@
+---
+id: {{ nexus_repository_notebooks_name }}-read
+name: {{ nexus_repository_notebooks_name }}-read
+description: Jupyter Notebooks internal repository read access
+privileges:
+ - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-browse
+ - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-read
diff --git a/common/playbooks/templates/nexus/role-notebooks-write.yaml b/common/playbooks/templates/nexus/role-notebooks-write.yaml
new file mode 100644
index 0000000..38f5883
--- /dev/null
+++ b/common/playbooks/templates/nexus/role-notebooks-write.yaml
@@ -0,0 +1,10 @@
+---
+id: {{ nexus_repository_notebooks_name }}-write
+name: {{ nexus_repository_notebooks_name }}-write
+description: Jupyter Notebooks internal repository write access
+privileges:
+ - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-add
+ - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-browse
+ - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-delete
+ - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-edit
+ - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-read
diff --git a/common/playbooks/templates/nexus/user-binder.yaml b/common/playbooks/templates/nexus/user-binder.yaml
new file mode 100644
index 0000000..032c8fd
--- /dev/null
+++ b/common/playbooks/templates/nexus/user-binder.yaml
@@ -0,0 +1,9 @@
+---
+userId: binder
+firstName: Jupyter Binder
+lastName: Writer
+emailAddress: binder@{{ registry_binder_hostname }}
+password: {{ secrets['binder'] }}
+status: active
+roles:
+ - binder
diff --git a/common/playbooks/templates/nexus/user-notebooks-reader.yaml b/common/playbooks/templates/nexus/user-notebooks-reader.yaml
new file mode 100644
index 0000000..0329bc0
--- /dev/null
+++ b/common/playbooks/templates/nexus/user-notebooks-reader.yaml
@@ -0,0 +1,9 @@
+---
+userId: notebooks-reader
+firstName: Jupyter Notebooks
+lastName: Reader
+emailAddress: notebooks-reader@{{ registry_notebooks_hostname }}
+password: {{ secrets['notebooks-reader'] }}
+status: active
+roles:
+ - {{ nexus_repository_notebooks_name }}-read
diff --git a/common/playbooks/templates/nexus/user-notebooks-writer.yaml b/common/playbooks/templates/nexus/user-notebooks-writer.yaml
new file mode 100644
index 0000000..40c3fc0
--- /dev/null
+++ b/common/playbooks/templates/nexus/user-notebooks-writer.yaml
@@ -0,0 +1,9 @@
+---
+userId: notebooks-writer
+firstName: Jupyter Notebooks
+lastName: Writer
+emailAddress: notebooks-writer@{{ registry_notebooks_hostname }}
+password: {{ secrets['notebooks-writer'] }}
+status: active
+roles:
+ - {{ nexus_repository_notebooks_name }}-write
--
GitLab