Compare commits

...

70 Commits

Author SHA1 Message Date
c22287f53b build 2025-12-23 13:44:28 +01:00
f618476534 Update .gitlab-ci.yml file 2025-12-23 13:43:58 +01:00
5865df9abc Update .gitlab-ci.yml file 2025-12-23 13:43:42 +01:00
f83ee560c1 Update .gitlab-ci.yml file 2025-12-23 13:32:45 +01:00
5ce8573013 build 2025-12-23 13:19:41 +01:00
e8191802b1 Update .gitlab-ci.yml file 2025-12-23 13:19:07 +01:00
e546d0cf3f build 2025-12-23 13:16:48 +01:00
daf219329a Update .gitlab-ci.yml file 2025-12-23 13:15:47 +01:00
b601ecc0c3 build 2025-12-23 13:13:44 +01:00
1a8e532a02 build 2025-12-23 12:52:18 +01:00
d6e4db6dd4 Update .gitlab-ci.yml file 2025-12-23 12:52:02 +01:00
78012cec65 build 2025-12-23 12:46:55 +01:00
ba098499f5 Update .gitlab-ci.yml file 2025-12-23 12:45:58 +01:00
ce24b5c00d build 2025-12-23 12:45:09 +01:00
46143a7c12 build 2025-12-23 12:39:42 +01:00
c731fbe0de Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 12:39:36 +01:00
d878a2baa0 build 2025-12-23 12:39:32 +01:00
e280ea67f7 Update .gitlab-ci.yml file 2025-12-23 12:39:03 +01:00
260eb63262 build 2025-12-23 12:37:57 +01:00
41d6ec9914 Update .gitlab-ci.yml file 2025-12-23 12:37:44 +01:00
15e442d49b build 2025-12-23 12:35:33 +01:00
14c31575af build 2025-12-23 12:30:03 +01:00
406513b4b8 Update .gitlab-ci.yml file 2025-12-23 12:29:03 +01:00
0c1b624972 Update .gitlab-ci.yml file 2025-12-23 12:25:05 +01:00
db5209e3fb build 2025-12-23 12:17:24 +01:00
111c70ef00 Update .gitlab-ci.yml file 2025-12-23 12:17:07 +01:00
8bba4d1d18 build 2025-12-23 12:10:06 +01:00
db45a48106 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 12:10:00 +01:00
53438a3fb0 build 2025-12-23 12:09:56 +01:00
5646e0692d Update .gitlab-ci.yml file 2025-12-23 12:08:17 +01:00
6634cc20fa build 2025-12-23 12:05:41 +01:00
fc9f25a203 Update .gitlab-ci.yml file 2025-12-23 12:05:25 +01:00
8351e9f1b1 Update .gitlab-ci.yml file 2025-12-23 12:05:13 +01:00
d007510704 build 2025-12-23 12:03:42 +01:00
3e202c9fd8 build 2025-12-23 11:55:22 +01:00
d6e5c4087d build 2025-12-23 11:53:06 +01:00
f70ebecc49 build 2025-12-23 11:41:50 +01:00
b8264994c5 Update .gitlab-ci.yml file 2025-12-23 11:41:37 +01:00
6081c44d4c build 2025-12-23 11:40:49 +01:00
abb5fc7708 Update .gitlab-ci.yml file 2025-12-23 11:40:31 +01:00
a9eecac96d build 2025-12-23 11:35:19 +01:00
e7bcee762d build 2025-12-23 11:33:51 +01:00
e3fed3304a Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 11:33:46 +01:00
338aa66565 build 2025-12-23 11:33:42 +01:00
058554a0ea Update .gitlab-ci.yml file 2025-12-23 11:33:19 +01:00
b28a7c8273 build 2025-12-23 11:27:57 +01:00
e464c498ff build 2025-12-23 11:22:46 +01:00
b028a48fc0 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 11:22:40 +01:00
b2373f7016 build 2025-12-23 11:22:36 +01:00
1b50b3337c Update .gitlab-ci.yml file 2025-12-23 11:22:09 +01:00
ac68b5be6f build 2025-12-23 11:20:43 +01:00
035abffeab Update .gitlab-ci.yml file 2025-12-23 11:20:29 +01:00
6097d2b442 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 11:19:44 +01:00
55bdff1745 build 2025-12-23 11:19:30 +01:00
b385f3db12 Update .gitlab-ci.yml file 2025-12-23 11:18:49 +01:00
94a3ccfd23 Update .gitlab-ci.yml file 2025-12-23 11:18:03 +01:00
b0c570d7ba build 2025-12-23 11:16:56 +01:00
4339a7d769 build 2025-12-23 10:59:28 +01:00
039078191f build 2025-12-23 10:56:22 +01:00
f5d76d87e0 build 2025-12-23 09:55:31 +01:00
f561508d2e build 2025-12-21 00:52:21 +01:00
974966fdd8 build 2025-12-21 00:49:24 +01:00
63e158899e build 2025-12-20 15:22:59 +01:00
9336b56f96 build 2025-12-20 15:22:40 +01:00
66fba7b994 build 2025-12-20 15:20:29 +01:00
7804dbb117 build 2025-12-20 15:19:00 +01:00
fb1763e14d build 2025-12-20 15:16:40 +01:00
829891d1ba build 2025-12-20 15:13:14 +01:00
174aab4faa build 2025-12-19 17:23:20 +01:00
9c6445ee03 build 2025-12-19 17:08:15 +01:00
5 changed files with 124 additions and 69 deletions

