Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
wait4x
22 changes: 20 additions & 2 deletions scripts/common.Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,13 @@ show-venv: venv ## show venv info
@echo venv: $(VENV_DIR)

.PHONY: install
install: requirements.txt venv ## install dependencies from ./requirements.txt
@VIRTUAL_ENV=$(VENV_DIR) $(UV) pip install --requirement $<
install: guard-optional-REQUIREMENTS_FILE venv ## install requirements.txt dependencies
@if [ -z "$(REQUIREMENTS_FILE)" ]; then \
REQUIREMENTS_FILE=./requirements.txt; \
else \
REQUIREMENTS_FILE=$(REQUIREMENTS_FILE); \
fi; \
VIRTUAL_ENV=$(VENV_DIR) $(UV) pip install --requirement $$REQUIREMENTS_FILE

# https://github.com/kolypto/j2cli?tab=readme-ov-file#customization
ifeq ($(shell test -f j2cli_customization.py && echo -n yes),yes)
Expand All @@ -362,3 +367,16 @@ define jinja
endef

endif

#
# wait-fot-it functionality
#

WAIT_FOR_IT := $(REPO_BASE_DIR)/scripts/wait4x

# https://github.com/wait4x/wait4x
$(WAIT_FOR_IT):
@curl --output-dir /tmp --silent --location --remote-name https://github.com/wait4x/wait4x/releases/download/v3.5.0/wait4x-linux-amd64.tar.gz
@tar -xf /tmp/wait4x-linux-amd64.tar.gz -C $(REPO_BASE_DIR)/scripts/
@rm /tmp/wait4x-linux-amd64.tar.gz
$@ version
22 changes: 2 additions & 20 deletions services/monitoring/grafana/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,11 @@ terraform-apply: $(REPO_CONFIG_LOCATION) terraform/plan.cache $(TF_STATE_FILE) e
terraform -chdir=./terraform apply plan.cache

.PHONY: ensure-grafana-online
ensure-grafana-online:
ensure-grafana-online: $(WAIT_FOR_IT)
@set -o allexport; \
source $(REPO_CONFIG_LOCATION); \
set +o allexport; \
url=$${TF_VAR_GRAFANA_URL}; \
echo "Waiting for grafana at $$url to become reachable..."; \
attempts=0; \
max_attempts=10; \
while [ $$attempts -lt $$max_attempts ]; do \
status_code=$$(curl -k -o /dev/null -s -w "%{http_code}" --max-time 10 $$url); \
if [ "$$status_code" -ge 200 ] && [ "$$status_code" -lt 400 ]; then \
echo "Grafana is online"; \
break; \
else \
echo "Grafana still unreachable, waiting 5s for grafana to become reachable... (Attempt $$((attempts+1)))"; \
sleep 5; \
attempts=$$((attempts + 1)); \
fi; \
done; \
if [ $$attempts -eq $$max_attempts ]; then \
echo "Max attempts reached, Grafana is still unreachable."; \
exit 1; \
fi;
$(WAIT_FOR_IT) http $$TF_VAR_GRAFANA_URL --timeout=120s --interval=5s --expect-status-code 200

.PHONY: assets
assets: ${REPO_CONFIG_LOCATION}
Expand Down
42 changes: 11 additions & 31 deletions services/portainer/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,50 +15,38 @@ include ${REPO_BASE_DIR}/scripts/common.Makefile
.PHONY: up ## Deploys portainer stack
up: .init .env secrets ${TEMP_COMPOSE}
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE} ${STACK_NAME}
@$(MAKE) --noprint configure-portainer-registry

.PHONY: up-local ## Deploys portainer stack for local deployment
up-local: .init .env secrets ${TEMP_COMPOSE} ${TEMP_COMPOSE}-local
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE}-local ${STACK_NAME}

.PHONY: up-letsencrypt-http ## Deploys portainer stack using let's encrypt http challenge
up-letsencrypt-http: .init .env secrets ${TEMP_COMPOSE}-letsencrypt-http
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE}-letsencrypt-http ${STACK_NAME}

