Compare commits

..

204 Commits

Author SHA1 Message Date
jaydee 8067ac8561 build 2026-03-28 12:33:16 +01:00
jaydee 79e9e708b4 build 2026-03-28 12:33:00 +01:00
ladislav.dusa 64c3615705 build 2026-03-25 14:56:28 +01:00
jaydee f5c883964a build 2026-03-24 23:19:58 +01:00
jaydee bc984f05d2 build 2026-03-24 12:26:15 +01:00
jaydee a9a4de2038 build 2026-03-24 12:16:29 +01:00
jaydee 9205e0c8f7 build 2026-03-23 16:56:46 +01:00
jaydee 062176a875 build 2026-03-22 15:28:56 +01:00
jaydee db7005b304 build 2026-03-21 22:46:02 +01:00
jaydee 0e8fcaa530 build 2026-03-21 21:53:36 +01:00
jaydee df151c4c7f build 2026-03-21 21:24:03 +01:00
jaydee eba757bf25 build 2026-03-21 21:19:48 +01:00
jaydee 1e1f82e658 build 2026-03-21 21:07:18 +01:00
jaydee 5e36820f88 build 2026-03-21 19:09:41 +01:00
jaydee 546f695a0d build 2026-03-21 11:33:26 +01:00
jaydee 4a3609ef27 build 2026-03-21 10:36:53 +01:00
jaydee 9986c1bc03 build 2026-03-20 22:11:03 +01:00
jaydee 18afddee86 build 2026-03-20 20:57:15 +01:00
jaydee c5c5a5fafd build 2026-03-19 21:56:33 +01:00
jaydee 654c244f82 build 2026-03-19 21:51:19 +01:00
jaydee c6bba8c9a5 Update .gitlab-ci.yml file 2026-03-19 10:11:14 +01:00
jaydee 06b281ccc8 build-all 2026-03-19 09:25:47 +01:00
jaydee 13563428b6 build 2026-03-17 11:08:24 +01:00
jaydee f35d6e794d build 2026-03-12 21:38:19 +01:00
jaydee b7130b835d build 2026-03-11 13:46:07 +01:00
jaydee e911eee76b Update .gitlab-ci.yml file 2026-03-11 13:45:50 +01:00
jaydee 86a18e66f7 build 2026-03-11 13:42:56 +01:00
jaydee ec27800d62 build 2026-03-11 13:41:13 +01:00
jaydee 30e66ce890 Update .gitlab-ci.yml file 2026-03-11 13:40:59 +01:00
jaydee f022223da3 build 2026-03-11 13:40:33 +01:00
jaydee 94e84d03c2 Update .gitlab-ci.yml file 2026-03-11 13:40:21 +01:00
jaydee 9915863650 Update .gitlab-ci.yml file 2026-03-11 13:38:21 +01:00
jaydee 88c41f2bcf build 2026-03-11 13:35:27 +01:00
jaydee 9ac830a505 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2026-03-11 13:35:20 +01:00
jaydee e85fe9c98e build 2026-03-11 13:35:16 +01:00
jaydee ce2460b33a Update .gitlab-ci.yml file 2026-03-11 13:35:09 +01:00
jaydee 9007376ad3 build 2026-03-11 13:30:41 +01:00
jaydee cc91ef474c build 2026-03-11 13:29:55 +01:00
jaydee e3e05a6430 Update .gitlab-ci.yml file 2026-03-11 13:29:47 +01:00
jaydee 4a422730b1 build 2026-03-11 13:24:02 +01:00
jaydee 233d5c4ff8 Update .gitlab-ci.yml file 2026-03-11 13:23:53 +01:00
jaydee 6907785036 build 2026-03-11 13:22:33 +01:00
jaydee 8c911c27a9 Update .gitlab-ci.yml file 2026-03-11 13:21:58 +01:00
jaydee bd138dd5aa build-all 2026-03-02 10:03:40 +01:00
jaydee f7ebd259e1 build 2026-03-02 01:33:28 +01:00
jaydee faa4c90e98 build 2026-03-02 01:29:04 +01:00
jaydee 2421b93730 Update .gitlab-ci.yml file 2026-03-02 01:28:25 +01:00
jaydee 83d4514356 build 2026-03-02 01:25:55 +01:00
jaydee 2cc8b6171d build 2026-03-01 02:32:50 +01:00
jaydee 51081cf879 build 2026-03-01 02:30:46 +01:00
jaydee 9d3fd8ef3e build-all 2026-02-28 02:48:05 +01:00
jaydee 40b1d5d967 build-all 2026-02-28 02:13:48 +01:00
jaydee 12036631be build-all 2026-02-27 10:14:02 +01:00
jaydee cdbd508f95 build-all 2026-02-27 09:48:31 +01:00
jaydee d1302c58a7 build-all 2026-02-26 15:45:50 +01:00
jaydee 42ebef5f45 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2026-02-26 15:45:44 +01:00
jaydee 1a2f57a81f build-all 2026-02-26 15:45:40 +01:00
jaydee eee10f8338 Update .gitlab-ci.yml file 2026-02-26 15:21:21 +01:00
jaydee 6d30afc469 Update .gitlab-ci.yml file 2026-02-26 15:20:37 +01:00
jaydee 02db7ce43b build 2026-02-26 11:53:53 +01:00
jaydee 5cbeb664f3 Update .gitlab-ci.yml file 2026-02-26 11:53:39 +01:00
jaydee 2cf4b2b67d build 2026-02-26 11:21:31 +01:00
jaydee 6aecddef8a Update .gitlab-ci.yml file 2026-02-26 11:21:18 +01:00
jaydee 2283143aca build 2026-02-26 11:19:21 +01:00
jaydee 37a6b1548b Update .gitlab-ci.yml file 2026-02-26 11:18:36 +01:00
jaydee 3add3a4362 build 2026-02-25 21:44:28 +01:00
jaydee 8bba5974fd build 2026-02-25 20:58:47 +01:00
jaydee e7851cf976 build 2026-02-22 23:18:44 +01:00
jaydee 27feb31606 Update .gitlab-ci.yml file 2026-02-13 19:09:15 +01:00
jaydee ce71433620 Update .gitlab-ci.yml file 2026-02-13 19:09:00 +01:00
jaydee d98024a696 build 2026-02-13 19:07:53 +01:00
jaydee 19dae69fab build 2026-02-13 19:05:56 +01:00
jaydee 67d9bef57d build 2026-02-13 19:04:01 +01:00
jaydee 39a0175111 build 2026-02-13 19:01:47 +01:00
jaydee 29ca908c75 build 2026-02-10 21:47:40 +01:00
jaydee a16188a110 build 2026-02-10 21:44:52 +01:00
jaydee 90f9b19f5e build 2026-02-10 21:34:03 +01:00
jaydee 48708436df build 2026-02-10 21:33:11 +01:00
jaydee 0c2be26cf0 build 2026-02-10 21:29:42 +01:00
jaydee 8de33d2d92 Update .gitlab-ci.yml file 2026-02-10 21:29:23 +01:00
jaydee 475e49d4e9 build 2026-02-10 21:21:43 +01:00
jaydee f770cbc926 build 2026-02-10 21:17:52 +01:00
jaydee 8ba22f79b9 build 2026-01-24 21:07:26 +01:00
jaydee ddeb67750f build 2026-01-24 21:05:09 +01:00
jaydee 12ff88f8e8 build 2026-01-24 21:03:32 +01:00
jaydee 9d27e804a5 build 2026-01-24 21:02:34 +01:00
jaydee 1f0a19b7b1 build 2026-01-24 21:00:19 +01:00
jaydee 5adfbbcf3d build 2026-01-24 20:59:37 +01:00
jaydee 0dda82be87 build 2026-01-24 20:57:21 +01:00
jaydee 1a54c1e341 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2026-01-24 20:55:55 +01:00
jaydee 8d4bd382ee build 2026-01-24 20:55:51 +01:00
ladislav.dusa 96068d4fb3 Merge branch 'main' of https://gitlab.sectorq.eu/jaydee/portainer 2026-01-13 13:22:24 +01:00
ladislav.dusa de37276ab6 build 2026-01-13 13:22:20 +01:00
jaydee 2dc800f7f9 build 2026-01-12 23:30:30 +01:00
jaydee ae387a794c build 2026-01-09 17:34:31 +01:00
jaydee a3518ec0bb build 2026-01-09 14:07:31 +01:00
ladislav.dusa 3e86a75502 build 2026-01-08 09:06:23 +01:00
ladislav.dusa 11cd76215a build 2026-01-08 08:52:35 +01:00
jaydee 4bbe283211 build 2026-01-06 22:44:38 +01:00
jaydee fc3fe7b837 build 2026-01-06 22:02:06 +01:00
jaydee 3152014ca3 build 2026-01-05 17:45:29 +01:00
jaydee e411c81224 build 2026-01-05 17:43:44 +01:00
jaydee 8ae696a96a build 2026-01-05 17:35:54 +01:00
jaydee abd989a0db build 2026-01-05 14:55:37 +01:00
jaydee bb8ef3bdb8 build 2025-12-30 22:08:02 +01:00
jaydee 99aa451620 build 2025-12-30 21:51:26 +01:00
jaydee fd1fcf90a4 build 2025-12-27 19:17:53 +01:00
jaydee 135447d7aa build 2025-12-27 19:04:45 +01:00
jaydee 164252534e build 2025-12-27 18:48:26 +01:00
jaydee 807437c47e build 2025-12-23 22:33:08 +01:00
jaydee 08a15f3bb9 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 22:32:41 +01:00
jaydee 1f4319f4dd build 2025-12-23 22:32:36 +01:00
jaydee 9800b01ea2 Update .gitlab-ci.yml file 2025-12-23 22:32:18 +01:00
jaydee 4cd9cfce20 build 2025-12-23 22:30:57 +01:00
jaydee 12a095e169 build 2025-12-23 22:23:00 +01:00
jaydee 162c270c02 build 2025-12-23 22:19:02 +01:00
jaydee 0c4a91d7ae build 2025-12-23 22:16:52 +01:00
jaydee 36cb83694c Update .gitlab-ci.yml file 2025-12-23 22:16:34 +01:00
jaydee f97cd105ba build 2025-12-23 22:14:56 +01:00
jaydee 954d5b2dd7 Update .gitlab-ci.yml file 2025-12-23 22:14:35 +01:00
jaydee 73a68a0f1b build 2025-12-23 22:12:50 +01:00
jaydee 4598caca89 Update .gitlab-ci.yml file 2025-12-23 22:12:23 +01:00
jaydee 4d22e77689 build 2025-12-23 22:10:23 +01:00
jaydee c2a1a7d115 build 2025-12-23 22:08:48 +01:00
jaydee 9a79910428 Update .gitlab-ci.yml file 2025-12-23 22:08:27 +01:00
jaydee bc69ff6223 build 2025-12-23 22:06:24 +01:00
jaydee 4e610eea32 build 2025-12-23 22:00:09 +01:00
jaydee 4e8c0ab3a0 build 2025-12-23 15:08:18 +01:00
jaydee b057dfcce4 build 2025-12-23 15:06:21 +01:00
jaydee ab15e7c8ea Update .gitlab-ci.yml file 2025-12-23 15:05:56 +01:00
jaydee 74269b0368 build 2025-12-23 15:01:24 +01:00
jaydee 45c97d1791 Update .gitlab-ci.yml file 2025-12-23 14:59:37 +01:00
jaydee c341c2332f build 2025-12-23 14:58:01 +01:00
jaydee d0f2cfc75f Update .gitlab-ci.yml file 2025-12-23 14:57:46 +01:00
jaydee c22287f53b build 2025-12-23 13:44:28 +01:00
jaydee f618476534 Update .gitlab-ci.yml file 2025-12-23 13:43:58 +01:00
jaydee 5865df9abc Update .gitlab-ci.yml file 2025-12-23 13:43:42 +01:00
jaydee f83ee560c1 Update .gitlab-ci.yml file 2025-12-23 13:32:45 +01:00
jaydee 5ce8573013 build 2025-12-23 13:19:41 +01:00
jaydee e8191802b1 Update .gitlab-ci.yml file 2025-12-23 13:19:07 +01:00
jaydee e546d0cf3f build 2025-12-23 13:16:48 +01:00
jaydee daf219329a Update .gitlab-ci.yml file 2025-12-23 13:15:47 +01:00
jaydee b601ecc0c3 build 2025-12-23 13:13:44 +01:00
jaydee 1a8e532a02 build 2025-12-23 12:52:18 +01:00
jaydee d6e4db6dd4 Update .gitlab-ci.yml file 2025-12-23 12:52:02 +01:00
jaydee 78012cec65 build 2025-12-23 12:46:55 +01:00
jaydee ba098499f5 Update .gitlab-ci.yml file 2025-12-23 12:45:58 +01:00
jaydee ce24b5c00d build 2025-12-23 12:45:09 +01:00
jaydee 46143a7c12 build 2025-12-23 12:39:42 +01:00
jaydee c731fbe0de Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 12:39:36 +01:00
jaydee d878a2baa0 build 2025-12-23 12:39:32 +01:00
jaydee e280ea67f7 Update .gitlab-ci.yml file 2025-12-23 12:39:03 +01:00
jaydee 260eb63262 build 2025-12-23 12:37:57 +01:00
jaydee 41d6ec9914 Update .gitlab-ci.yml file 2025-12-23 12:37:44 +01:00
jaydee 15e442d49b build 2025-12-23 12:35:33 +01:00
jaydee 14c31575af build 2025-12-23 12:30:03 +01:00
jaydee 406513b4b8 Update .gitlab-ci.yml file 2025-12-23 12:29:03 +01:00
jaydee 0c1b624972 Update .gitlab-ci.yml file 2025-12-23 12:25:05 +01:00
jaydee db5209e3fb build 2025-12-23 12:17:24 +01:00
jaydee 111c70ef00 Update .gitlab-ci.yml file 2025-12-23 12:17:07 +01:00
jaydee 8bba4d1d18 build 2025-12-23 12:10:06 +01:00
jaydee db45a48106 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 12:10:00 +01:00
jaydee 53438a3fb0 build 2025-12-23 12:09:56 +01:00
jaydee 5646e0692d Update .gitlab-ci.yml file 2025-12-23 12:08:17 +01:00
jaydee 6634cc20fa build 2025-12-23 12:05:41 +01:00
jaydee fc9f25a203 Update .gitlab-ci.yml file 2025-12-23 12:05:25 +01:00
jaydee 8351e9f1b1 Update .gitlab-ci.yml file 2025-12-23 12:05:13 +01:00
jaydee d007510704 build 2025-12-23 12:03:42 +01:00
jaydee 3e202c9fd8 build 2025-12-23 11:55:22 +01:00
jaydee d6e5c4087d build 2025-12-23 11:53:06 +01:00
jaydee f70ebecc49 build 2025-12-23 11:41:50 +01:00
jaydee b8264994c5 Update .gitlab-ci.yml file 2025-12-23 11:41:37 +01:00
jaydee 6081c44d4c build 2025-12-23 11:40:49 +01:00
jaydee abb5fc7708 Update .gitlab-ci.yml file 2025-12-23 11:40:31 +01:00
jaydee a9eecac96d build 2025-12-23 11:35:19 +01:00
jaydee e7bcee762d build 2025-12-23 11:33:51 +01:00
jaydee e3fed3304a Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 11:33:46 +01:00
jaydee 338aa66565 build 2025-12-23 11:33:42 +01:00
jaydee 058554a0ea Update .gitlab-ci.yml file 2025-12-23 11:33:19 +01:00
jaydee b28a7c8273 build 2025-12-23 11:27:57 +01:00
jaydee e464c498ff build 2025-12-23 11:22:46 +01:00
jaydee b028a48fc0 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 11:22:40 +01:00
jaydee b2373f7016 build 2025-12-23 11:22:36 +01:00
jaydee 1b50b3337c Update .gitlab-ci.yml file 2025-12-23 11:22:09 +01:00
jaydee ac68b5be6f build 2025-12-23 11:20:43 +01:00
jaydee 035abffeab Update .gitlab-ci.yml file 2025-12-23 11:20:29 +01:00
jaydee 6097d2b442 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 11:19:44 +01:00
jaydee 55bdff1745 build 2025-12-23 11:19:30 +01:00
jaydee b385f3db12 Update .gitlab-ci.yml file 2025-12-23 11:18:49 +01:00
jaydee 94a3ccfd23 Update .gitlab-ci.yml file 2025-12-23 11:18:03 +01:00
jaydee b0c570d7ba build 2025-12-23 11:16:56 +01:00
jaydee 4339a7d769 build 2025-12-23 10:59:28 +01:00
jaydee 039078191f build 2025-12-23 10:56:22 +01:00
jaydee f5d76d87e0 build 2025-12-23 09:55:31 +01:00
jaydee f561508d2e build 2025-12-21 00:52:21 +01:00
jaydee 974966fdd8 build 2025-12-21 00:49:24 +01:00
jaydee 63e158899e build 2025-12-20 15:22:59 +01:00
jaydee 9336b56f96 build 2025-12-20 15:22:40 +01:00
jaydee 66fba7b994 build 2025-12-20 15:20:29 +01:00
jaydee 7804dbb117 build 2025-12-20 15:19:00 +01:00
jaydee fb1763e14d build 2025-12-20 15:16:40 +01:00
jaydee 829891d1ba build 2025-12-20 15:13:14 +01:00
jaydee 174aab4faa build 2025-12-19 17:23:20 +01:00
jaydee 9c6445ee03 build 2025-12-19 17:08:15 +01:00
15 changed files with 433 additions and 169 deletions
+52 -11
View File
@@ -1,13 +1,30 @@
stages: # List of stages for jobs, and their order of execution stages: # List of stages for jobs, and their order of execution
- notify1
- lint - lint
- test
- security
- dast
- build - build
- clean - clean
- notify - notify
variables: variables:
GIT_SSH_COMMAND: "ssh -i /home/gitlab-runner/.ssh/id_rsa -o IdentitiesOnly=yes" GIT_SSH_COMMAND: "ssh -i /home/gitlab-runner/.ssh/id_rsa -o IdentitiesOnly=yes"
CS_IMAGE: r.sectorq.eu/jaydee/builder:amd64
notify1:
stage: notify1 # Should be in a later stage than the job that might fail
when: on_success # <-- This is the key keyword
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: lint:
stage: lint stage: lint
image: r.sectorq.eu/jaydee/builder-portainer:latest image: r.sectorq.eu/jaydee/builder:latest
before_script: before_script:
- python3 -m pip install --break-system-packages flake8 black pylint tabulate prompt_toolkit hvac - python3 -m pip install --break-system-packages flake8 black pylint tabulate prompt_toolkit hvac
- export PATH="$PATH:/home/gitlab-runner/.local/bin" - export PATH="$PATH:/home/gitlab-runner/.local/bin"
@@ -19,17 +36,23 @@ lint:
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
rules: rules:
- if: '$CI_COMMIT_MESSAGE =~ /lint/' - if: '$CI_COMMIT_MESSAGE =~ /lint/'
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
- template: Security/DAST.gitlab-ci.yml
#- template: Security/Container-Scanning.gitlab-ci.yml
build-job: # This job runs in the build stage, which runs first. build-job: # This job runs in the build stage, which runs first.
stage: build stage: build
image: r.sectorq.eu/jaydee/builder-portainer:latest image: r.sectorq.eu/jaydee/builder:amd64
script: script:
- mkdir -p ~/.ssh - pyinstaller --onefile --clean -n portainer main.py
- 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 #- 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.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 - curl -F "file=@dist/portainer" https://myapps.sectorq.eu/
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
artifacts: artifacts:
paths: paths:
@@ -40,19 +63,37 @@ build-job: # This job runs in the build stage, which runs first.
# - curl -X POST https://kestra.sectorq.eu/api/v1/executions/webhook/jaydee/ansible-all/${flow_id} -d '{"tag":["proxmox"],"target":["servers"]}' -H "Content-Type${column} application/json" # - curl -X POST https://kestra.sectorq.eu/api/v1/executions/webhook/jaydee/ansible-all/${flow_id} -d '{"tag":["proxmox"],"target":["servers"]}' -H "Content-Type${column} application/json"
rules: rules:
- if: '$CI_COMMIT_MESSAGE =~ /build/' - if: '$CI_COMMIT_MESSAGE =~ /build/'
build-job-arm: # This job runs in the build stage, which runs first.
stage: build
image: r.sectorq.eu/jaydee/builder:arm64
script:
- pyinstaller --onefile --clean -n portainer_arm main.py
#- 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_arm jd@192.168.77.12:/myapps/bin/ || true
- curl -F "file=@dist/portainer_arm" https://myapps.sectorq.eu/
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
artifacts:
paths:
- dist/
expire_in: 1 week
# - column=":"
# - echo "${flow_id}"
# - curl -X POST https://kestra.sectorq.eu/api/v1/executions/webhook/jaydee/ansible-all/${flow_id} -d '{"tag":["proxmox"],"target":["servers"]}' -H "Content-Type${column} application/json"
rules:
- if: '$CI_COMMIT_MESSAGE =~ /build-all/'
clean-job: # This job runs in the build stage, which runs first. clean-job: # This job runs in the build stage, which runs first.
stage: clean stage: clean
script: script:
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
rules: rules:
- if: '$CI_COMMIT_MESSAGE =~ /build/' - if: '$CI_COMMIT_MESSAGE =~ /build/'
cleanup_on_failure_job: cleanup_on_failure_job:
stage: clean # Should be in a later stage than the job that might fail stage: clean # Should be in a later stage than the job that might fail
when: on_failure # <-- This is the key keyword when: on_failure # <-- This is the key keyword
script: script:
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
notify: notify:
stage: notify # Should be in a later stage than the job that might fail stage: notify # Should be in a later stage than the job that might fail
when: on_success # <-- This is the key keyword when: on_success # <-- This is the key keyword
@@ -61,7 +102,8 @@ notify:
- echo "${flow_id}" - echo "${flow_id}"
- curl -XPOST http://192.168.77.101:8123/api/webhook/voice-notifications-tC_8YKxMJIAaQRV5riKuC7Zl --data-raw 'message=portainer build job completed' - 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 - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
rules:
- if: '$CI_COMMIT_MESSAGE =~ /build/'
notify2: notify2:
stage: notify # Should be in a later stage than the job that might fail stage: notify # Should be in a later stage than the job that might fail
when: on_failure # <-- This is the key keyword when: on_failure # <-- This is the key keyword
@@ -72,4 +114,3 @@ notify2:
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
rules: rules:
- if: '$CI_COMMIT_MESSAGE =~ /build/' - if: '$CI_COMMIT_MESSAGE =~ /build/'
+2
View File
@@ -0,0 +1,2 @@
[semgrep]
exclude = ["bandit.B101", "java_deserialization_rule-JacksonUnsafeDeserialization"]
+3
View File
@@ -0,0 +1,3 @@
{
"python.defaultInterpreterPath": "../../venvs/portainer/bin/python"
}
Binary file not shown.
+168 -59
View File
@@ -5,24 +5,32 @@ This module provides a wrapper for interacting with the Portainer API
to manage endpoints, stacks, and containers. to manage endpoints, stacks, and containers.
""" """
# !/myapps/venvs/portainer/bin/python3
import os import os
import logging import logging
import signal import signal
import sys import sys
import json import json
import argparse import argparse
import tty
import termios
import hvac import hvac
import time
import base64
import shutil
import requests
import tempfile
import subprocess
from portainer.api import PortainerApi
from git import Repo
from concurrent.futures import ThreadPoolExecutor
from tabulate import tabulate from tabulate import tabulate
from port import Portainer
from prompt_toolkit import prompt from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.shortcuts import checkboxlist_dialog from prompt_toolkit.shortcuts import checkboxlist_dialog
from prompt_toolkit.shortcuts import radiolist_dialog from prompt_toolkit.shortcuts import radiolist_dialog
VAULT_ADDR = os.environ.get("VAULT_ADDR", "http://192.168.77.101:8200")
def setup_vault():
# VAULT_ADDR = os.environ.get("VAULT_ADDR", "http://192.168.77.101:8200")
VAULT_ADDR = os.environ.get("VAULT_ADDR", "https://vault.sectorq.eu")
try: try:
VAULT_TOKEN = os.environ.get("VAULT_TOKEN") VAULT_TOKEN = os.environ.get("VAULT_TOKEN")
if VAULT_TOKEN is None: if VAULT_TOKEN is None:
@@ -31,15 +39,17 @@ except KeyError:
VAULT_TOKEN = prompt("Valult root token : ", is_password=True) VAULT_TOKEN = prompt("Valult root token : ", is_password=True)
os.environ["VAULT_TOKEN"] = VAULT_TOKEN os.environ["VAULT_TOKEN"] = VAULT_TOKEN
client = hvac.Client(url=VAULT_ADDR, token=VAULT_TOKEN) vclient = hvac.Client(url=VAULT_ADDR, token=VAULT_TOKEN)
# Check if connected # Check if connected
if client.is_authenticated(): if vclient.is_authenticated():
print("Connected to Vault") print("Connected to Vault")
else: else:
raise Exception("Failed to authenticate with Vault") raise Exception("Failed to authenticate with Vault")
# Specify the mount point of your KV engine # Specify the mount point of your KV engine
return vclient
VERSION = "0.1.77"
VERSION = "0.1.16"
defaults = { defaults = {
"endpoint_id": "vm01", "endpoint_id": "vm01",
@@ -83,10 +93,6 @@ def load_config(defaults=defaults):
print("Configuration written to /myapps/portainer.conf") print("Configuration written to /myapps/portainer.conf")
return cur_config return cur_config
a = load_config(defaults) a = load_config(defaults)
# ENV_VARS = [ # ENV_VARS = [
@@ -109,7 +115,11 @@ def update_configs(cur_config):
print("Configuration written to /myapps/portainer.conf") print("Configuration written to /myapps/portainer.conf")
parser = argparse.ArgumentParser( 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( parser.add_argument(
"--base", "--base",
@@ -132,21 +142,31 @@ parser.add_argument(
default=None, default=None,
help="Service ID to limit service operations", help="Service ID to limit service operations",
) )
parser.add_argument("--stack", "-s", type=str, default=None, nargs="+", help="Stack ID for operations") parser.add_argument("--stack", "-s", type=str, default=None, help="Stack ID for operations")
parser.add_argument("--action", "-a", type=str, default=None, help="Action to perform") parser.add_argument("--action", "-a", type=str, default=None, help="Action to perform")
parser.add_argument( parser.add_argument(
"--autostart", "-Z", action="store_true", help="Auto-start created stacks" "--autostart", "-Z", action="store_true", help="Auto-start created stacks"
) )
parser.add_argument("--update", "-u", action="store_true", help="Update service if it exists") 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("--debug", "-D", action="store_true")
parser.add_argument("--launcher", "-L", action="store_true")
parser.add_argument("--gpu", "-g", 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("--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("--deploy-mode", "-m", type=str, default="git", help="Deploy mode")
parser.add_argument("--stack-mode", "-w", default=None, help="Stack mode") parser.add_argument("--stack-mode", "-w", default=None, help="Stack mode")
parser.add_argument("--print-command", "-P", action="store_true", help="Print quick command from action")
parser.add_argument(
"-E", "--excluded",
nargs="+",
default=["portainer", "nginx"],
help="Exclude from update"
)
args = parser.parse_args() args = parser.parse_args()
print("Running version:", VERSION) print("Running version:", VERSION)
print("Environment:", args.site) print("Environment:", args.site)
args.client = client
args.client = setup_vault()
if args.site is not None: if args.site is not None:
cur_config["PORTAINER_SITE"] = args.site cur_config["PORTAINER_SITE"] = args.site
if args.endpoint_id is not None: if args.endpoint_id is not None:
@@ -206,6 +226,41 @@ def wl(msg):
if args.debug: if args.debug:
print(msg) print(msg)
def run(cmd, cwd=None):
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(result.stderr)
return result.stdout.strip()
def get_compose_files():
#git clone --depth=1 --filter=blob:none --no-checkout https://github.com/user/repo.git
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = os.path.join(tmpdir, "repo")
# Clone with minimal data (no checkout, no blobs)
run([
"git", "clone",
"--depth=1",
"--filter=blob:none",
"--no-checkout",
"git@gitlab.sectorq.eu:/home/docker-compose.git",
repo_path
])
# List files in HEAD
output = run([
"git", "ls-tree",
"-r",
"HEAD",
"--name-only"
], cwd=repo_path)
folders = []
for line in output.splitlines():
if "/" in line and line.split("/")[0] != "__swarm":
folders.append(line.split("/")[0])
return list(dict.fromkeys(folders))
def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None): def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
""" """
@@ -241,19 +296,23 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
elif field == "stack": elif field == "stack":
if args.action == "create_stack": if args.action == "create_stack":
# input(json.dumps(stacks, indent=2)) # input(json.dumps(stacks, indent=2))
commands = [ commands = get_compose_files()
'authentik', 'bitwarden', 'bookstack', 'dockermon', 'fail2ban', 'gitea', 'gitlab', 'grafana', # commands = [
'hashicorp', 'home-assistant', 'homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'mailu3', # 'api_server', 'authentik', 'bitwarden', 'bookstack', 'databasus', 'dockermon', 'duplicati', 'fail2ban', 'filebrowser', 'gitea', 'gitlab', 'grafana', 'grocy',
'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nebula', 'nextcloud', 'nginx', # 'hashicorp', 'home-assistant', 'homebox','homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'kopia', 'linkding', 'linkwarden', 'mailu3',
'node-red', 'octoprint', 'ollama', 'onlyoffice', 'paperless-ngx', 'pihole', 'portainer-ce', 'rancher', 'registry', # 'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nebula', 'nextcloud', 'nginx',
'regsync', 'semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wordpress', # 'node-red', 'octoprint', 'ollama', 'onlyoffice', 'paperless-ngx', 'pihole', 'portainer-ce','portainerce', 'puppet', 'puppet-agent', 'rancher', 'registry',
'wud', 'zabbix-server'] # 'regsync', 'repo_mirror', 'searxng','semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wordpress',
# 'wud', 'zabbix-server']
try: try:
print(por.all_data['stacks'][defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]]['by_name'].keys()) print(por.all_data['stacks'][defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]]['by_name'].keys())
for s in por.all_data['stacks'][defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]]['by_name'].keys(): for s in por.all_data['stacks'][defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]]['by_name'].keys():
print(s)
#print(s) try:
commands.remove(s) commands.remove(s)
except ValueError as e:
print(e)
except KeyError: except KeyError:
print("No stacks found for endpoint", defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]) print("No stacks found for endpoint", defaults_in[f"PORTAINER_ENDPOINT_ID".upper()])
@@ -283,6 +342,7 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
commands.sort() commands.sort()
commands_tuples = [(cmd, cmd) for cmd in commands] commands_tuples = [(cmd, cmd) for cmd in commands]
commands_tuples.insert(0, ("__ALL__", "[Select ALL]")) commands_tuples.insert(0, ("__ALL__", "[Select ALL]"))
commands_tuples.insert(0, ("mandatory", "[Mandatory]"))
value_in = checkboxlist_dialog( value_in = checkboxlist_dialog(
title="Select Services", title="Select Services",
text="Choose one or more services:", text="Choose one or more services:",
@@ -295,12 +355,14 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
elif "__ALL__" in value_in: elif "__ALL__" in value_in:
# User selected "Select ALL" # User selected "Select ALL"
value_in = commands # all real commands value_in = commands # all real commands
elif "mandatory" in value_in:
# User selected "Select ALL"
value_in = ['pihole', 'nginx', 'authentik', 'hashicorp', 'mosquitto','homepage', 'mailu3', 'home-assistant', 'mediacenter' ] # all real commands
value_in.sort() value_in.sort()
if "pihole" in value_in: if "pihole" in value_in:
if action == "delete_stack": if args.action in ["delete_stack","stop_stack"]:
value_in.remove("pihole") value_in.remove("pihole")
value_in.append("pihole") value_in.append("pihole")
else: else:
@@ -372,7 +434,7 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
if __name__ == "__main__": if __name__ == "__main__":
# Example usage: set PORTAINER_USER and PORTAINER_PASS in env, or pass literals below. # 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): def signal_handler(sig, frame):
logger.warning("Killed manually %s, %s", sig, frame) logger.warning("Killed manually %s, %s", sig, frame)
print("\nTerminated by user") print("\nTerminated by user")
@@ -390,6 +452,8 @@ if __name__ == "__main__":
("update_service","update_service"), ("update_service","update_service"),
("update_containers","update_containers"), ("update_containers","update_containers"),
("list_stacks","list_stacks"), ("list_stacks","list_stacks"),
("list_all_stacks", "list_all_stacks"),
("delete_ophaned_stacks", "delete_ophaned_stacks"),
("update_stack","update_stack"), ("update_stack","update_stack"),
("secrets","secrets"), ("secrets","secrets"),
("print_all_data","print_all_data"), ("print_all_data","print_all_data"),
@@ -403,18 +467,13 @@ if __name__ == "__main__":
] ]
selected_action = radiolist_dialog( selected_action = radiolist_dialog(
title="Select one service", title=f"Select one service - version: {VERSION}",
text="Choose a service:", text="Choose a service:",
values=actions values=actions
).run() ).run()
print("Selected:", selected_action) print("Selected:", selected_action)
# print("Possible actions: \n") # print("Possible actions: \n")
# i = 1 # i = 1
# for a in actions: # for a in actions:
@@ -426,7 +485,7 @@ if __name__ == "__main__":
os.system("cls" if os.name == "nt" else "clear") os.system("cls" if os.name == "nt" else "clear")
# Example: list endpoints # Example: list endpoints
por = Portainer(cur_config["PORTAINER_SITE"], args) por = PortainerApi(cur_config["PORTAINER_SITE"], args)
por.set_defaults(cur_config) por.set_defaults(cur_config)
if args.debug: if args.debug:
por._debug = True por._debug = True
@@ -450,7 +509,6 @@ if __name__ == "__main__":
for key, value in secrets.items(): for key, value in secrets.items():
res = por.create_secret(key, value, args.endpoint_id, args.timeout) res = por.create_secret(key, value, args.endpoint_id, args.timeout)
print(res) print(res)
sys.exit()
if args.action == "delete_stack": if args.action == "delete_stack":
args = prompt_missing_args( args = prompt_missing_args(
@@ -471,7 +529,6 @@ if __name__ == "__main__":
args.endpoint_id, args.endpoint_id,
args.stack, args.stack,
) )
sys.exit()
if args.action == "create_stack": if args.action == "create_stack":
por.action = "create_stack" por.action = "create_stack"
@@ -496,7 +553,7 @@ if __name__ == "__main__":
args.autostart, args.autostart,
args.stack_mode, args.stack_mode,
) )
sys.exit()
if args.action == "stop_stack": if args.action == "stop_stack":
args = prompt_missing_args( args = prompt_missing_args(
@@ -510,7 +567,6 @@ if __name__ == "__main__":
) )
por.stop_stack(args.stack, args.endpoint_id) por.stop_stack(args.stack, args.endpoint_id)
sys.exit()
if args.action == "start_stack": if args.action == "start_stack":
args = prompt_missing_args( args = prompt_missing_args(
@@ -523,7 +579,7 @@ if __name__ == "__main__":
], ],
) )
por.start_stack(args.stack, args.endpoint_id) por.start_stack(args.stack, args.endpoint_id)
sys.exit()
if args.action == "restart_service": if args.action == "restart_service":
args = prompt_missing_args( args = prompt_missing_args(
@@ -535,7 +591,7 @@ if __name__ == "__main__":
], ],
) )
por.restart_service(args.endpoint_id, "lala") por.restart_service(args.endpoint_id, "lala")
sys.exit()
if args.action == "update_service": if args.action == "update_service":
args = prompt_missing_args( args = prompt_missing_args(
@@ -547,7 +603,9 @@ if __name__ == "__main__":
], ],
) )
por.update_service() por.update_service()
sys.exit() if args.launcher:
input("\nPress ENTER to continue...")
if args.action == "update_containers": if args.action == "update_containers":
@@ -562,7 +620,6 @@ if __name__ == "__main__":
], ],
) )
por.update_containers() por.update_containers()
sys.exit()
if args.action == "list_stacks": if args.action == "list_stacks":
args = prompt_missing_args( args = prompt_missing_args(
@@ -574,13 +631,37 @@ if __name__ == "__main__":
], ],
) )
por.print_stacks(args) por.print_stacks(args)
if args.launcher:
input("Press ENTER to continue...")
# print(json.dumps(por.all_data, indent=2)) # print(json.dumps(por.all_data, indent=2))
sys.exit()
if args.action == "list_all_stacks":
por.get_stacks_failed()
if args.launcher:
input("Press ENTER to continue...")
# print(json.dumps(por.all_data, indent=2))
if args.action == "delete_ophaned_stacks":
por.delete_failed_stack()
if args.launcher:
input("Press ENTER to continue...")
# print(json.dumps(por.all_data, indent=2))
if args.action == "list_containers": if args.action == "list_containers":
print("Getting containers") print("Getting containers")
print(por.get_containers()) args = prompt_missing_args(
sys.exit() args,
cur_config,
[
("site", "Site"),
("endpoint_id", "Endpoint ID"),
],
)
print("\n".join(por.get_containers()))
if args.launcher:
input("\nPress ENTER to continue...")
if args.action == "update_stack": if args.action == "update_stack":
args = prompt_missing_args( args = prompt_missing_args(
@@ -593,14 +674,18 @@ if __name__ == "__main__":
) )
por.update_stack(args) por.update_stack(args)
sys.exit() if args.launcher:
input("\nPress ENTER to continue...")
if args.action == "print_all_data": if args.action == "print_all_data":
print(json.dumps(por.all_data, indent=2)) print(json.dumps(por.all_data, indent=2))
sys.exit() if args.launcher:
input("\nPress ENTER to continue...")
if args.action == "update_status": if args.action == "update_status":
por.update_status(args.endpoint_id, args.stack) por.update_status(args.endpoint_id, args.stack)
sys.exit()
if args.action == "list_endpoints": if args.action == "list_endpoints":
eps = por.get_endpoints(args) eps = por.get_endpoints(args)
@@ -609,20 +694,30 @@ if __name__ == "__main__":
export_data.append([i, eps["by_id"][i]]) export_data.append([i, eps["by_id"][i]])
headers = ["EndpointId", "Name"] headers = ["EndpointId", "Name"]
print(tabulate(export_data, headers=headers, tablefmt="github")) print(tabulate(export_data, headers=headers, tablefmt="github"))
if args.launcher:
input("\nPress ENTER to continue...")
sys.exit()
if args.action == "stop_containers": 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: if por.all_data["endpoints_status"][args.endpoint_id] != 1:
print(f"Endpoint {por.get_endpoint_name(args.endpoint_id)} is offline") print(f"Endpoint {por.get_endpoint_name(args.endpoint_id)} is offline")
sys.exit()
print(f"Stopping containers on {por.get_endpoint_name(args.endpoint_id)}") print(f"Stopping containers on {por.get_endpoint_name(args.endpoint_id)}")
cont = [] cont = []
for c in por.all_data["containers"][args.endpoint_id]: for c in por.all_data["containers"][args.endpoint_id]:
if args.stack in (c, "all"): if args.stack in (c, "all"):
cont += por.all_data["containers"][args.endpoint_id][c] cont += por.all_data["containers"][args.endpoint_id][c]
por.stop_containers(args.endpoint_id, cont) por.stop_containers(args.endpoint_id, cont)
sys.exit()
if args.action == "start_containers": if args.action == "start_containers":
print("Starting containers") print("Starting containers")
@@ -632,7 +727,7 @@ if __name__ == "__main__":
if args.stack in (c, "all"): if args.stack in (c, "all"):
cont += por.all_data["containers"][args.endpoint_id][c] cont += por.all_data["containers"][args.endpoint_id][c]
por.start_containers(args.endpoint_id, cont) por.start_containers(args.endpoint_id, cont)
sys.exit()
if args.action == "start_containers": if args.action == "start_containers":
print("Starting containers") print("Starting containers")
cont = [] cont = []
@@ -641,14 +736,28 @@ if __name__ == "__main__":
if args.stack in (c, "all"): if args.stack in (c, "all"):
cont += por.all_data["containers"][args.endpoint_id][c] cont += por.all_data["containers"][args.endpoint_id][c]
por.start_containers(args.endpoint_id, cont) por.start_containers(args.endpoint_id, cont)
sys.exit()
if args.action == "refresh_environment": if args.action == "refresh_environment":
cont = por.refresh() cont = por.refresh()
sys.exit()
if args.action == "refresh_status": if args.action == "refresh_status":
if args.stack == "all": por.refresh_status(args)
print("Stopping all stacks...")
stcks = por.get_stacks(endpoint_id=args.endpoint_id) if args.print_command:
else: one_time_command = "portainer"
por.refresh_status(args.stack_id) if args.action:
one_time_command += f" --action={args.action}"
if por.endpoint_name:
one_time_command += f" --endpoint-id={por.endpoint_name}"
if por.site:
one_time_command += f" --site={por.site}"
if args.stack:
if type(args.stack) == list:
args.stack = ",".join(args.stack)
one_time_command += f" --stack={args.stack}"
width = shutil.get_terminal_size().columns
input(width)
print("#"*width)
print(f"COMMAND : {one_time_command}")
print("#"*width)
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+159 -53
View File
@@ -13,6 +13,8 @@ import tabulate
from git import Repo from git import Repo
import requests import requests
import hvac import hvac
import subprocess
import tempfile
from prompt_toolkit import prompt from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.shortcuts import checkboxlist_dialog from prompt_toolkit.shortcuts import checkboxlist_dialog
@@ -21,7 +23,7 @@ from prompt_toolkit.shortcuts import radiolist_dialog
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Portainer: class PortainerApi:
""" """
Simple wrapper around the module-level Portainer helper functions. Simple wrapper around the module-level Portainer helper functions.
Instantiate with base_url and optional token/timeout and call methods Instantiate with base_url and optional token/timeout and call methods
@@ -37,6 +39,7 @@ class Portainer:
self.timeout = timeout self.timeout = timeout
self.git_url = "git@gitlab.sectorq.eu:home/docker-compose.git" self.git_url = "git@gitlab.sectorq.eu:home/docker-compose.git"
self.stack_name = None self.stack_name = None
self.stack_names = []
self.stacks_all = {} self.stacks_all = {}
self.stack_id = None self.stack_id = None
self.stack_ids = [] self.stack_ids = []
@@ -122,6 +125,7 @@ class Portainer:
self.cur_config = config self.cur_config = config
def get_site(self, site): def get_site(self, site):
self.site = site
if site == "portainer": if site == "portainer":
self.base_url = os.getenv( self.base_url = os.getenv(
"PORTAINER_URL", "https://portainer.sectorq.eu/api" "PORTAINER_URL", "https://portainer.sectorq.eu/api"
@@ -141,6 +145,13 @@ class Portainer:
self.get_endpoints() self.get_endpoints()
self.get_stacks() self.get_stacks()
def refresh_status(self, args):
for s in self.all_data['stacks']['m-s']['by_id']:
path = f'/stacks/{s}/images_status?refresh=true'
print(path)
res = self._api_get(path, timeout=args.timeout)
def _is_number(self, s): def _is_number(self, s):
"""Check if the input string is a number.""" """Check if the input string is a number."""
try: try:
@@ -159,7 +170,7 @@ class Portainer:
response = requests.post( response = requests.post(
"https://gotify.sectorq.eu/message", "https://gotify.sectorq.eu/message",
data=payload, data=payload,
headers={"X-Gotify-Key": "ASn_fIAd5OVjm8c"} headers={"X-Gotify-Key": "A1krRuo8GIW-fpY"}
) )
logger.debug(response.text) logger.debug(response.text)
# print("Status:", response.status_code) # print("Status:", response.status_code)
@@ -169,7 +180,7 @@ class Portainer:
def _api_get(self, path, timeout=120): def _api_get(self, path, timeout=120):
url = f"{self.base_url.rstrip('/')}{path}" url = f"{self.base_url.rstrip('/')}{path}"
headers = {"X-API-Key": f"{self.token}"} headers = {"X-API-Key": f"{self.token}"}
resp = requests.get(url, headers=headers, timeout=timeout) resp = requests.get(url, headers=headers, timeout=120)
if resp.status_code != 200: if resp.status_code != 200:
return resp.status_code return resp.status_code
print(f"Error: {resp.status_code} - {resp.text}") print(f"Error: {resp.status_code} - {resp.text}")
@@ -230,6 +241,34 @@ class Portainer:
self.get_containers(self) self.get_containers(self)
return True return True
def get_stacks_failed(self, timeout=20):
'''Get a list of stacks for a specific endpoint or all endpoints.'''
path = "/stacks"
stcks = []
stacks = self._api_get(path, timeout=timeout)
self.stacks_all = {}
fail_endponts = [20, 39, 41, 32, 43]
# print(json.dumps(stacks,indent=2))
for s in stacks:
stcks.append((s["EndpointId"], s["Id"]))
print(f"{s['EndpointId']} : {s['Id']}")
return stcks
def delete_failed_stack(self, timeout=20):
'''Get a list of stacks for a specific endpoint or all endpoints.'''
path = "/stacks"
stcks = []
stacks = self._api_get(path, timeout=timeout)
# print(json.dumps(stacks,indent=2))
eid = input("Enter endpoint : ")
for s in stacks:
# print(s)
# print(f"{s['EndpointId']} : {s['Id']}")
if int(eid) == s["EndpointId"]:
print(f"deleting stack {s['Id']}")
self._delete_single_stack(s["Id"], "50")
def get_stacks(self, endpoint_id="all", timeout=20): def get_stacks(self, endpoint_id="all", timeout=20):
'''Get a list of stacks for a specific endpoint or all endpoints.''' '''Get a list of stacks for a specific endpoint or all endpoints.'''
if endpoint_id != "all": if endpoint_id != "all":
@@ -238,7 +277,7 @@ class Portainer:
stcks = [] stcks = []
stacks = self._api_get(path, timeout=timeout) stacks = self._api_get(path, timeout=timeout)
self.stacks_all = {} self.stacks_all = {}
fail_endponts = [20, 39, 41] fail_endponts = [20, 39, 41, 32, 43]
# print(json.dumps(stacks,indent=2)) # print(json.dumps(stacks,indent=2))
webhooks = {} webhooks = {}
for s in stacks: for s in stacks:
@@ -315,6 +354,7 @@ class Portainer:
def get_endpoint_id(self): def get_endpoint_id(self):
'''Get endpoint ID from either ID or name input.''' '''Get endpoint ID from either ID or name input.'''
# input(self.args.endpoint_id)
if self._is_number(self.args.endpoint_id): if self._is_number(self.args.endpoint_id):
self.endpoint_id = self.args.endpoint_id self.endpoint_id = self.args.endpoint_id
self.endpoint_name = self.endpoints["by_id"][self.args.endpoint_id] self.endpoint_name = self.endpoints["by_id"][self.args.endpoint_id]
@@ -342,7 +382,6 @@ class Portainer:
# print(stack) # print(stack)
cont = [] cont = []
data = {} data = {}
eps = [ep for ep in self.all_data['endpoints']['by_id'].keys()] eps = [ep for ep in self.all_data['endpoints']['by_id'].keys()]
#input(eps) #input(eps)
for endpoint in eps: for endpoint in eps:
@@ -361,11 +400,15 @@ class Portainer:
print(f"failed to get containers from {path}: {e}") print(f"failed to get containers from {path}: {e}")
continue continue
contr = [] contr = []
# print(f"Containers: {containers}")
try: try:
for c in containers: for c in containers:
#input(c) # print(c)
try:
cont.append([c["Names"][0].replace("/", ""),c["Id"], c['Image']]) cont.append([c["Names"][0].replace("/", ""),c["Id"], c['Image']])
contr.append([c["Names"][0].replace("/", ""), c["Id"], c['Image']]) contr.append([c["Names"][0].replace("/", ""), c["Id"], c['Image']])
except:
print("Unable to parse container info")
if self.all_data["endpoints"]["by_id"][endpoint] in data: if self.all_data["endpoints"]["by_id"][endpoint] in data:
data[self.all_data["endpoints"]["by_id"][endpoint]] = contr data[self.all_data["endpoints"]["by_id"][endpoint]] = contr
data[endpoint] = contr data[endpoint] = contr
@@ -394,14 +437,13 @@ class Portainer:
else: else:
eps = [self.get_endpoint_id()] eps = [self.get_endpoint_id()]
#input(eps)
for endpoint in eps:
# print(s) for endpoint in eps:
#print(self.args.stack) #print(self.args.stack)
if self.args.stack in ["all", None]: if self.args.stack in ["all", None]:
# input([id for id in self.all_data["stacks"][endpoint]['by_id'].keys()]) # 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"]: # if s not in self.all_data["stacks"]:
# continue # continue
#input(self.all_data) #input(self.all_data)
@@ -409,7 +451,7 @@ class Portainer:
# print(f"Endpoint {self.all_data["endpoints"]["by_id"][s]} is offline") # print(f"Endpoint {self.all_data["endpoints"]["by_id"][s]} is offline")
continue continue
# input(self.all_data["stacks"][endpoint]["by_name"]) # input(self.all_data["stacks"][endpoint]["by_name"])
for e in self.all_data["stacks"][endpoint]["by_name"]:
#input(e) #input(e)
path = ( path = (
f"/endpoints/{endpoint}/docker/containers/json" f"/endpoints/{endpoint}/docker/containers/json"
@@ -489,7 +531,7 @@ class Portainer:
stacks_tuples.append((s['Webhook'],s['Name'])) stacks_tuples.append((s['Webhook'],s['Name']))
# print(s['Name'], " : ", s['Webhook']) # print(s['Name'], " : ", s['Webhook'])
stacks_dict = dict(stacks_tuples) stacks_dict = dict(stacks_tuples)
print(stacks_dict) # print(stacks_dict)
#input(stacks_tuples) #input(stacks_tuples)
# stacks_tuples = [(s['AutoUpdate']['Webhook'], s['Name']) for s in stacks if "Webhook" in s['AutoUpdate'] ] # stacks_tuples = [(s['AutoUpdate']['Webhook'], s['Name']) for s in stacks if "Webhook" in s['AutoUpdate'] ]
@@ -514,7 +556,7 @@ class Portainer:
values=stacks_tuples values=stacks_tuples
).run() ).run()
stcs = [] stcs = []
input(stack_ids) #input(stack_ids)
if args.stack == "all": if args.stack == "all":
for s in stack_dict: for s in stack_dict:
@@ -524,18 +566,18 @@ class Portainer:
if s in stack_ids: if s in stack_ids:
stcs.append([s, stack_dict[s]]) stcs.append([s, stack_dict[s]])
print(stcs) # print(stcs)
with ThreadPoolExecutor(max_workers=10) as exe: with ThreadPoolExecutor(max_workers=10) as exe:
list(exe.map(update, stcs)) list(exe.map(update, stcs))
input('UPDATED') #input('UPDATED')
if not args.autostart: if not args.autostart:
time.sleep(120) time.sleep(120)
cont = [] cont = []
for c in self.all_data["containers"][endpoint]: for c in self.all_data["containers"][args.endpoint_id]:
if stack == c or stack == "all": if args.stack == c or args.stack == "all":
cont += self.all_data["containers"][endpoint][c] cont += self.all_data["containers"][args.endpoint_id][c]
self.stop_containers(endpoint, cont) self.stop_containers(args.endpoint_id, cont)
def get_endpoints(self, timeout=10): def get_endpoints(self, timeout=10):
'''Get a list of all endpoints.''' '''Get a list of all endpoints.'''
@@ -618,7 +660,18 @@ class Portainer:
autostart=False, autostart=False,
stack_mode="swarm", stack_mode="swarm",
): ):
diff_stacks = ['mediacenter']
for stack in stacks: for stack in stacks:
server = ""
print("Stack:", stack)
print("Endpoint:", endpoint)
if stack in diff_stacks:
if endpoint == "nas":
server = "_nas"
elif endpoint == "m-s":
server = "_m-server"
if stack_mode == "swarm": if stack_mode == "swarm":
swarm_id = self.get_swarm_id(endpoint) swarm_id = self.get_swarm_id(endpoint)
p = "swarm" p = "swarm"
@@ -710,8 +763,8 @@ class Portainer:
}, },
"repositoryURL": "https://gitlab.sectorq.eu/home/docker-compose.git", "repositoryURL": "https://gitlab.sectorq.eu/home/docker-compose.git",
"ReferenceName": "refs/heads/main", "ReferenceName": "refs/heads/main",
"composeFile": f"{stack}/docker-compose.yml", "composeFile": f"{stack}/docker-compose{server}.yml",
"ConfigFilePath": f"{stack}/docker-compose.yml", "ConfigFilePath": f"{stack}/docker-compose{server}.yml",
"repositoryAuthentication": True, "repositoryAuthentication": True,
"repositoryUsername": "jaydee", "repositoryUsername": "jaydee",
"repositoryPassword": "glpat-uj-n-eEfTY398PE4vKSS", "repositoryPassword": "glpat-uj-n-eEfTY398PE4vKSS",
@@ -834,7 +887,7 @@ class Portainer:
} }
self._api_post_file(path, self.endpoint_id, stack, envs, file) 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.""" """Print a table of stacks, optionally filtered by endpoint."""
stacks = self.get_stacks() stacks = self.get_stacks()
count = 0 count = 0
@@ -842,11 +895,11 @@ class Portainer:
stack_names = [] stack_names = []
for stack in stacks: for stack in stacks:
# print(stack) # print(stack)
if endpoint is not None: if args.endpoint_id is not None:
if not stack["EndpointId"] in self.endpoints["by_id"]: if not stack["EndpointId"] in self.endpoints["by_id"]:
continue continue
if endpoint != "all": if args.endpoint_id != "all":
if self.endpoints["by_name"][endpoint] != stack["EndpointId"]: if self.endpoints["by_name"][args.endpoint_id] != stack["EndpointId"]:
continue continue
try: try:
stack_names.append(stack["Name"]) stack_names.append(stack["Name"])
@@ -873,7 +926,7 @@ class Portainer:
def update_containers(self): def update_containers(self):
all_containers = self.all_data["containers"][self.args.endpoint_id] all_containers = self.all_data["containers"][self.args.endpoint_id]
#input(all_containers) #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_tuples = sorted(service_tuples, key=lambda x: x[1])
service_dict = dict(service_tuples) service_dict = dict(service_tuples)
# input(service_tuples) # input(service_tuples)
@@ -913,9 +966,12 @@ class Portainer:
#print(longest) #print(longest)
ok = "\033[92m✔\033[0m" ok = "\033[92m✔\033[0m"
err = "\033[91m✖\033[0m" err = "\033[91m✖\033[0m"
updates = []
for service_id in service_ids: for service_id in service_ids:
# print(self.all_data["containers"][self.args.endpoint_id]) # print(self.all_data["containers"][self.args.endpoint_id])
if service_dict[service_id] in self.args.excluded:
print(f"App {service_dict[service_id]} excluded")
continue
print("\033[?25l", end="") print("\033[?25l", end="")
print(f"{service_dict[service_id]:<{longest}} ", end="", flush=True) print(f"{service_dict[service_id]:<{longest}} ", end="", flush=True)
path = f"/docker/{self.get_endpoint_id()}/containers/{service_id}/image_status?refresh=true" path = f"/docker/{self.get_endpoint_id()}/containers/{service_id}/image_status?refresh=true"
@@ -930,10 +986,10 @@ class Portainer:
print("?") print("?")
elif resp['Status'] == "outdated": elif resp['Status'] == "outdated":
if pull: if pull:
print("Recreate") #print("Recreate")
self.recreate_container(service_id, pull) self.recreate_container(service_id, pull)
#print(f"Service {service_dict[service_id]:<{longest}} : updated") #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=" ") print(ok, end=" ")
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]: for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
if name.startswith(service_dict[service_id]): if name.startswith(service_dict[service_id]):
@@ -941,7 +997,7 @@ class Portainer:
else: else:
print(f"\r\033[4m{service_dict[service_id]:<{longest}}\033[0m ", end="", flush=True) 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") #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=" ") print(err, end=" ")
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]: for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
if name.startswith(service_dict[service_id]): if name.startswith(service_dict[service_id]):
@@ -951,12 +1007,24 @@ class Portainer:
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]: for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
if name.startswith(service_dict[service_id]): if name.startswith(service_dict[service_id]):
print(image) 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="") print("\033[?25h", end="")
return True return True
def update_service(self): def update_service(self):
all_services = self.get_services(self.get_endpoint_id()) all_services = self.get_services(self.get_endpoint_id())
#input(all_services) if self.args.debug:
print(all_services)
if all_services == 503:
print("No services found on this endpoint.")
return False
if len(all_services) == 0:
print("No services found on this endpoint.")
return False
service_tuples = [(s['ID'], s['Spec']['Name']) for s in all_services] service_tuples = [(s['ID'], s['Spec']['Name']) for s in all_services]
service_tuples = sorted(service_tuples, key=lambda x: x[1]) service_tuples = sorted(service_tuples, key=lambda x: x[1])
service_dict = dict(service_tuples) service_dict = dict(service_tuples)
@@ -1000,6 +1068,7 @@ class Portainer:
#print(longest) #print(longest)
ok = "\033[92m✔\033[0m" ok = "\033[92m✔\033[0m"
err = "\033[91m✖\033[0m" err = "\033[91m✖\033[0m"
service_to_update = []
for service_id in service_ids: for service_id in service_ids:
print("\033[?25l", end="") print("\033[?25l", end="")
print(f"{service_dict[service_id]:<{longest}} ", end="", flush=True) print(f"{service_dict[service_id]:<{longest}} ", end="", flush=True)
@@ -1012,18 +1081,23 @@ class Portainer:
return [] return []
if resp['Status'] == "outdated": if resp['Status'] == "outdated":
service_to_update.append(service_dict[service_id])
if pull: if pull:
if service_dict[service_id] in self.args.excluded:
continue
self.restart_srv(service_id, pull) self.restart_srv(service_id, pull)
#print(f"Service {service_dict[service_id]:<{longest}} : updated") #print(f"Service {service_dict[service_id]:<{longest}} : updated")
self.gotify_message(f"Service {service_dict[service_id]} updated") print(f"{ok} updated")
print(ok)
else: else:
print(f"\r\033[4m{service_dict[service_id]:<{longest}}\033[0m ", end="", flush=True) 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") #print(f"\033[4m{service_dict[service_id]:<{longest}} {err}\033[0m")
self.gotify_message(f"Service update available for {service_dict[service_id]}")
print(err) print(err)
else: else:
print(ok) print(ok)
if pull:
self.gotify_message(f"Services updated: {', '.join(service_to_update)}")
else:
self.gotify_message(f"Service update available: {', '.join(service_to_update)}")
print("\033[?25h", end="") print("\033[?25h", end="")
return True return True
@@ -1094,10 +1168,10 @@ class Portainer:
def recreate_container(self,service_id, pull=False): def recreate_container(self,service_id, pull=False):
"""Restart a service on an endpoint.""" """Restart a service on an endpoint."""
path = f"/docker/{self.endpoint_id}/containers/{service_id}/recreate" path = f"/docker/{self.endpoint_id}/containers/{service_id}/recreate"
print(path) # print(path)
params={"pullImage": pull} params={"pullImage": pull}
try: try:
resp = self._api_post(path, json=params, timeout=20) resp = self._api_post(path, json=params, timeout=120)
#print(resp) #print(resp)
except ValueError as e: except ValueError as e:
print(f"Error restarting service: {e}") print(f"Error restarting service: {e}")
@@ -1108,7 +1182,7 @@ class Portainer:
path = f"/endpoints/{self.endpoint_id}/forceupdateservice" path = f"/endpoints/{self.endpoint_id}/forceupdateservice"
params={"serviceID": service_id, "pullImage": pool} params={"serviceID": service_id, "pullImage": pool}
try: try:
resp = self._api_put(path, json=params, timeout=20) resp = self._api_put(path, json=params, timeout=120)
# print(resp) # print(resp)
except ValueError as e: except ValueError as e:
print(f"Error restarting service: {e}") print(f"Error restarting service: {e}")
@@ -1149,44 +1223,75 @@ class Portainer:
def start_stack(self, stack=None, endpoint_id=None): def start_stack(self, stack=None, endpoint_id=None):
"""Start one stack or all stacks on an endpoint.""" """Start one stack or all stacks on an endpoint."""
ok = "\033[92m✔\033[0m"
ok2 = "\033[93m✔\033[0m"
err = "\033[91m✖\033[0m"
if endpoint_id is not None: if endpoint_id is not None:
print("Getting endpoint") print("Getting endpoint")
self.get_endpoint(endpoint_id) self.get_endpoint(endpoint_id)
size = 0
if stack is not None: if stack is not None:
if type(stack) == str:
stack = stack.split(",")
for s in stack: for s in stack:
self.stack_ids = [self._resolve_stack_id(s, endpoint_id)] if len(s) > size:
size = len(s)
self.stack_ids.append(self._resolve_stack_id(s, endpoint_id))
size = size + 5
for stck in self.stack_ids: for stck in self.stack_ids:
print(
f"Starting stack {self.stacks_all[self.endpoint_id]['by_id'][stck][:size].ljust(size)}",
end="", flush=True
)
path = f"/stacks/{stck}/start" path = f"/stacks/{stck}/start"
if self.endpoint_id is not None: if self.endpoint_id is not None:
path += f"?endpointId={self.endpoint_id}" path += f"?endpointId={self.endpoint_id}"
try: try:
resp = self._api_post_no_body(path, timeout=20) resp = self._api_post_no_body(path, timeout=120)
except ValueError as e: except ValueError as e:
print(f"Error stoping stack: {e}") print(f"Error starting stack: {e}")
return [] return []
if "Id" in json.loads(resp): if "Id" in json.loads(resp):
print( print(ok)
f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stck]} : started" elif "already running" in json.loads(resp)['message']:
) print(ok2)
else: else:
print( print(
f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stck]} : {json.loads(resp)['message']}" f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stck]} : {json.loads(resp)['message']}"
) )
return True return True
def stop_stack(self, stack, endpoint_id): def stop_stack(self, stack, endpoint_id):
"""Stop one stack or all stacks on an endpoint.""" """Stop one stack or all stacks on an endpoint."""
print(f"Stopping stack {stack}") # print(f"Stopping stack {stack}")
protected_stack = ['hashicorp','nginx','pihole',]
ok = "\033[92m✔\033[0m"
ok2 = "\033[93m✔\033[0m"
err = "\033[91m✖\033[0m"
if endpoint_id is not None: if endpoint_id is not None:
self.get_endpoint(endpoint_id) self.get_endpoint(endpoint_id)
size = 0
if stack is not None: if stack is not None:
if type(stack) == str:
stack = stack.split(",")
for s in stack: for s in stack:
self.stack_ids = [self._resolve_stack_id(s, endpoint_id)] if size < len(s):
# print(self.stack_ids) size = len(s)
self.stack_ids.append(self._resolve_stack_id(s, endpoint_id))
size = size + 5
self.stack_ids = list(dict.fromkeys(self.stack_ids))
for stck in self.stack_ids: for stck in self.stack_ids:
if self.stacks_all[self.endpoint_id]['by_id'] in protected_stack:
ans = input(f"Really stop {self.stacks_all[self.endpoint_id]['by_id'][stck]} ? ") or "n"
if ans != "y":
continue
print(
f"Stopping stack {self.stacks_all[self.endpoint_id]['by_id'][stck][:size].ljust(size)}",
end="",
flush=True
)
path = f"/stacks/{stck}/stop" path = f"/stacks/{stck}/stop"
# print(path) # print(path)
if self.endpoint_id is not None: if self.endpoint_id is not None:
@@ -1197,9 +1302,9 @@ class Portainer:
print(f"Error stopping stack: {e}") print(f"Error stopping stack: {e}")
return [] return []
if "Id" in json.loads(resp): if "Id" in json.loads(resp):
print( print(ok)
f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stck]} : stopped" elif "already inactive" in json.loads(resp)['message']:
) print(ok2)
else: else:
print( print(
f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stck]} : {json.loads(resp)['message']}" f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stck]} : {json.loads(resp)['message']}"
@@ -1224,8 +1329,8 @@ class Portainer:
return "all" return "all"
if not self._is_number(stack): if not self._is_number(stack):
result = self.get_stack(stack, endpoint_id) result = self.stacks_all[endpoint_id]['by_name'][stack]
return result["Id"] return result
return int(stack) return int(stack)
@@ -1351,4 +1456,5 @@ class Portainer:
path = f"/endpoints/{endpoint_id}/docker/secrets/create" path = f"/endpoints/{endpoint_id}/docker/secrets/create"
encoded = base64.b64encode(value.encode()).decode() encoded = base64.b64encode(value.encode()).decode()
data = {"Name": name, "Data": encoded} data = {"Name": name, "Data": encoded}
return self._api_post(path, data, timeout=timeout) return self._api_post(path, data, timeout=timeout)
+3
View File
@@ -5,3 +5,6 @@ tabulate
flake8 flake8
pylint pylint
black black
docker
hvac
prompt_toolkit