View File

@@ -1,10 +1,20 @@
stages: # List of stages for jobs, and their order of execution
# - notify_start
- lint
- build
- clean
- notify
variables:
GIT_SSH_COMMAND: "ssh -i /home/gitlab-runner/.ssh/id_rsa -o IdentitiesOnly=yes"
# notify_start:
# stage: notify_start # Should be in a later stage than the job that might fail
# script:
# - column=':'
# - echo "${flow_id}"
# - curl -XPOST http://192.168.77.101:8123/api/webhook/voice-notifications-tC_8YKxMJIAaQRV5riKuC7Zl --data-raw 'message=portainer build job started'
# - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
# rules:
# - if: '$CI_COMMIT_MESSAGE =~ /build/'
lint:
stage: lint
image: r.sectorq.eu/jaydee/builder-portainer:latest
@@ -22,15 +32,19 @@ lint:
build-job: # This job runs in the build stage, which runs first.
stage: build
image: r.sectorq.eu/jaydee/builder-portainer:latest
before_script:
- export PYTHONPATH=$PWD
script:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- pyinstaller --onefile portainer.py
#- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.80.222:/myapps/bin/ || true
- ls -la
- pyinstaller --onefile --clean --hidden-import=portainer_api portainer.py
- strings dist/portainer | grep portainer_api || true
- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.80.222:/myapps/bin/ || true
- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.77.12:/myapps/bin/ || true
- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.77.101:/myapps/bin/ || true
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.*
artifacts:
paths:
- dist/
@@ -53,7 +67,7 @@ cleanup_on_failure_job:
script:
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
notify:
notify_complete:
stage: notify # Should be in a later stage than the job that might fail
when: on_success # <-- This is the key keyword
script:
@@ -61,8 +75,9 @@ notify:
- echo "${flow_id}"
- curl -XPOST http://192.168.77.101:8123/api/webhook/voice-notifications-tC_8YKxMJIAaQRV5riKuC7Zl --data-raw 'message=portainer build job completed'
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
notify2:
rules:
- if: '$CI_COMMIT_MESSAGE =~ /build/'
notify_failed:
stage: notify # Should be in a later stage than the job that might fail
when: on_failure # <-- This is the key keyword
script:

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.defaultInterpreterPath": "../../venvs/portainer/bin/python"
}

View File