.PHONY: up-letsencrypt-dns ## Deploys portainer stack using let's encrypt dns challenge
up-letsencrypt-dns: .init .env secrets ${TEMP_COMPOSE}-letsencrypt-dns
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE}-letsencrypt-dns ${STACK_NAME}
@$(MAKE) --noprint configure-portainer-registry

.PHONY: up-dalco ## Deploys portainer stack for Dalco Cluster
up-dalco: .init .env secrets ${TEMP_COMPOSE}-dalco
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE}-dalco ${STACK_NAME}
@$(MAKE) --noprint configure-portainer-registry

.PHONY: up-aws ## Deploys portainer stack for AWS
up-aws: .init .env secrets ${TEMP_COMPOSE}-aws
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE}-aws ${STACK_NAME}
@$(MAKE) --noprint configure-portainer-registry

.PHONY: up-public ## Deploys portainer stack for public access Cluster
up-public: up-dalco

.PHONY: up-master ## Deploys portainer stack for master Cluster
up-master: .init .env secrets ${TEMP_COMPOSE}-master
@docker stack deploy --with-registry-auth --prune --compose-file ${TEMP_COMPOSE}-master ${STACK_NAME}
@$(MAKE) --noprint configure-portainer-registry


.PHONY: configure-registry
configure-registry: ## Add if necessary dockerhub registry configuration to portainer.
@set -o allexport; \
source $(REPO_CONFIG_LOCATION); \
set +o allexport; \
while [ "$$(curl -s -o /dev/null -I -w "%{http_code}" --max-time 10 -H "Accept: application/json" -H "Content-Type: application/json" -X GET https://"$$MONITORING_DOMAIN"/portainer/#/auth)" != 200 ]; do\
echo "waiting for portainer to run...";\
sleep 5s;\
done;\
echo "Updating docker-hub config";\
authentificationToken=$$(curl -o /dev/null -X POST "https://"$$MONITORING_DOMAIN"/portainer/api/auth" -H "Content-Type: application/json" -d "{ \"Username\": \"$${PORTAINER_ADMIN_LOGIN}\", \"Password\": \"$${PORTAINER_ADMIN_PWD}\"}"); \
authentificationToken=$$(echo "$$authentificationToken" | jq --raw-output '.jwt'); \
update_hub=$$(curl -o /dev/null -X PUT "https://"$$MONITORING_DOMAIN"/portainer/api/dockerhub" -H "accept: application/json" -H \
"Authorization: Bearer $${authentificationToken}" -H "Content-Type: application/json" \
-d "{ \"Authentication\": true, \"Username\": \"$$DOCKER_HUB_LOGIN\", \"Password\": \"$$DOCKER_HUB_PASSWORD\"}"); \
.PHONY: configure-portainer-registry
configure-portainer-registry: venv $(VENV_BIN)/python $(WAIT_FOR_IT) ## Add if necessary dockerhub registry configuration to portainer.
@$(MAKE) --no-print install REQUIREMENTS_FILE=./scripts/requirements.txt
@set -o allexport; source $(REPO_CONFIG_LOCATION); set +o allexport; \
$(WAIT_FOR_IT) http $$PORTAINER_URL --timeout=120s --interval=5s --expect-status-code 200 && \
$(VENV_BIN)/python ./scripts/configure_portainer_registry.py


# Helpers -------------------------------------------------
Expand All @@ -67,14 +55,6 @@ configure-registry: ## Add if necessary dockerhub registry configuration to por
${TEMP_COMPOSE}: docker-compose.yml .env
@${REPO_BASE_DIR}/scripts/docker-stack-config.bash -e .env $< > $@

.PHONY: ${TEMP_COMPOSE}-letsencrypt-http
${TEMP_COMPOSE}-letsencrypt-http: docker-compose.yml docker-compose.letsencrypt.http.yml .env
@${REPO_BASE_DIR}/scripts/docker-stack-config.bash -e .env $< docker-compose.letsencrypt.http.yml > $@

.PHONY: ${TEMP_COMPOSE}-letsencrypt-dns
${TEMP_COMPOSE}-letsencrypt-dns: docker-compose.yml docker-compose.letsencrypt.dns.yml .env
@${REPO_BASE_DIR}/scripts/docker-stack-config.bash -e .env $< docker-compose.letsencrypt.dns.yml > $@

