From 97b69f84e8854ad933a3645191caed597123fe2e Mon Sep 17 00:00:00 2001 From: Lennu Vuolanne Date: Mon, 4 Aug 2025 16:43:32 +0300 Subject: [PATCH 01/10] add github actions --- .github/workflows/pull_request.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..43ff2af --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,18 @@ +name: Pulumi +on: + - pull_request +jobs: + preview: + name: Pulumi Preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.13 + + - run: pip install -r requirements.txt + - uses: pulumi/actions@v6 + with: + command: preview + cloud-url: ${{ secrets.PULUMI_CLOUD_URL }} From c1938b6d24597df0c5c66defecdd90639c4c9b35 Mon Sep 17 00:00:00 2001 From: Lennu Vuolanne Date: Mon, 4 Aug 2025 17:01:28 +0300 Subject: [PATCH 02/10] add gcp auth to actions --- .github/workflows/pull_request.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 43ff2af..4b49062 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,9 +10,13 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.13 - + - uses: "google-github-actions/auth@v2" + with: + project_id: "osakunta-telegram-bot" + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - run: pip install -r requirements.txt - uses: pulumi/actions@v6 with: command: preview + stack-name: prod cloud-url: ${{ secrets.PULUMI_CLOUD_URL }} From 464ef8da1b3a16d48430b2376ae26371ac6c4f10 Mon Sep 17 00:00:00 2001 From: Lennu Vuolanne Date: Mon, 4 Aug 2025 17:03:26 +0300 Subject: [PATCH 03/10] update actions permissions --- .github/workflows/pull_request.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4b49062..7efaf13 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -5,6 +5,9 @@ jobs: preview: name: Pulumi Preview runs-on: ubuntu-latest + permissions: + contents: "read" + id-token: "write" steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From 0c3e667a3da28f1d33d76ae0571b661e6a977d2d Mon Sep 17 00:00:00 2001 From: Lennu Vuolanne Date: Thu, 7 Aug 2025 17:36:18 +0300 Subject: [PATCH 04/10] separate infra and app code workflows --- .github/workflows/pull_request_app.yml | 25 +++++++++++++++++ ...ull_request.yml => pull_request_infra.yml} | 5 +++- README.md | 28 +++++++++++++++---- infra/__main__.py | 22 ++------------- 4 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/pull_request_app.yml rename .github/workflows/{pull_request.yml => pull_request_infra.yml} (87%) diff --git a/.github/workflows/pull_request_app.yml b/.github/workflows/pull_request_app.yml new file mode 100644 index 0000000..2a79823 --- /dev/null +++ b/.github/workflows/pull_request_app.yml @@ -0,0 +1,25 @@ +name: Pulumi +on: + pull_request: + paths-ignore: + - infra/** + - .github/workflows/pull_request_infra.yml +jobs: + preview: + name: Pulumi Preview + runs-on: ubuntu-latest + permissions: + contents: "read" + id-token: "write" + steps: + - uses: actions/checkout@v4 + - uses: "google-github-actions/auth@v2" + with: + project_id: "osakunta" + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + - run: pip install -r requirements.txt + - uses: pulumi/actions@v6 + with: + command: preview + stack-name: prod + cloud-url: ${{ secrets.PULUMI_CLOUD_URL }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request_infra.yml similarity index 87% rename from .github/workflows/pull_request.yml rename to .github/workflows/pull_request_infra.yml index 7efaf13..1c97720 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request_infra.yml @@ -1,6 +1,9 @@ name: Pulumi on: - - pull_request + pull_request: + paths: + - infra/** + - .github/workflows/pull_request_infra.yml jobs: preview: name: Pulumi Preview diff --git a/README.md b/README.md index c9a1bcb..26d8eb2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -SatO Telegram Bot -================= +# SatO Telegram Bot This bot has some useful functionality for Satakuntalainen Osakunta. It can be found on Telegram as `@osakuntabot`. @@ -10,9 +9,27 @@ Available commands: /ruokalista /tjviisi +## Development -Development ------------ +⚠️⚠️ Read the entirety of this section before making changes to the repository ⚠️⚠️ + +### Development flow + +1. Always create a new branch for whatever you are working on +2. When you're done, create a pull request +3. Check the preview for what changes will be done upon deployment +4. If everything looks good, merge the pull request and the changes will be automatically done + +### Warnings + +The code in the `master` branch is automatically deployed. Therefore you should be careful as to what you merge to this branch. + +Changes to the `infra/` directory modify the resources that are deployed on the cloud. These can incur extra cost if you are not careful. +Changes to the application source code (`main.py`, `telegram_bot/`) can also incur costs if they run for long or consume a lot of resources (processor time, bandwidth) + +Be mindful to the changes you make. You can ask for help and comments on your pull request before merging. After large changes, monitor the costs incurred manually on the cloud dashboard. + +### Local development Get `pipenv` from [here](https://pipenv.pypa.io/en/latest/) @@ -30,5 +47,4 @@ You can test the commands on commandline by: python main.py /command [args] -The bot is hosted in Google Cloud Functions and CircleCI is used to continuously deploy new versions of the bot to GCP. -NB! If you add dependencies to the project, remember to generate a new requirements.txt with `pipenv lock -r` and push it to the repo. Pipfile is not supported by Google Cloud Functions. +NB! If you add dependencies to the project, remember to generate a new requirements.txt with `pipenv requirements > requirements.txt` and push it to the repo. Pipfile is not supported by Google Cloud Functions. diff --git a/infra/__main__.py b/infra/__main__.py index 54f9d34..b2f8c78 100644 --- a/infra/__main__.py +++ b/infra/__main__.py @@ -1,29 +1,14 @@ -"""A Python Pulumi program""" - import pulumi import pulumi_gcp as gcp -import tarfile -import hashlib - -# create .tar.gz source - -# SOURCE_TAR_NAME = "source.tar.gz" - -# tarfile_hash = None -# with tarfile.open(SOURCE_TAR_NAME, "w|gz") as tar: -# tar.add("../telegram_bot", arcname="telegram_bot") -# tar.add("../main.py", arcname="main.py") -# tar.add("../requirements.txt", arcname="requirements.txt") -# last_modified = max([member.mtime for member in tar.getmembers()]) -# tarfile_hash = hashlib.sha256(str(last_modified).encode()).hexdigest() +# Be careful editing this file, if you are unfamiliar with Pulumi or the Google Cloud Platform. +# Make sure you read the README.md in the root of this repository first. # setup infrastructure PROJECT_ID = "osakunta-telegram-bot" LOCATION = "europe-north1" - # Set up secret to hold the Telegram API token telegram_bot_token = gcp.secretmanager.Secret("telegram-bot-token", secret_id="telegram-bot-token", @@ -102,5 +87,4 @@ name=function.name, role="roles/run.invoker", member="allUsers" -) - +) \ No newline at end of file From 5cbca736e1ba323403e95b7573f84f7caa667418 Mon Sep 17 00:00:00 2001 From: Lennu Vuolanne Date: Fri, 15 Aug 2025 17:38:47 +0300 Subject: [PATCH 05/10] updates --- .gcloudignore | 10 ++ Pipfile | 1 + Pipfile.lock | 234 ++++++++++++++++++++++++++++++++++++++++- infra/Pulumi.prod.yaml | 5 +- infra/__main__.py | 228 +++++++++++++++++++++++++++------------ infra/cicd.py | 106 +++++++++++++++++++ infra/github.py | 0 infra/project.py | 91 ++++++++++++++++ infra/utils.py | 15 +++ 9 files changed, 622 insertions(+), 68 deletions(-) create mode 100644 .gcloudignore create mode 100644 infra/cicd.py create mode 100644 infra/github.py create mode 100644 infra/project.py create mode 100644 infra/utils.py diff --git a/.gcloudignore b/.gcloudignore new file mode 100644 index 0000000..bfc4c90 --- /dev/null +++ b/.gcloudignore @@ -0,0 +1,10 @@ +# ignore everything +* + +# except the following +!.gcloudignore +!.main.py +!.requirements.txt +!telegram_bot/bot.py +!telegram_bot/menu.py +!telegram_bot/init.py \ No newline at end of file diff --git a/Pipfile b/Pipfile index 0ce813b..351f595 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,7 @@ python-dotenv = "*" python-telegram-bot = "*" pytz = "*" functions-framework = "*" +pulumi-random = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index b5efe36..70bb6d6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6b3c4289bbf92fcb79325b7fa45280eb28120f3dba79d1e79ee0d957b1c9d77b" + "sha256": "9328f1813d374fe25e5d07d0d2b1c2ec8a671299b9c2262a391564e9a26ec4d0" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,21 @@ "markers": "python_version >= '3.9'", "version": "==4.9.0" }, + "arpeggio": { + "hashes": [ + "sha256:c790b2b06e226d2dd468e4fbfb5b7f506cec66416031fde1441cf1de2a0ba700", + "sha256:f7c8ae4f4056a89e020c24c7202ac8df3e2bc84e416746f20b0da35bb1de0250" + ], + "version": "==2.0.2" + }, + "attrs": { + "hashes": [ + "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", + "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" + ], + "markers": "python_version >= '3.8'", + "version": "==25.3.0" + }, "beautifulsoup4": { "hashes": [ "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", @@ -169,6 +184,38 @@ ], "version": "==1.11.0" }, + "debugpy": { + "hashes": [ + "sha256:135ccd2b1161bade72a7a099c9208811c137a150839e970aeaf121c2467debe8", + "sha256:19c9521962475b87da6f673514f7fd610328757ec993bf7ec0d8c96f9a325f9e", + "sha256:211238306331a9089e253fd997213bc4a4c65f949271057d6695953254095376", + "sha256:2801329c38f77c47976d341d18040a9ac09d0c71bf2c8b484ad27c74f83dc36f", + "sha256:2a3958fb9c2f40ed8ea48a0d34895b461de57a1f9862e7478716c35d76f56c65", + "sha256:31e69a1feb1cf6b51efbed3f6c9b0ef03bc46ff050679c4be7ea6d2e23540870", + "sha256:64473c4a306ba11a99fe0bb14622ba4fbd943eb004847d9b69b107bde45aa9ea", + "sha256:67371b28b79a6a12bcc027d94a06158f2fde223e35b5c4e0783b6f9d3b39274a", + "sha256:687c7ab47948697c03b8f81424aa6dc3f923e6ebab1294732df1ca9773cc67bc", + "sha256:70f5fcd6d4d0c150a878d2aa37391c52de788c3dc680b97bdb5e529cb80df87a", + "sha256:75f204684581e9ef3dc2f67687c3c8c183fde2d6675ab131d94084baf8084121", + "sha256:833a61ed446426e38b0dd8be3e9d45ae285d424f5bf6cd5b2b559c8f12305508", + "sha256:85df3adb1de5258dca910ae0bb185e48c98801ec15018a263a92bb06be1c8787", + "sha256:8624a6111dc312ed8c363347a0b59c5acc6210d897e41a7c069de3c53235c9a6", + "sha256:88eb9ffdfb59bf63835d146c183d6dba1f722b3ae2a5f4b9fc03e925b3358922", + "sha256:a2ba6fc5d7c4bc84bcae6c5f8edf5988146e55ae654b1bb36fecee9e5e77e9e2", + "sha256:b202e2843e32e80b3b584bcebfe0e65e0392920dc70df11b2bfe1afcb7a085e4", + "sha256:b2abae6dd02523bec2dee16bd6b0781cccb53fd4995e5c71cc659b5f45581898", + "sha256:b5aea1083f6f50023e8509399d7dc6535a351cc9f2e8827d1e093175e4d9fa4c", + "sha256:bee89e948bc236a5c43c4214ac62d28b29388453f5fd328d739035e205365f0b", + "sha256:c2c47c2e52b40449552843b913786499efcc3dbc21d6c49287d939cd0dbc49fd", + "sha256:cf358066650439847ec5ff3dae1da98b5461ea5da0173d93d5e10f477c94609a", + "sha256:d58c48d8dbbbf48a3a3a638714a2d16de537b0dace1e3432b8e92c57d43707f8", + "sha256:e5ca7314042e8a614cc2574cd71f6ccd7e13a9708ce3c6d8436959eae56f2378", + "sha256:f8340a3ac2ed4f5da59e064aa92e39edd52729a88fbde7bbaa54e08249a04493", + "sha256:fee6db83ea5c978baf042440cfe29695e1a5d48a30147abf4c3be87513609817" + ], + "markers": "python_version >= '3.8'", + "version": "==1.8.16" + }, "deprecation": { "hashes": [ "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", @@ -176,6 +223,14 @@ ], "version": "==2.1.0" }, + "dill": { + "hashes": [ + "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", + "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049" + ], + "markers": "python_version >= '3.8'", + "version": "==0.4.0" + }, "flask": { "hashes": [ "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", @@ -193,6 +248,67 @@ "markers": "python_version >= '3.5' and python_version < '4'", "version": "==3.9.2" }, + "grpcio": { + "hashes": [ + "sha256:02697eb4a5cbe5a9639f57323b4c37bcb3ab2d48cec5da3dc2f13334d72790dd", + "sha256:03b0b307ba26fae695e067b94cbb014e27390f8bc5ac7a3a39b7723fed085604", + "sha256:05bc2ceadc2529ab0b227b1310d249d95d9001cd106aa4d31e8871ad3c428d73", + "sha256:06de8ec0bd71be123eec15b0e0d457474931c2c407869b6c349bd9bed4adbac3", + "sha256:0be4e0490c28da5377283861bed2941d1d20ec017ca397a5df4394d1c31a9b50", + "sha256:12fda97ffae55e6526825daf25ad0fa37483685952b5d0f910d6405c87e3adb6", + "sha256:1caa38fb22a8578ab8393da99d4b8641e3a80abc8fd52646f1ecc92bcb8dee34", + "sha256:2018b053aa15782db2541ca01a7edb56a0bf18c77efed975392583725974b249", + "sha256:20657d6b8cfed7db5e11b62ff7dfe2e12064ea78e93f1434d61888834bc86d75", + "sha256:2335c58560a9e92ac58ff2bc5649952f9b37d0735608242973c7a8b94a6437d8", + "sha256:31fd163105464797a72d901a06472860845ac157389e10f12631025b3e4d0453", + "sha256:38b68498ff579a3b1ee8f93a05eb48dc2595795f2f62716e797dc24774c1aaa8", + "sha256:3b00efc473b20d8bf83e0e1ae661b98951ca56111feb9b9611df8efc4fe5d55d", + "sha256:3ed71e81782966ffead60268bbda31ea3f725ebf8aa73634d5dda44f2cf3fb9c", + "sha256:45a3d462826f4868b442a6b8fdbe8b87b45eb4f5b5308168c156b21eca43f61c", + "sha256:49f0ca7ae850f59f828a723a9064cadbed90f1ece179d375966546499b8a2c9c", + "sha256:4e504572433f4e72b12394977679161d495c4c9581ba34a88d843eaf0f2fbd39", + "sha256:4ea1d062c9230278793820146c95d038dc0f468cbdd172eec3363e42ff1c7d01", + "sha256:563588c587b75c34b928bc428548e5b00ea38c46972181a4d8b75ba7e3f24231", + "sha256:6001e575b8bbd89eee11960bb640b6da6ae110cf08113a075f1e2051cc596cae", + "sha256:66a0cd8ba6512b401d7ed46bb03f4ee455839957f28b8d61e7708056a806ba6a", + "sha256:6851de821249340bdb100df5eacfecfc4e6075fa85c6df7ee0eb213170ec8e5d", + "sha256:728bdf36a186e7f51da73be7f8d09457a03061be848718d0edf000e709418987", + "sha256:73e3b425c1e155730273f73e419de3074aa5c5e936771ee0e4af0814631fb30a", + "sha256:73fc8f8b9b5c4a03e802b3cd0c18b2b06b410d3c1dcbef989fdeb943bd44aff7", + "sha256:78fa51ebc2d9242c0fc5db0feecc57a9943303b46664ad89921f5079e2e4ada7", + "sha256:7b2c86457145ce14c38e5bf6bdc19ef88e66c5fee2c3d83285c5aef026ba93b3", + "sha256:7d69ce1f324dc2d71e40c9261d3fdbe7d4c9d60f332069ff9b2a4d8a257c7b2b", + "sha256:802d84fd3d50614170649853d121baaaa305de7b65b3e01759247e768d691ddf", + "sha256:80fd702ba7e432994df208f27514280b4b5c6843e12a48759c9255679ad38db8", + "sha256:8ac475e8da31484efa25abb774674d837b343afb78bb3bcdef10f81a93e3d6bf", + "sha256:950da58d7d80abd0ea68757769c9db0a95b31163e53e5bb60438d263f4bed7b7", + "sha256:99a641995a6bc4287a6315989ee591ff58507aa1cbe4c2e70d88411c4dcc0839", + "sha256:9c3a99c519f4638e700e9e3f83952e27e2ea10873eecd7935823dab0c1c9250e", + "sha256:9c509a4f78114cbc5f0740eb3d7a74985fd2eff022971bc9bc31f8bc93e66a3b", + "sha256:a18e20d8321c6400185b4263e27982488cb5cdd62da69147087a76a24ef4e7e3", + "sha256:a917d26e0fe980b0ac7bfcc1a3c4ad6a9a4612c911d33efb55ed7833c749b0ee", + "sha256:a9539f01cb04950fd4b5ab458e64a15f84c2acc273670072abe49a3f29bbad54", + "sha256:ad2efdbe90c73b0434cbe64ed372e12414ad03c06262279b104a029d1889d13e", + "sha256:b672abf90a964bfde2d0ecbce30f2329a47498ba75ce6f4da35a2f4532b7acbc", + "sha256:bbd27c24a4cc5e195a7f56cfd9312e366d5d61b86e36d46bbe538457ea6eb8dd", + "sha256:c400ba5675b67025c8a9f48aa846f12a39cf0c44df5cd060e23fda5b30e9359d", + "sha256:c408f5ef75cfffa113cacd8b0c0e3611cbfd47701ca3cdc090594109b9fcbaed", + "sha256:c806852deaedee9ce8280fe98955c9103f62912a5b2d5ee7e3eaa284a6d8d8e7", + "sha256:ce89f5876662f146d4c1f695dda29d4433a5d01c8681fbd2539afff535da14d4", + "sha256:d25a14af966438cddf498b2e338f88d1c9706f3493b1d73b93f695c99c5f0e2a", + "sha256:d8d4732cc5052e92cea2f78b233c2e2a52998ac40cd651f40e398893ad0d06ec", + "sha256:d9a9724a156c8ec6a379869b23ba3323b7ea3600851c91489b871e375f710bc8", + "sha256:e636ce23273683b00410f1971d209bf3689238cf5538d960adc3cdfe80dd0dbd", + "sha256:e88264caad6d8d00e7913996030bac8ad5f26b7411495848cc218bd3a9040b6c", + "sha256:f145cc21836c332c67baa6fc81099d1d27e266401565bf481948010d6ea32d46", + "sha256:fb57870449dfcfac428afbb5a877829fcb0d6db9d9baa1148705739e9083880e", + "sha256:fb70487c95786e345af5e854ffec8cb8cc781bcc5df7930c4fbb7feaa72e1cdf", + "sha256:fe96281713168a3270878255983d2cb1a97e034325c8c2c25169a69289d3ecfa", + "sha256:ff1f7882e56c40b0d33c4922c15dfa30612f05fb785074a012f7cda74d1c3679" + ], + "markers": "python_version >= '3.8'", + "version": "==1.66.2" + }, "gunicorn": { "hashes": [ "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", @@ -324,6 +440,55 @@ "markers": "python_version >= '3.8'", "version": "==25.0" }, + "parver": { + "hashes": [ + "sha256:2281b187276c8e8e3c15634f62287b2fb6fe0efe3010f739a6bd1e45fa2bf2b2", + "sha256:b9fde1e6bb9ce9f07e08e9c4bea8d8825c5e78e18a0052d02e02bf9517eb4777" + ], + "markers": "python_version >= '3.8'", + "version": "==0.5" + }, + "pip": { + "hashes": [ + "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2", + "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717" + ], + "markers": "python_version >= '3.9'", + "version": "==25.2" + }, + "protobuf": { + "hashes": [ + "sha256:077ff8badf2acf8bc474406706ad890466274191a48d0abd3bd6987107c9cde5", + "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", + "sha256:27d498ffd1f21fb81d987a041c32d07857d1d107909f5134ba3350e1ce80a4af", + "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0", + "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd", + "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0", + "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7", + "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9", + "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f", + "sha256:d552c53d0415449c8d17ced5c341caba0d89dbf433698e1436c8fa0aae7808a3", + "sha256:f4510b93a3bec6eba8fd8f1093e9d7fb0d4a24d1a81377c10c0e5bbfe9e4ed24" + ], + "markers": "python_version >= '3.8'", + "version": "==4.25.8" + }, + "pulumi": { + "hashes": [ + "sha256:8f3ae0f23f077c9578e7bc3814b6c65cb7166bd3daa0a644513b80cb0e834e48" + ], + "markers": "python_version >= '3.9'", + "version": "==3.188.0" + }, + "pulumi-random": { + "hashes": [ + "sha256:99e121d79aec547287ed36769a36ae76f4cb0b47ad027d289018abbde3507579", + "sha256:c980a7d32b3b8f814545aa52ff49cae2640f9b4c47b10bf7ad748a17a0b85e58" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==4.18.3" + }, "python-dotenv": { "hashes": [ "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", @@ -350,6 +515,65 @@ "index": "pypi", "version": "==2025.2" }, + "pyyaml": { + "hashes": [ + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.2" + }, "requests": { "hashes": [ "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", @@ -359,6 +583,14 @@ "markers": "python_version >= '3.8'", "version": "==2.32.4" }, + "semver": { + "hashes": [ + "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", + "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602" + ], + "markers": "python_version >= '3.7'", + "version": "==3.0.4" + }, "sniffio": { "hashes": [ "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", diff --git a/infra/Pulumi.prod.yaml b/infra/Pulumi.prod.yaml index cae52a9..5dd4553 100644 --- a/infra/Pulumi.prod.yaml +++ b/infra/Pulumi.prod.yaml @@ -1,3 +1,4 @@ -encryptionsalt: v1:MpvbSqwJ0R0=:v1:Gb5Aayq2PEDs+22I:lFM2LBSteNXhvO4PjNRCgZ5h783eDQ== +encryptionsalt: v1:dPeFWTLi9AI=:v1:oVtFIvCQGewIBERG:46y/iE1CtLrjeTkc2SilpwEjfJg0iw== config: - gcp:project: osakunta-telegram-bot + gcp:region: europe-north1 + gcp:disableGlobalProjectWarning: "true" diff --git a/infra/__main__.py b/infra/__main__.py index b2f8c78..30b480c 100644 --- a/infra/__main__.py +++ b/infra/__main__.py @@ -1,90 +1,188 @@ import pulumi -import pulumi_gcp as gcp +import pulumi_gcp as gcp +import pulumi_random as random +import utils -# Be careful editing this file, if you are unfamiliar with Pulumi or the Google Cloud Platform. -# Make sure you read the README.md in the root of this repository first. +gcp_config = pulumi.Config("gcp") +LOCATION = gcp_config.require("region") +STACK_NAME = pulumi.get_stack() -# setup infrastructure +project_id = random.RandomPet("project-id", + length=2 +) -PROJECT_ID = "osakunta-telegram-bot" -LOCATION = "europe-north1" +project = gcp.organizations.Project("project", + name=f"Telegram Bot {STACK_NAME}", + project_id=project_id.id, + folder_id="452932952214" +) -# Set up secret to hold the Telegram API token -telegram_bot_token = gcp.secretmanager.Secret("telegram-bot-token", - secret_id="telegram-bot-token", +# Enable required services / APIs +secretmanager_service = gcp.projects.Service("secretmanager-service", + project=project_id.id, + service="secretmanager.googleapis.com", + disable_on_destroy=True +) + +cloudbuild_service = gcp.projects.Service("cloudbuild-service", + project=project_id.id, + service="cloudbuild.googleapis.com", + disable_on_destroy=True +) + +cloudrun_service = gcp.projects.Service("cloudrun-service", + project=project_id.id, + service="run.googleapis.com", + disable_on_destroy=True +) + +cloudfunctions_service = gcp.projects.Service("cloudfunctions-service", + project=project_id.id, + service="cloudfunctions.googleapis.com", + disable_on_destroy=True +) + +cloudresourcemanager_service = gcp.projects.Service("cloudresourcemanager-service", + project=project_id.id, + service="cloudresourcemanager.googleapis.com", + disable_on_destroy=True +) + + +# --- Set up github repo connection --- +github_token_secret = gcp.secretmanager.Secret("github-token-secret", + project=project_id.id, + secret_id="github-token", replication={ "user_managed": { "replicas": [{ "location": LOCATION }] } - } + }, + opts=pulumi.ResourceOptions( + depends_on=[secretmanager_service] + ) ) -# Set up a service account that has access to the secret, for the Function to use -service_account = gcp.serviceaccount.Account("service-account", - account_id="telegram-bot-service-account", - display_name="Telegram Bot Service Account") - -secret_access = gcp.secretmanager.SecretIamMember("secret-access", - secret_id=telegram_bot_token.id, +github_connection_service_account_secret_access = gcp.secretmanager.SecretIamMember("github-connection-service-account-secret-access", + project=project_id.id, + secret_id=github_token_secret.id, role="roles/secretmanager.secretAccessor", - member=service_account.email.apply(lambda email: f"serviceAccount:{email}") + member=pulumi.Output.concat( + "serviceAccount:service-", + project.number, + "@gcp-sa-cloudbuild.iam.gserviceaccount.com" + ), + opts=pulumi.ResourceOptions( + depends_on=[cloudbuild_service] + ) ) +github_connection = gcp.cloudbuildv2.Connection("github-connection", + project=project_id.id, + name="github-connection", + location=LOCATION, + github_config={ + "app_installation_id": 30357801, + "authorizer_credential": { + "oauth_token_secret_version": github_token_secret.name.apply(lambda name: f"{name}/versions/latest") + } + }, + opts=pulumi.ResourceOptions( + depends_on=[github_connection_service_account_secret_access] + ) +) -# Set up the source code -source_bucket = gcp.storage.Bucket("source-bucket", +github_repository = gcp.cloudbuildv2.Repository("github-repository", + project=project_id.id, + name="telegram-bot", location=LOCATION, - name=f"{PROJECT_ID}-source-bucket", + parent_connection=github_connection.name, + remote_uri="https://github.com/osakunta/telegram-bot.git", ) -source_asset = pulumi.AssetArchive({ - "telegram_bot": pulumi.FileArchive("../telegram_bot"), - "main.py": pulumi.FileAsset("../main.py"), - "requirements.txt": pulumi.FileAsset("../requirements.txt") -}) -source_object = gcp.storage.BucketObject("source-object", - bucket=source_bucket.name, - name="telegram-bot-source", - source=source_asset +# --- Set up CI/CD --- + +cicd_service_account = utils.service_account_with_roles( + "cicd-service-account", + [ + "roles/logging.logWriter", + "roles/cloudfunctions.developer", + "roles/iam.serviceAccountUser", + "roles/storage.objectViewer", + "roles/artifactregistry.writer" + ], + project=project_id.id, + account_id="cicd-service-account", + display_name="CICD Service Account" ) -# Set up the Function, which handles the requests -function = gcp.cloudfunctionsv2.Function("function", - location=LOCATION, - name="telegram-bot-function", - description="Cloud Run Function for handling telegram bot requests", - build_config={ - "runtime": "python313", - "entryPoint": "telegram_bot", - "source": { - "storage_source": { - "bucket": source_bucket.name, - "object": source_object.name, - "generation": source_object.generation - } +runtime_service_account = utils.service_account_with_roles( + "runtime-service-account", + [ "roles/iam.serviceAccountUser" ], + project=project_id.id, + account_id="runtime-service-account", + display_name="Function Runtime Service Account" +) + +telegram_bot_token = gcp.secretmanager.Secret("telegram-bot-token", + project=project_id.id, + secret_id="telegram-bot-token", + replication={ + "user_managed": { + "replicas": [{ "location": LOCATION }] } - }, - service_config={ - "availableMemory": "128Mi", - "maxInstanceCount": 1, # No need for more than one instance - "minInstanceCount": 0, # Important to allow scale-to-zero, to save costs - "service_account_email": service_account.email, - "ingressSettings": "ALLOW_ALL", - "secret_environment_variables": [{ - "key": "TOKEN", - "project_id": PROJECT_ID, - "secret": telegram_bot_token.secret_id, - "version": "latest" - }], - } + }, + opts=pulumi.ResourceOptions( + depends_on=[secretmanager_service] + ) ) -# Finally, set an IAM policy to allow unauthenticated people (anyone) to invoke the function -# this has to be cloudrun.ServiceIamMember instead of cloudfunctions.FunctionIamMember -# because the function is v2 -function_public_iam = gcp.cloudrunv2.ServiceIamMember("function-public-iam", +telegram_bot_token_secret_access = gcp.secretmanager.SecretIamMember("telegram-bot-token-secret-access", + project=project_id.id, + secret_id=telegram_bot_token.id, + role="roles/secretmanager.secretAccessor", + member=runtime_service_account.member, +) + +deploy_trigger = gcp.cloudbuild.Trigger("deploy-trigger", + project=project_id.id, + name="deploy", location=LOCATION, - name=function.name, - role="roles/run.invoker", - member="allUsers" + service_account=cicd_service_account.id, + repository_event_config={ + "repository": github_repository.id, + "push": { + "branch": f"^{STACK_NAME}$", + } + }, + build={ + "steps": [ + { + "name": "gcr.io/cloud-builders/gcloud", + "args": [ + "functions", "deploy", "telegram-bot", + "--region", LOCATION, + "--runtime", "python313", + "--entry-point", "telegram_bot", + "--trigger-http", + "--allow-unauthenticated", + "--timeout", "5s", + "--gen2", + "--max-instances", "1", + "--min-instances", "0", + "--memory", "128Mi", + "--set-secrets", telegram_bot_token.name.apply(lambda name: f"TOKEN={name}/versions/latest"), + "--source", ".", + "--run-service-account", runtime_service_account.email, + "--build-service-account", cicd_service_account.id, + ], + } + ], + "options": { + "logging": "CLOUD_LOGGING_ONLY" + } + }, + opts=pulumi.ResourceOptions( + depends_on=[ cloudrun_service, cloudfunctions_service, cloudresourcemanager_service ] + ) ) \ No newline at end of file diff --git a/infra/cicd.py b/infra/cicd.py new file mode 100644 index 0000000..231205f --- /dev/null +++ b/infra/cicd.py @@ -0,0 +1,106 @@ +import pulumi +import pulumi_gcp as gcp + +def setup_cicd(): + gcp_config = pulumi.Config("gcp") + LOCATION = gcp_config.require("region") + PROJECT_ID = gcp_config.require("project") + + config = pulumi.Config() + STACK_NAME = config.require("stack_name") + GITHUB_BRANCH = config.require("github_branch") + + + # Set up secret to hold the GitHub token + github_token_secret = gcp.secretmanager.Secret("github-token-secret", + secret_id="github-token", + replication={ + "user_managed": { + "replicas": [{ "location": LOCATION }] + } + } + ) + # Populate this manually + github_token_secret_version = gcp.secretmanager.get_secret_version(secret=github_token_secret.id) + + PROJECT_NUMBER = gcp.projects.get_project(filter=f"id:{PROJECT_ID}").projects[0].number + service_account_secret_access = gcp.secretmanager.SecretIamMember("service-account-secret-access", + secret_id=github_token_secret.id, + role="roles/secretmanager.secretAccessor", + member=f"serviceAccount:service-{PROJECT_NUMBER}@gcp-sa-cloudbuild.iam.gserviceaccount.com" + ) + + github_connection = gcp.cloudbuildv2.Connection("github-connection", + name="github-connection", + location=LOCATION, + github_config={ + "app_installation_id": 30357801, + "authorizer_credential": { + "oauth_token_secret_version": github_token_secret_version.id, + } + }, + opts=pulumi.ResourceOptions( + depends_on=[service_account_secret_access] + ) + ) + + github_repository = gcp.cloudbuildv2.Repository("github-repository", + name="telegram-bot", + location=LOCATION, + parent_connection=github_connection.name, + remote_uri="https://github.com/osakunta/telegram-bot.git", + ) + + cicd_service_account = gcp.serviceaccount.Account("cicd-service-account", + account_id="cicd-service-account", + display_name="CICD Service Account" + ) + + # Artefact Registry stores the Docker images built by Cloud Build + artefact_repository = gcp.artifactregistry.Repository("artefact-repository", + repository_id="telegram-bot", + location=LOCATION, + format="DOCKER", + description="Docker repository for the Telegram Bot", + labels={ + "osakunta": "telegram-bot" + } + ) + + # staging_build_trigger = gcp.cloudbuild.Trigger("staging-build-trigger", + # name="staging-build", + # location=LOCATION, + # repository_event_config={ + # "repository": github_repository.id, + # "push": { + # "branch": "^staging$", + # } + # } + # ) + + IMAGE_URL = artefact_repository.name.apply(lambda name: f"{LOCATION}-docker.pkg.dev/{PROJECT_ID}/{name}/telegram-bot:$COMMIT_SHA") + build_trigger = gcp.cloudbuild.Trigger("build-trigger", + name="prod-build", + location=LOCATION, + service_account=cicd_service_account.id, + repository_event_config={ + "repository": github_repository.id, + "push": { + "branch": "^main$", + } + }, + build={ + "images": [IMAGE_URL], + "steps": [{ + "name": "gcr.io/k8s-skaffold/pack", + "entrypoint": "pack", + "args": [ + "build", + IMAGE_URL, + "--builder", "gcr.io/buildpacks/builder:latest", + "--network", + "cloudbuild" + ], + }], + } + ) \ No newline at end of file diff --git a/infra/github.py b/infra/github.py new file mode 100644 index 0000000..e69de29 diff --git a/infra/project.py b/infra/project.py new file mode 100644 index 0000000..1560cf7 --- /dev/null +++ b/infra/project.py @@ -0,0 +1,91 @@ +import pulumi +import pulumi_gcp as gcp + +def setup_project(): + config = pulumi.Config("gcp") + LOCATION = config.require("region") + STACK_NAME = pulumi.get_stack() + + project = gcp.organizations.Project("project", + name=f"telegram-bot-{STACK_NAME}", + project_id=f"telegram-bot-{STACK_NAME}", + ) + + # # Set up secret to hold the Telegram API token + # telegram_bot_token = gcp.secretmanager.Secret("telegram-bot-token", + # secret_id="telegram-bot-token", + # replication={ + # "user_managed": { + # "replicas": [{ "location": LOCATION }] + # } + # } + # ) + + # # Set up a service account that has access to the secret, for the Function to use + # function_service_account = gcp.serviceaccount.Account("function-service-account", + # account_id="telegram-bot-service-account", + # display_name="Telegram Bot Service Account") + + # secret_access = gcp.secretmanager.SecretIamMember("secret-access", + # secret_id=telegram_bot_token.id, + # role="roles/secretmanager.secretAccessor", + # member=function_service_account.email.apply(lambda email: f"serviceAccount:{email}") + # ) + + # # Set up the source code + # source_bucket = gcp.storage.Bucket("source-bucket", + # location=LOCATION, + # name=f"{PROJECT_ID}-source-bucket", + # ) + + # source_asset = pulumi.AssetArchive({ + # "telegram_bot": pulumi.FileArchive("../telegram_bot"), + # "main.py": pulumi.FileAsset("../main.py"), + # "requirements.txt": pulumi.FileAsset("../requirements.txt") + # }) + # source_object = gcp.storage.BucketObject("source-object", + # bucket=source_bucket.name, + # name="telegram-bot-source", + # source=source_asset + # ) + + # # Set up the Function, which handles the requests + # function = gcp.cloudfunctionsv2.Function("function", + # location=LOCATION, + # name="telegram-bot-function", + # description="Cloud Run Function for handling telegram bot requests", + # build_config={ + # "runtime": "python313", + # "entryPoint": "telegram_bot", + # "source": { + # "storage_source": { + # "bucket": source_bucket.name, + # "object": source_object.name, + # "generation": source_object.generation + # } + # } + # }, + # service_config={ + # "availableMemory": "128Mi", + # "maxInstanceCount": 1, # No need for more than one instance + # "minInstanceCount": 0, # Important to allow scale-to-zero, to save costs + # "service_account_email": function_service_account.email, + # "ingressSettings": "ALLOW_ALL", + # "secret_environment_variables": [{ + # "key": "TOKEN", + # "project_id": PROJECT_ID, + # "secret": telegram_bot_token.secret_id, + # "version": "latest" + # }], + # } + # ) + + # # Finally, set an IAM policy to allow unauthenticated people (anyone) to invoke the function + # # this has to be cloudrun.ServiceIamMember instead of cloudfunctions.FunctionIamMember + # # because the function is v2 + # function_public_iam = gcp.cloudrunv2.ServiceIamMember("function-public-iam", + # location=LOCATION, + # name=function.name, + # role="roles/run.invoker", + # member="allUsers" + # ) \ No newline at end of file diff --git a/infra/utils.py b/infra/utils.py new file mode 100644 index 0000000..a36b799 --- /dev/null +++ b/infra/utils.py @@ -0,0 +1,15 @@ +import pulumi +import pulumi_gcp as gcp + +def service_account_with_roles(name, roles, **account_args): + account = gcp.serviceaccount.Account(name, **account_args) + for role in roles: + gcp.projects.IAMMember(f"{name}-role:{role}", + project=account.project, + role=role, + member=account.member, + opts=pulumi.ResourceOptions( + depends_on=[account] + ) + ) + return account From 486216ec7d4ab711ee0df3b6d407aec39f74deae Mon Sep 17 00:00:00 2001 From: Lennu Vuolanne Date: Fri, 15 Aug 2025 17:39:10 +0300 Subject: [PATCH 06/10] cleanup --- infra/cicd.py | 106 ----------------------------------------------- infra/github.py | 0 infra/project.py | 91 ---------------------------------------- 3 files changed, 197 deletions(-) delete mode 100644 infra/cicd.py delete mode 100644 infra/github.py delete mode 100644 infra/project.py diff --git a/infra/cicd.py b/infra/cicd.py deleted file mode 100644 index 231205f..0000000 --- a/infra/cicd.py +++ /dev/null @@ -1,106 +0,0 @@ -import pulumi -import pulumi_gcp as gcp - -def setup_cicd(): - gcp_config = pulumi.Config("gcp") - LOCATION = gcp_config.require("region") - PROJECT_ID = gcp_config.require("project") - - config = pulumi.Config() - STACK_NAME = config.require("stack_name") - GITHUB_BRANCH = config.require("github_branch") - - - # Set up secret to hold the GitHub token - github_token_secret = gcp.secretmanager.Secret("github-token-secret", - secret_id="github-token", - replication={ - "user_managed": { - "replicas": [{ "location": LOCATION }] - } - } - ) - # Populate this manually - github_token_secret_version = gcp.secretmanager.get_secret_version(secret=github_token_secret.id) - - PROJECT_NUMBER = gcp.projects.get_project(filter=f"id:{PROJECT_ID}").projects[0].number - service_account_secret_access = gcp.secretmanager.SecretIamMember("service-account-secret-access", - secret_id=github_token_secret.id, - role="roles/secretmanager.secretAccessor", - member=f"serviceAccount:service-{PROJECT_NUMBER}@gcp-sa-cloudbuild.iam.gserviceaccount.com" - ) - - github_connection = gcp.cloudbuildv2.Connection("github-connection", - name="github-connection", - location=LOCATION, - github_config={ - "app_installation_id": 30357801, - "authorizer_credential": { - "oauth_token_secret_version": github_token_secret_version.id, - } - }, - opts=pulumi.ResourceOptions( - depends_on=[service_account_secret_access] - ) - ) - - github_repository = gcp.cloudbuildv2.Repository("github-repository", - name="telegram-bot", - location=LOCATION, - parent_connection=github_connection.name, - remote_uri="https://github.com/osakunta/telegram-bot.git", - ) - - cicd_service_account = gcp.serviceaccount.Account("cicd-service-account", - account_id="cicd-service-account", - display_name="CICD Service Account" - ) - - # Artefact Registry stores the Docker images built by Cloud Build - artefact_repository = gcp.artifactregistry.Repository("artefact-repository", - repository_id="telegram-bot", - location=LOCATION, - format="DOCKER", - description="Docker repository for the Telegram Bot", - labels={ - "osakunta": "telegram-bot" - } - ) - - # staging_build_trigger = gcp.cloudbuild.Trigger("staging-build-trigger", - # name="staging-build", - # location=LOCATION, - # repository_event_config={ - # "repository": github_repository.id, - # "push": { - # "branch": "^staging$", - # } - # } - # ) - - IMAGE_URL = artefact_repository.name.apply(lambda name: f"{LOCATION}-docker.pkg.dev/{PROJECT_ID}/{name}/telegram-bot:$COMMIT_SHA") - build_trigger = gcp.cloudbuild.Trigger("build-trigger", - name="prod-build", - location=LOCATION, - service_account=cicd_service_account.id, - repository_event_config={ - "repository": github_repository.id, - "push": { - "branch": "^main$", - } - }, - build={ - "images": [IMAGE_URL], - "steps": [{ - "name": "gcr.io/k8s-skaffold/pack", - "entrypoint": "pack", - "args": [ - "build", - IMAGE_URL, - "--builder", "gcr.io/buildpacks/builder:latest", - "--network", - "cloudbuild" - ], - }], - } - ) \ No newline at end of file diff --git a/infra/github.py b/infra/github.py deleted file mode 100644 index e69de29..0000000 diff --git a/infra/project.py b/infra/project.py deleted file mode 100644 index 1560cf7..0000000 --- a/infra/project.py +++ /dev/null @@ -1,91 +0,0 @@ -import pulumi -import pulumi_gcp as gcp - -def setup_project(): - config = pulumi.Config("gcp") - LOCATION = config.require("region") - STACK_NAME = pulumi.get_stack() - - project = gcp.organizations.Project("project", - name=f"telegram-bot-{STACK_NAME}", - project_id=f"telegram-bot-{STACK_NAME}", - ) - - # # Set up secret to hold the Telegram API token - # telegram_bot_token = gcp.secretmanager.Secret("telegram-bot-token", - # secret_id="telegram-bot-token", - # replication={ - # "user_managed": { - # "replicas": [{ "location": LOCATION }] - # } - # } - # ) - - # # Set up a service account that has access to the secret, for the Function to use - # function_service_account = gcp.serviceaccount.Account("function-service-account", - # account_id="telegram-bot-service-account", - # display_name="Telegram Bot Service Account") - - # secret_access = gcp.secretmanager.SecretIamMember("secret-access", - # secret_id=telegram_bot_token.id, - # role="roles/secretmanager.secretAccessor", - # member=function_service_account.email.apply(lambda email: f"serviceAccount:{email}") - # ) - - # # Set up the source code - # source_bucket = gcp.storage.Bucket("source-bucket", - # location=LOCATION, - # name=f"{PROJECT_ID}-source-bucket", - # ) - - # source_asset = pulumi.AssetArchive({ - # "telegram_bot": pulumi.FileArchive("../telegram_bot"), - # "main.py": pulumi.FileAsset("../main.py"), - # "requirements.txt": pulumi.FileAsset("../requirements.txt") - # }) - # source_object = gcp.storage.BucketObject("source-object", - # bucket=source_bucket.name, - # name="telegram-bot-source", - # source=source_asset - # ) - - # # Set up the Function, which handles the requests - # function = gcp.cloudfunctionsv2.Function("function", - # location=LOCATION, - # name="telegram-bot-function", - # description="Cloud Run Function for handling telegram bot requests", - # build_config={ - # "runtime": "python313", - # "entryPoint": "telegram_bot", - # "source": { - # "storage_source": { - # "bucket": source_bucket.name, - # "object": source_object.name, - # "generation": source_object.generation - # } - # } - # }, - # service_config={ - # "availableMemory": "128Mi", - # "maxInstanceCount": 1, # No need for more than one instance - # "minInstanceCount": 0, # Important to allow scale-to-zero, to save costs - # "service_account_email": function_service_account.email, - # "ingressSettings": "ALLOW_ALL", - # "secret_environment_variables": [{ - # "key": "TOKEN", - # "project_id": PROJECT_ID, - # "secret": telegram_bot_token.secret_id, - # "version": "latest" - # }], - # } - # ) - - # # Finally, set an IAM policy to allow unauthenticated people (anyone) to invoke the function - # # this has to be cloudrun.ServiceIamMember instead of cloudfunctions.FunctionIamMember - # # because the function is v2 - # function_public_iam = gcp.cloudrunv2.ServiceIamMember("function-public-iam", - # location=LOCATION, - # name=function.name, - # role="roles/run.invoker", - # member="allUsers" - # ) \ No newline at end of file From 00e099fe5c6a2f6fbda813b7b20914b9d5352149 Mon Sep 17 00:00:00 2001 From: Lennu Vuolanne Date: Fri, 15 Aug 2025 17:41:24 +0300 Subject: [PATCH 07/10] infra update --- infra/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/__main__.py b/infra/__main__.py index 30b480c..e7c8bbd 100644 --- a/infra/__main__.py +++ b/infra/__main__.py @@ -107,6 +107,7 @@ [ "roles/logging.logWriter", "roles/cloudfunctions.developer", + "roles/run.admin", "roles/iam.serviceAccountUser", "roles/storage.objectViewer", "roles/artifactregistry.writer" From 5018afaa9bb5852291c8c240b8f4811c18a4c580 Mon Sep 17 00:00:00 2001 From: Lennu Vuolanne Date: Fri, 15 Aug 2025 20:43:01 +0300 Subject: [PATCH 08/10] infra update --- infra/__main__.py | 79 +++++++++++++++++++++++++++++++++++++++++------ infra/utils.py | 17 ++++++++++ 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/infra/__main__.py b/infra/__main__.py index e7c8bbd..5fd7e44 100644 --- a/infra/__main__.py +++ b/infra/__main__.py @@ -125,9 +125,12 @@ display_name="Function Runtime Service Account" ) -telegram_bot_token = gcp.secretmanager.Secret("telegram-bot-token", +# used by the cloud function to access telegram API +telegram_api_token = utils.secret_with_access( + "telegram-api-token", + members=[runtime_service_account.member, cicd_service_account.member], project=project_id.id, - secret_id="telegram-bot-token", + secret_id="telegram-api-token", replication={ "user_managed": { "replicas": [{ "location": LOCATION }] @@ -136,13 +139,22 @@ opts=pulumi.ResourceOptions( depends_on=[secretmanager_service] ) -) +) -telegram_bot_token_secret_access = gcp.secretmanager.SecretIamMember("telegram-bot-token-secret-access", +# telegram sends this alongside the updates to the bot, so that malicious actors cannot use the endpoint +telegram_webhook_token = utils.secret_with_access( + "telegram-webhook-token", + members=[runtime_service_account.member, cicd_service_account.member], project=project_id.id, - secret_id=telegram_bot_token.id, - role="roles/secretmanager.secretAccessor", - member=runtime_service_account.member, + secret_id="telegram-webhook-token", + replication={ + "user_managed": { + "replicas": [{ "location": LOCATION }] + } + }, + opts=pulumi.ResourceOptions( + depends_on=[secretmanager_service] + ) ) deploy_trigger = gcp.cloudbuild.Trigger("deploy-trigger", @@ -159,7 +171,9 @@ build={ "steps": [ { - "name": "gcr.io/cloud-builders/gcloud", + "id": "Deploy function", + "name": "gcr.io/google.com/cloudsdktool/cloud-sdk:slim", + "entrypoint": "gcloud", "args": [ "functions", "deploy", "telegram-bot", "--region", LOCATION, @@ -172,16 +186,61 @@ "--max-instances", "1", "--min-instances", "0", "--memory", "128Mi", - "--set-secrets", telegram_bot_token.name.apply(lambda name: f"TOKEN={name}/versions/latest"), + "--set-env-vars", "API_TOKEN=$$API_TOKEN,WEBHOOK_TOKEN=$$WEBHOOK_TOKEN", + "--clear-secrets", "--source", ".", "--run-service-account", runtime_service_account.email, "--build-service-account", cicd_service_account.id, ], + "secretEnv": [ + "API_TOKEN", + "WEBHOOK_TOKEN" + ] + }, + { + "id": "Get function URL", + "name": "gcr.io/google.com/cloudsdktool/cloud-sdk:slim", + "entrypoint": "bash", + "args": [ + "-c", + project_id.id.apply( + lambda id: + f"gcloud functions describe telegram-bot --region={LOCATION} --project={id} --format='value(url)' > /workspace/url.txt" + ) + ] + }, + { + "id": "Set webhook URL", + "name": "gcr.io/gcp-runtimes/ubuntu_20_0_4", + "entrypoint": "bash", + "args": [ + "-c", + "curl -X POST \ + -H \"Content-Type: application/json\" \ + -d \"{\\\"url\\\":\\\"$(cat /workspace/url.txt)\\\",\\\"secret_token\\\":\\\"$${WEBHOOK_TOKEN}\\\"}\" \ + \"https://api.telegram.org/bot$${API_TOKEN}/setWebhook\"" + ], + "secretEnv": [ + "API_TOKEN", + "WEBHOOK_TOKEN" + ] } ], "options": { "logging": "CLOUD_LOGGING_ONLY" - } + }, + "availableSecrets": { + "secretManager": [ + { + "env": "API_TOKEN", + "versionName": telegram_api_token.name.apply(lambda name: f"{name}/versions/latest"), + }, + { + "env": "WEBHOOK_TOKEN", + "versionName": telegram_webhook_token.name.apply(lambda name: f"{name}/versions/latest"), + } + ] + }, }, opts=pulumi.ResourceOptions( depends_on=[ cloudrun_service, cloudfunctions_service, cloudresourcemanager_service ] diff --git a/infra/utils.py b/infra/utils.py index a36b799..e601992 100644 --- a/infra/utils.py +++ b/infra/utils.py @@ -13,3 +13,20 @@ def service_account_with_roles(name, roles, **account_args): ) ) return account + + +def secret_with_access(name, members, **secret_args): + secret = gcp.secretmanager.Secret(name, **secret_args) + for member in members: + member.apply( + lambda m: gcp.secretmanager.SecretIamMember(f"{name}-access:{m}", + project=secret.project, + secret_id=secret.id, + role="roles/secretmanager.secretAccessor", + member=m, + opts=pulumi.ResourceOptions( + depends_on=[secret] + ) + ) + ) + return secret \ No newline at end of file From 0a2f09f74ba3a9ed7bd2a5cc1424bf86053ef885 Mon Sep 17 00:00:00 2001 From: Lennu Vuolanne Date: Fri, 15 Aug 2025 20:43:37 +0300 Subject: [PATCH 09/10] bot update --- main.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 3faac09..e5ecce1 100644 --- a/main.py +++ b/main.py @@ -23,7 +23,13 @@ def telegram_bot(request): level=logging.INFO ) - bot = telegram.Bot(token=os.getenv('TOKEN')) + # check the header for the secret token + secret_token = request.headers.get('X-Telegram-Bot-Api-Secret-Token') + if secret_token != os.getenv('WEBHOOK_TOKEN'): + logging.error("Invalid secret token") + return "Forbidden", 403 + + bot = telegram.Bot(token=os.getenv('API_TOKEN')) if request and request.method == "POST": try: @@ -31,8 +37,12 @@ def telegram_bot(request): command, args = parse_instructions(update) execute_bot_command(command, args, bot, update) + return "OK", 200 except Exception as e: logging.error(f"Error processing update: {e}") + return "Internal Server Error", 500 + + return "Invalid request", 400 # Used to test the bot on commandline by: python main.py /command [args] if __name__ == '__main__': From 197f2b9a71fc3ede26f9a8f859aadf9294f9487b Mon Sep 17 00:00:00 2001 From: Lennu Vuolanne Date: Fri, 15 Aug 2025 20:44:28 +0300 Subject: [PATCH 10/10] remove github actions --- .github/workflows/pull_request_app.yml | 25 --------------------- .github/workflows/pull_request_infra.yml | 28 ------------------------ 2 files changed, 53 deletions(-) delete mode 100644 .github/workflows/pull_request_app.yml delete mode 100644 .github/workflows/pull_request_infra.yml diff --git a/.github/workflows/pull_request_app.yml b/.github/workflows/pull_request_app.yml deleted file mode 100644 index 2a79823..0000000 --- a/.github/workflows/pull_request_app.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Pulumi -on: - pull_request: - paths-ignore: - - infra/** - - .github/workflows/pull_request_infra.yml -jobs: - preview: - name: Pulumi Preview - runs-on: ubuntu-latest - permissions: - contents: "read" - id-token: "write" - steps: - - uses: actions/checkout@v4 - - uses: "google-github-actions/auth@v2" - with: - project_id: "osakunta" - workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - - run: pip install -r requirements.txt - - uses: pulumi/actions@v6 - with: - command: preview - stack-name: prod - cloud-url: ${{ secrets.PULUMI_CLOUD_URL }} diff --git a/.github/workflows/pull_request_infra.yml b/.github/workflows/pull_request_infra.yml deleted file mode 100644 index 1c97720..0000000 --- a/.github/workflows/pull_request_infra.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Pulumi -on: - pull_request: - paths: - - infra/** - - .github/workflows/pull_request_infra.yml -jobs: - preview: - name: Pulumi Preview - runs-on: ubuntu-latest - permissions: - contents: "read" - id-token: "write" - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.13 - - uses: "google-github-actions/auth@v2" - with: - project_id: "osakunta-telegram-bot" - workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - - run: pip install -r requirements.txt - - uses: pulumi/actions@v6 - with: - command: preview - stack-name: prod - cloud-url: ${{ secrets.PULUMI_CLOUD_URL }}