@@ -5,18 +5,15 @@ This module provides a wrapper for interacting with the Portainer API
to manage endpoints, stacks, and containers.
"""
# !/myapps/venvs/portainer/bin/python3
import os
import logging
import signal
import sys
import json
import argparse
import tty
import termios
import hvac
from tabulate import tabulate
from port import Portainer
from portainer_api import PortainerApi
from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.shortcuts import checkboxlist_dialog
@@ -39,7 +36,7 @@ else:
raise Exception("Failed to authenticate with Vault")
# Specify the mount point of your KV engine
VERSION = "0.1.16"
VERSION = "0.1.42"
defaults = {
"endpoint_id": "vm01",
@@ -83,10 +80,6 @@ def load_config(defaults=defaults):
print("Configuration written to /myapps/portainer.conf")
return cur_config
a = load_config(defaults)
# ENV_VARS = [
@@ -109,7 +102,11 @@ def update_configs(cur_config):
print("Configuration written to /myapps/portainer.conf")
parser = argparse.ArgumentParser(
description="Portainer helper - use env vars or pass credentials."
description=f"""\
Portainer helper - use env vars or pass credentials."
version: {VERSION}
""",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--base",
@@ -139,10 +136,12 @@ parser.add_argument(
)
parser.add_argument("--update", "-u", action="store_true", help="Update service if it exists")
parser.add_argument("--debug", "-D", action="store_true")
parser.add_argument("--launcher", "-L", action="store_true")
parser.add_argument("--gpu", "-g", action="store_true")
parser.add_argument("--timeout", type=int, default=10, help="Request timeout seconds")
parser.add_argument("--deploy-mode", "-m", type=str, default="git", help="Deploy mode")
parser.add_argument("--stack-mode", "-w", default=None, help="Stack mode")
args = parser.parse_args()
print("Running version:", VERSION)
print("Environment:", args.site)
@@ -372,7 +371,7 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
if __name__ == "__main__":
# Example usage: set PORTAINER_USER and PORTAINER_PASS in env, or pass literals below.
# token = get_portainer_token(base,"admin","l4c1j4yd33Du5lo") # or get_portainer_token(base, "admin", "secret")
def signal_handler(sig, frame):
logger.warning("Killed manually %s, %s", sig, frame)
print("\nTerminated by user")
@@ -426,7 +425,7 @@ if __name__ == "__main__":
os.system("cls" if os.name == "nt" else "clear")
# Example: list endpoints
por = Portainer(cur_config["PORTAINER_SITE"], args)
por = PortainerApi(cur_config["PORTAINER_SITE"], args)
por.set_defaults(cur_config)
if args.debug:
por._debug = True
@@ -547,6 +546,8 @@ if __name__ == "__main__":
],
)
por.update_service()
if args.launcher:
input("\nPress ENTER to continue...")
sys.exit()
if args.action == "update_containers":
@@ -574,12 +575,24 @@ if __name__ == "__main__":
],
)
por.print_stacks(args)
if args.launcher:
input("Press ENTER to continue...")
# print(json.dumps(por.all_data, indent=2))
sys.exit()
if args.action == "list_containers":
print("Getting containers")
print(por.get_containers())
args = prompt_missing_args(
args,
cur_config,
[
("site", "Site"),
("endpoint_id", "Endpoint ID"),
],
)
print("\n".join(por.get_containers()))
if args.launcher:
input("\nPress ENTER to continue...")
sys.exit()
if args.action == "update_stack":
@@ -593,11 +606,16 @@ if __name__ == "__main__":
)
por.update_stack(args)
if args.launcher:
input("\nPress ENTER to continue...")
sys.exit()
if args.action == "print_all_data":
print(json.dumps(por.all_data, indent=2))
if args.launcher:
input("\nPress ENTER to continue...")
sys.exit()
if args.action == "update_status":
por.update_status(args.endpoint_id, args.stack)
sys.exit()
@@ -609,10 +627,20 @@ if __name__ == "__main__":
export_data.append([i, eps["by_id"][i]])
headers = ["EndpointId", "Name"]
print(tabulate(export_data, headers=headers, tablefmt="github"))
if args.launcher:
input("\nPress ENTER to continue...")
sys.exit()
if args.action == "stop_containers":
# TODO: does not work
args = prompt_missing_args(
args,
cur_config,
[
("site", "Site"),
("endpoint_id", "Endpoint ID"),
],
)
if por.all_data["endpoints_status"][args.endpoint_id] != 1:
print(f"Endpoint {por.get_endpoint_name(args.endpoint_id)} is offline")
sys.exit()

View File

@@ -21,7 +21,7 @@ from prompt_toolkit.shortcuts import radiolist_dialog
logger = logging.getLogger(__name__)
class Portainer:
class PortainerApi:
"""
Simple wrapper around the module-level Portainer helper functions.
Instantiate with base_url and optional token/timeout and call methods
@@ -315,6 +315,7 @@ class Portainer:
def get_endpoint_id(self):
'''Get endpoint ID from either ID or name input.'''
# input(self.args.endpoint_id)
if self._is_number(self.args.endpoint_id):
self.endpoint_id = self.args.endpoint_id
self.endpoint_name = self.endpoints["by_id"][self.args.endpoint_id]
@@ -394,14 +395,13 @@ class Portainer:
else:
eps = [self.get_endpoint_id()]
#input(eps)
for endpoint in eps:
# print(s)
#print(self.args.stack)
if self.args.stack in ["all", None]:
# input([id for id in self.all_data["stacks"][endpoint]['by_id'].keys()])
for s in [id for id in self.all_data["stacks"][endpoint]['by_id'].keys()]:
for e in [id for id in self.all_data["stacks"][endpoint]['by_name'].keys()]:
#input(e)
# if s not in self.all_data["stacks"]:
# continue
#input(self.all_data)
@@ -409,36 +409,36 @@ class Portainer:
# print(f"Endpoint {self.all_data["endpoints"]["by_id"][s]} is offline")
continue
# input(self.all_data["stacks"][endpoint]["by_name"])
for e in self.all_data["stacks"][endpoint]["by_name"]:
#input(e)
path = (
f"/endpoints/{endpoint}/docker/containers/json"
f'?all=1&filters={{"label": ["com.docker.compose.project={e}"]}}'
#input(e)
path = (
f"/endpoints/{endpoint}/docker/containers/json"
f'?all=1&filters={{"label": ["com.docker.compose.project={e}"]}}'
)
logging.info(f"request : {path}")
try:
containers = self._api_get(path)
#input(containers)
except Exception as e:
print(f"failed to get containers from {path}: {e}")
continue
contr = []
try:
for c in containers:
# input(c)
cont.append(c["Names"][0].replace("/", ""))
contr.append(c["Names"][0].replace("/", ""))
if self.all_data["endpoints"]["by_id"][endpoint] in data:
data[self.all_data["endpoints"]["by_id"][endpoint]][e] = contr
else:
data[self.all_data["endpoints"]["by_id"][endpoint]] = {
e: contr
}
except Exception as e:
logger.debug(
f"Exception while getting containers for stack {e} ",
f"on endpoint {self.all_data['endpoints']['by_id'][endpoint]}: {e}",
)
logging.info(f"request : {path}")
try:
containers = self._api_get(path)
#input(containers)
except Exception as e:
print(f"failed to get containers from {path}: {e}")
continue
contr = []
try:
for c in containers:
# input(c)
cont.append(c["Names"][0].replace("/", ""))
contr.append(c["Names"][0].replace("/", ""))
if self.all_data["endpoints"]["by_id"][endpoint] in data:
data[self.all_data["endpoints"]["by_id"][endpoint]][e] = contr
else:
data[self.all_data["endpoints"]["by_id"][endpoint]] = {
e: contr
}
except Exception as e:
logger.debug(
f"Exception while getting containers for stack {e} ",
f"on endpoint {self.all_data['endpoints']['by_id'][endpoint]}: {e}",
)
self.all_data["containers"] = data
@@ -532,10 +532,10 @@ class Portainer:
if not args.autostart:
time.sleep(120)
cont = []
for c in self.all_data["containers"][endpoint]:
if stack == c or stack == "all":
cont += self.all_data["containers"][endpoint][c]
self.stop_containers(endpoint, cont)
for c in self.all_data["containers"][args.endpoint_id]:
if args.stack == c or args.stack == "all":
cont += self.all_data["containers"][args.endpoint_id][c]
self.stop_containers(args.endpoint_id, cont)
def get_endpoints(self, timeout=10):
'''Get a list of all endpoints.'''
@@ -834,7 +834,7 @@ class Portainer:
}
self._api_post_file(path, self.endpoint_id, stack, envs, file)
def print_stacks(self, endpoint="all"):
def print_stacks(self, args):
"""Print a table of stacks, optionally filtered by endpoint."""
stacks = self.get_stacks()
count = 0
@@ -842,11 +842,11 @@ class Portainer:
stack_names = []
for stack in stacks:
# print(stack)
if endpoint is not None:
if args.endpoint_id is not None:
if not stack["EndpointId"] in self.endpoints["by_id"]:
continue
if endpoint != "all":
if self.endpoints["by_name"][endpoint] != stack["EndpointId"]:
if args.endpoint_id != "all":
if self.endpoints["by_name"][args.endpoint_id] != stack["EndpointId"]:
continue
try:
stack_names.append(stack["Name"])
@@ -868,12 +868,13 @@ class Portainer:
headers = ["StackID", "Name", "Endpoint"]
print(tabulate.tabulate(data, headers=headers, tablefmt="github"))
print(f"Total stacks: {count}")
input("Continue...")
# print(sorted(stack_names))
def update_containers(self):
all_containers = self.all_data["containers"][self.args.endpoint_id]
#input(all_containers)
service_tuples = [(s[1], s[0]) for s in all_containers if "." not in s[0]]
service_tuples = [(s[1], s[0]) for s in all_containers if "." not in s[0] and not s[0].startswith("runner-")]
service_tuples = sorted(service_tuples, key=lambda x: x[1])
service_dict = dict(service_tuples)
# input(service_tuples)
@@ -913,6 +914,7 @@ class Portainer:
#print(longest)
ok = "\033[92m✔\033[0m"
err = "\033[91m✖\033[0m"
updates = []
for service_id in service_ids:
# print(self.all_data["containers"][self.args.endpoint_id])
@@ -930,10 +932,10 @@ class Portainer:
print("?")
elif resp['Status'] == "outdated":
if pull:
print("Recreate")
#print("Recreate")
self.recreate_container(service_id, pull)
#print(f"Service {service_dict[service_id]:<{longest}} : updated")
self.gotify_message(f"Service {service_dict[service_id]} updated")
updates.append(service_dict[service_id])
print(ok, end=" ")
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
if name.startswith(service_dict[service_id]):
@@ -941,7 +943,7 @@ class Portainer:
else:
print(f"\r\033[4m{service_dict[service_id]:<{longest}}\033[0m ", end="", flush=True)
#print(f"\033[4m{service_dict[service_id]:<{longest}} {err}\033[0m")
self.gotify_message(f"Service update available for {service_dict[service_id]}")
updates.append(service_dict[service_id])
print(err, end=" ")
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
if name.startswith(service_dict[service_id]):
@@ -951,6 +953,11 @@ class Portainer:
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
if name.startswith(service_dict[service_id]):
print(image)
if len(updates) > 0:
if pull:
self.gotify_message(f"Services updated: {", ".join(updates)}")
else:
self.gotify_message(f"Services updates available: {', '.join(updates)}")
print("\033[?25h", end="")
return True
@@ -1016,7 +1023,7 @@ class Portainer:
self.restart_srv(service_id, pull)
#print(f"Service {service_dict[service_id]:<{longest}} : updated")
self.gotify_message(f"Service {service_dict[service_id]} updated")
print(ok)
print(f"{ok} updated")
else:
print(f"\r\033[4m{service_dict[service_id]:<{longest}}\033[0m ", end="", flush=True)
#print(f"\033[4m{service_dict[service_id]:<{longest}} {err}\033[0m")
@@ -1094,7 +1101,7 @@ class Portainer:
def recreate_container(self,service_id, pull=False):
"""Restart a service on an endpoint."""
path = f"/docker/{self.endpoint_id}/containers/{service_id}/recreate"
print(path)
# print(path)
params={"pullImage": pull}
try:
resp = self._api_post(path, json=params, timeout=20)
@@ -1351,4 +1358,5 @@ class Portainer:
path = f"/endpoints/{endpoint_id}/docker/secrets/create"
encoded = base64.b64encode(value.encode()).decode()
data = {"Name": name, "Data": encoded}
return self._api_post(path, data, timeout=timeout)

View File

@@ -4,4 +4,5 @@ tabulate
# Other dev tools
flake8
pylint
black
black
docker