.PHONY: ${TEMP_COMPOSE}-dalco
${TEMP_COMPOSE}-dalco: docker-compose.yml docker-compose.dalco.yml .env
@${REPO_BASE_DIR}/scripts/docker-stack-config.bash -e .env $< docker-compose.dalco.yml > $@
Expand Down
6 changes: 0 additions & 6 deletions services/portainer/docker-compose.letsencrypt.dns.yml

This file was deleted.

6 changes: 0 additions & 6 deletions services/portainer/docker-compose.letsencrypt.http.yml

This file was deleted.

138 changes: 138 additions & 0 deletions services/portainer/scripts/configure_portainer_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import logging
import os
from enum import Enum
from typing import TypedDict

import requests
from tenacity import retry

logger = logging.getLogger(__name__)


# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/portainer.Registry
class RegistryType(Enum):
DOCKER_HUB = 6


class Registry(TypedDict):
Id: int
Name: str
URL: str
Authentication: bool
Username: str
Type: RegistryType


@retry
def get_portainer_api_auth_token(
portainer_api_url: str, portainer_username: str, portainer_password: str
) -> str:
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/auth/AuthenticateUser
response = requests.post(
f"{portainer_api_url}/auth",
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/auth.authenticatePayload
json={"Username": portainer_username, "Password": portainer_password},
)

try:
response.raise_for_status()
except requests.HTTPError as e:
logger.error("Failed to authenticate with Portainer API: %s", e.response.text)
raise

return response.json()["jwt"]


@retry
def get_registries(portainer_api_url: str, auth_token: str) -> list[Registry]:
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/registries/RegistryList
response = requests.get(
f"{portainer_api_url}/registries",
headers={"Authorization": f"Bearer {auth_token}"},
)

try:
response.raise_for_status()
except requests.HTTPError as e:
logger.error("Failed to fetch registries: %s", e.response.text)
raise

return response.json()


@retry
def create_authenticated_dockerhub_registry(
portainer_api_url: str,
auth_token: str,
dockerhub_username: str,
dockerhub_password: str,
registry_name: str = "IT'IS Foundation",
) -> None:
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/registries/RegistryCreate
response = requests.post(
f"{portainer_api_url}/registries",
headers={"Authorization": f"Bearer {auth_token}"},
# https://app.swaggerhub.com/apis/portainer/portainer-ce/2.27.6#/registries.registryCreatePayload
json={
"name": registry_name,
"url": "docker.io",
"authentication": True,
"username": dockerhub_username,
"password": dockerhub_password,
"type": RegistryType.DOCKER_HUB.value,
},
)

try:
response.raise_for_status()
except requests.HTTPError as e:
logger.error(
"Failed to create authenticated Docker Hub registry: %s", e.response.text
)
raise

return response.json()


def main():
logger.info("Configuring Portainer registries...")

portainer_username = os.environ["SERVICES_USER"]
portainer_password = os.environ["SERVICES_PASSWORD"]
portainer_api_url = os.environ["PORTAINER_URL"] + "/api"

dockerhub_username = os.environ["DOCKER_HUB_LOGIN"]
dockerhub_password = os.environ["DOCKER_HUB_PASSWORD"]

portainer_jwt_token = get_portainer_api_auth_token(
portainer_api_url, portainer_username, portainer_password
)

registries = get_registries(portainer_api_url, portainer_jwt_token)

if not any(
r["Type"] == RegistryType.DOCKER_HUB.value and r["Authentication"] is True
for r in registries
):
logging.info("Creating authenticated Docker Hub registry in Portainer...")
create_authenticated_dockerhub_registry(
portainer_api_url,
portainer_jwt_token,
dockerhub_username,
dockerhub_password,
)
else:
logging.info("Portainer already has an authenticated Docker Hub registry.")

logging.info("Portainer registries configuration completed.")


def configure_logging():
logging.basicConfig(
level=logging.INFO,
)


if __name__ == "__main__":
configure_logging()
main()
2 changes: 2 additions & 0 deletions services/portainer/scripts/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tenacity==9.1.2
requests==2.32.4
Loading