Skip to content

Commit a7089d1

Browse files
committed
Automatically configure registry in portainer
Creates a single authneticated dockerhub registry that lets pulling private images (i.e. updating service's image (that is private) in portainer shall work now) * closes ITISFoundation#1089
1 parent 92a94d6 commit a7089d1

File tree

5 files changed

+140
-28
lines changed

5 files changed

+140
-28
lines changed

services/portainer/Makefile

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,6 @@ up: .init .env secrets ${TEMP_COMPOSE}
2020
up-local: .init .env secrets ${TEMP_COMPOSE} ${TEMP_COMPOSE}-local
2121
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE}-local ${STACK_NAME}
2222

23-
.PHONY: up-letsencrypt-http ## Deploys portainer stack using let's encrypt http challenge
24-
up-letsencrypt-http: .init .env secrets ${TEMP_COMPOSE}-letsencrypt-http
25-
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE}-letsencrypt-http ${STACK_NAME}
26-
27-
.PHONY: up-letsencrypt-dns ## Deploys portainer stack using let's encrypt dns challenge
28-
up-letsencrypt-dns: .init .env secrets ${TEMP_COMPOSE}-letsencrypt-dns
29-
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE}-letsencrypt-dns ${STACK_NAME}
30-
3123
.PHONY: up-dalco ## Deploys portainer stack for Dalco Cluster
3224
up-dalco: .init .env secrets ${TEMP_COMPOSE}-dalco
3325
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE}-dalco ${STACK_NAME}
@@ -67,14 +59,6 @@ configure-registry: ## Add if necessary dockerhub registry configuration to por
6759
${TEMP_COMPOSE}: docker-compose.yml .env
6860
@${REPO_BASE_DIR}/scripts/docker-stack-config.bash -e .env $< > $@
6961

70-
.PHONY: ${TEMP_COMPOSE}-letsencrypt-http
71-
${TEMP_COMPOSE}-letsencrypt-http: docker-compose.yml docker-compose.letsencrypt.http.yml .env
72-
@${REPO_BASE_DIR}/scripts/docker-stack-config.bash -e .env $< docker-compose.letsencrypt.http.yml > $@
73-
74-
.PHONY: ${TEMP_COMPOSE}-letsencrypt-dns
75-
${TEMP_COMPOSE}-letsencrypt-dns: docker-compose.yml docker-compose.letsencrypt.dns.yml .env
76-
@${REPO_BASE_DIR}/scripts/docker-stack-config.bash -e .env $< docker-compose.letsencrypt.dns.yml > $@
77-
7862
.PHONY: ${TEMP_COMPOSE}-dalco
7963
${TEMP_COMPOSE}-dalco: docker-compose.yml docker-compose.dalco.yml .env
8064
@${REPO_BASE_DIR}/scripts/docker-stack-config.bash -e .env $< docker-compose.dalco.yml > $@

services/portainer/docker-compose.letsencrypt.dns.yml

Lines changed: 0 additions & 6 deletions
This file was deleted.

services/portainer/docker-compose.letsencrypt.http.yml

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import logging
2+
import os
3+
from enum import Enum
4+
from typing import TypedDict
5+
6+
import requests
7+
from tenacity import retry
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/portainer.Registry
13+
class RegistryType(Enum):
14+
DOCKER_HUB = 6
15+
16+
17+
class Registry(TypedDict):
18+
Id: int
19+
Name: str
20+
URL: str
21+
Authentication: bool
22+
Username: str
23+
Type: RegistryType
24+
25+
26+
@retry
27+
def get_portainer_api_auth_token(
28+
portainer_api_url: str, portainer_username: str, portainer_password: str
29+
) -> str:
30+
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/auth/AuthenticateUser
31+
response = requests.post(
32+
f"{portainer_api_url}/auth",
33+
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/auth.authenticatePayload
34+
json={"Username": portainer_username, "Password": portainer_password},
35+
)
36+
37+
try:
38+
response.raise_for_status()
39+
except requests.HTTPError as e:
40+
logger.error("Failed to authenticate with Portainer API: %s", e.response.text)
41+
raise
42+
43+
return response.json()["jwt"]
44+
45+
46+
@retry
47+
def get_registries(portainer_api_url: str, auth_token: str) -> list[Registry]:
48+
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/registries/RegistryList
49+
response = requests.get(
50+
f"{portainer_api_url}/registries",
51+
headers={"Authorization": f"Bearer {auth_token}"},
52+
)
53+
54+
try:
55+
response.raise_for_status()
56+
except requests.HTTPError as e:
57+
logger.error("Failed to fetch registries: %s", e.response.text)
58+
raise
59+
60+
return response.json()
61+
62+
63+
@retry
64+
def create_authenticated_dockerhub_registry(
65+
portainer_api_url: str,
66+
auth_token: str,
67+
dockerhub_username: str,
68+
dockerhub_password: str,
69+
registry_name: str = "IT'IS Foundation",
70+
) -> None:
71+
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/registries/RegistryCreate
72+
response = requests.post(
73+
f"{portainer_api_url}/registries",
74+
headers={"Authorization": f"Bearer {auth_token}"},
75+
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/registries.registryCreatePayload
76+
json={
77+
"name": registry_name,
78+
"url": "docker.io",
79+
"authentication": True,
80+
"username": dockerhub_username,
81+
"password": dockerhub_password,
82+
"type": RegistryType.DOCKER_HUB.value,
83+
},
84+
)
85+
86+
try:
87+
response.raise_for_status()
88+
except requests.HTTPError as e:
89+
logger.error(
90+
"Failed to create authenticated Docker Hub registry: %s", e.response.text
91+
)
92+
raise
93+
94+
return response.json()
95+
96+
97+
def main():
98+
logger.info("Configuring Portainer registries...")
99+
100+
portainer_username = os.environ["SERVICES_USER"]
101+
portainer_password = os.environ["SERVICES_PASSWORD"]
102+
portainer_api_url = os.environ["PORTAINER_URL"] + "/api"
103+
104+
dockerhub_username = os.environ["DOCKER_HUB_LOGIN"]
105+
dockerhub_password = os.environ["DOCKER_HUB_PASSWORD"]
106+
107+
portainer_jwt_token = get_portainer_api_auth_token(
108+
portainer_api_url, portainer_username, portainer_password
109+
)
110+
111+
registries = get_registries(portainer_api_url, portainer_jwt_token)
112+
113+
if not any(
114+
r["Type"] == RegistryType.DOCKER_HUB.value and r["Authentication"] is True
115+
for r in registries
116+
):
117+
logging.info("Creating authenticated Docker Hub registry in Portainer...")
118+
create_authenticated_dockerhub_registry(
119+
portainer_api_url,
120+
portainer_jwt_token,
121+
dockerhub_username,
122+
dockerhub_password,
123+
)
124+
else:
125+
logging.info("Portainer already has an authenticated Docker Hub registry.")
126+
127+
logging.info("Portainer registries configuration completed.")
128+
129+
130+
def configure_logging():
131+
logging.basicConfig(
132+
level=logging.INFO,
133+
)
134+
135+
136+
if __name__ == "__main__":
137+
configure_logging()
138+
main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tenacity==2.32.4
2+
requests==9.1.2

0 commit comments

Comments
 (0)