Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initial version of gsm secret loader #213

Merged
merged 36 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
14abcf8
feat: initial version of gsm secret loader
DerTiedemann Sep 22, 2024
cff67c1
fix: dont use permissions
DerTiedemann Sep 22, 2024
4665755
fix: typo in project id fiedl
DerTiedemann Sep 22, 2024
ba3944b
fix: add gh provider
DerTiedemann Sep 22, 2024
51349ec
typo: export to env
DerTiedemann Sep 22, 2024
f4b4284
feat: add parse action
DerTiedemann Oct 7, 2024
dd13310
fix: plumbing to use new action
DerTiedemann Oct 7, 2024
c354ae5
change to new provider, cuz funny stuff happening at google
DerTiedemann Oct 9, 2024
55d6063
debug: add print env
DerTiedemann Oct 9, 2024
2a91156
fix: wrong assumption about variable names
DerTiedemann Oct 9, 2024
f33e919
fix: eof needs newline to be valid
DerTiedemann Oct 9, 2024
46bc4e5
fix: use different delimiter for nl delimited string for outputs
DerTiedemann Oct 9, 2024
0a974ba
fix: actually know pythin
DerTiedemann Oct 9, 2024
bc8eae6
fix: eof needs more newlines
DerTiedemann Oct 9, 2024
fb6867c
fix: add tests + delimiter global + removal of pycache from gitignore
DerTiedemann Oct 9, 2024
979f31a
feat: add typer and general improvements
DerTiedemann Oct 21, 2024
2e7ad13
ci: add testing for the parser
DerTiedemann Oct 21, 2024
9fada2e
simple docs
DerTiedemann Oct 21, 2024
d24de4d
docs: add hint to terraform
DerTiedemann Oct 21, 2024
e6b8946
fix: install typer
DerTiedemann Oct 22, 2024
50b7941
Update actions/load-secret-from-gsm/README.md
DerTiedemann Oct 29, 2024
da0dc23
Update actions/load-secret-from-gsm/README.md
DerTiedemann Oct 29, 2024
741f260
Update actions/load-secret-from-gsm/README.md
DerTiedemann Oct 29, 2024
8af4d45
Update actions/load-secret-from-gsm/README.md
DerTiedemann Oct 29, 2024
f89e898
Update actions/parse-secrets-definitions/main.py
DerTiedemann Oct 29, 2024
1ea06d3
fix: address review comments
DerTiedemann Oct 29, 2024
67543b4
fix: remove docs for test workflow
DerTiedemann Oct 29, 2024
0dc90bf
fix: formating
DerTiedemann Oct 29, 2024
b2b874b
fix: shellcheck lint
DerTiedemann Oct 29, 2024
409c181
fix: format
DerTiedemann Oct 29, 2024
90d13d4
fix: shellcheck
DerTiedemann Oct 29, 2024
1ae7318
Add symbolic links
DerTiedemann Nov 5, 2024
e525e60
Fix action yaml desc
DerTiedemann Nov 5, 2024
0f947d0
for now no new things to constrain
DerTiedemann Nov 5, 2024
de61ae6
fix symlinks
DerTiedemann Nov 6, 2024
0b3a092
fix: missing version
DerTiedemann Nov 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/_test-python-actions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Test python-setup-poetry action

on:
pull_request:
branches:
- main
paths:
- actions/parse-secret-definitions

jobs:
tests:
name: Python action unit tests
runs-on: ubuntu-latest
steps:
- uses: bakdata/ci-templates/actions/[email protected]
- name: setup python
uses: actions/setup-python@v2
with:
python-version: "3.12"
- name: Run all tests
run: |
find . -name "tests.py" -print0 | xargs -0i python -m unittest
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
tmp*
auto-doc*
./test*
**/__pycache__/
**/venv/
56 changes: 56 additions & 0 deletions actions/gcp-gsm-load-secrets/README.md
DerTiedemann marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# gcp-gsm-load-secrets

This action is set to replace GitHub actions integrated secret management.

## Usage

To load a secret from GSM figure out the following:

- check if the repository has access to the secret
- repository is owned by bakdata
- repository is private
- even if the labels are correctly set, you will need to run Terraform to set the proper roles
- use this template:

```yaml
- name: Load secrets
id: load-secrets
uses: bakdata/ci-templates/actions/gcp-gsm-load-secrets
with:
gke-project-name: <can be found from gcp console>
gke-project-id: <can be found from gcp console>
secrets-to-inject: |-
<secret_name>/<optional version, if not set the latest version is loaded>
<other_secret>/<optional version, if not set the latest version is loaded>
```

- it is possible to load multiple secrets in the same call
- loaded secrets will be injected as environment variables and the name will be cannonicalized to SCREAMING_SNAKE_CASE. Example: `i-like_trains__why_this?` -> `I_LIKE_TRAINS_WHY_THIS`

## References

### Inputs

<!-- AUTO-DOC-INPUT:START - Do not remove or modify this section -->

| INPUT | TYPE | REQUIRED | DEFAULT | DESCRIPTION |
| -------------------------- | ------ | -------- | -------- | --------------------------------------------- |
| export-to-environment | string | false | `"true"` | Export secrets to environment |
| gke-project-name | string | true | | GKE project name for authentication |
| gke-service-account | string | true | | GKE service account for authentication |
| secrets-to-inject | string | true | | Secrets to inject into the environment |
| workload-identity-provider | string | true | | Workload identity provider for authentication |

<!-- AUTO-DOC-INPUT:END -->

### Outputs

<!-- AUTO-DOC-OUTPUT:START - Do not remove or modify this section -->

| OUTPUT | TYPE | DESCRIPTION |
| ------- | ------ | ---------------------------------- |
| secrets | string | Secrets loaded from Secret Manager |

<!-- AUTO-DOC-OUTPUT:END -->

### Secrets
42 changes: 42 additions & 0 deletions actions/gcp-gsm-load-secrets/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: "Load secrets from Google Secret Manager"
description: "Load secrets from Google Secret Manager and inject them into the environment"
inputs:
gke-service-account:
description: "GKE service account for authentication"
required: true
gke-project-name:
description: "GKE project name for authentication"
required: true
workload-identity-provider:
description: "Workload identity provider for authentication"
required: true
secrets-to-inject:
description: "Secrets to inject into the environment"
required: true
export-to-environment:
description: "Export secrets to environment"
required: false
default: true
outputs:
secrets:
description: "Secrets loaded from Secret Manager"
value: ${{ steps.secrets.outputs.secrets }}
runs:
using: "composite"
steps:
- name: Authenticate at GCloud
uses: "google-github-actions/auth@v2"
with:
project_id: ${{ inputs.gke-project-name }}
workload_identity_provider: ${{ inputs.workload-identity-provider }}
service_account: ${{ inputs.gke-service-account }}
- id: "parse_secrets"
uses: "bakdata/ci-templates/actions/parse-secrets-definitions@tiedemann/gsm-secrets"
with:
project_name: ${{ inputs.gke-project-name }}
secrets_list: ${{ inputs.secrets-to-inject }}
- id: "secrets"
uses: "google-github-actions/get-secretmanager-secrets@v2"
with:
secrets: ${{ steps.parse_secrets.outputs.secrets-list }}
export_to_environment: ${{ inputs.export-to-environment }}
26 changes: 26 additions & 0 deletions actions/gcp-gsm-parse-secrets/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM python:3-slim AS builder
RUN pip install poetry==1.8.2

ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache

WORKDIR /app
COPY pyproject.toml poetry.lock ./
COPY main.py ./
RUN poetry install --no-root && rm -rf $POETRY_CACHE_DIR

# A distroless container image with Python and some basics like SSL certificates
# https://github.com/GoogleContainerTools/dis/i/itroless
FROM gcr.io/distroless/python3-debian12

ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"

COPY --from=builder /app /app
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}

WORKDIR /app
ENV PYTHONPATH /app
CMD ["/app/main.py"]
51 changes: 51 additions & 0 deletions actions/gcp-gsm-parse-secrets/README.md
DerTiedemann marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# gcp-gsm-parse-secrets

Converts a lists of strings of secrets references into screaming snake case. Look at the tests.py for furhter details.

### Inputs

<!-- AUTO-DOC-INPUT:START - Do not remove or modify this section -->

| INPUT | TYPE | REQUIRED | DEFAULT | DESCRIPTION |
| ------------ | ------ | -------- | ------- | --------------------------------------------- |
| project-name | string | true | | GKE project name where the secrets are stored |
| secrets-list | string | true | | Secrets to inject into the environment |

<!-- AUTO-DOC-INPUT:END -->

### Outputs

<!-- AUTO-DOC-OUTPUT:START - Do not remove or modify this section -->

| OUTPUT | TYPE | DESCRIPTION |
| ------------ | ------ | ------------------------------- |
| secrets-list | string | secret list with correct format |

<!-- AUTO-DOC-OUTPUT:END -->

### Secrets

## References

### Inputs

<!-- AUTO-DOC-INPUT:START - Do not remove or modify this section -->

| INPUT | TYPE | REQUIRED | DEFAULT | DESCRIPTION |
| ------------ | ------ | -------- | ------- | --------------------------------------------- |
| project-name | string | true | | GKE project name where the secrets are stored |
| secrets-list | string | true | | Secrets to inject into the environment |

<!-- AUTO-DOC-INPUT:END -->

### Outputs

<!-- AUTO-DOC-OUTPUT:START - Do not remove or modify this section -->

| OUTPUT | TYPE | DESCRIPTION |
| ------------ | ------ | ------------------------------- |
| secrets-list | string | secret list with correct format |

<!-- AUTO-DOC-OUTPUT:END -->

### Secrets
15 changes: 15 additions & 0 deletions actions/gcp-gsm-parse-secrets/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: "Parse secrets from GSM"
description: "Transform secrets into a common format"
inputs:
secrets-list:
description: "Secrets to inject into the environment"
required: true
project-name:
description: "GKE project name where the secrets are stored"
required: true
outputs:
secrets-list:
description: "secret list with correct format"
runs:
using: "docker"
image: "Dockerfile"
66 changes: 66 additions & 0 deletions actions/gcp-gsm-parse-secrets/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import os
import re

import typer
from typing_extensions import Annotated

DEFAULT_DELIMITER = "!!!"

# CAVEAT: will only work for one project at a time
# to add secrets form another project, invoke the action a second time with the other project name


# Set the output value by writing to the outputs in the Environment File, mimicking the behavior defined here:
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter
def set_github_action_output(output_name, output_value, delim):
if os.environ.get("GITHUB_ACTION"):
f = open(os.path.abspath(os.environ["GITHUB_OUTPUT"]), "a")
f.write(
f"{output_name}<<{delim}\n{output_value}{delim}\n"
) # ATTENTION: this might lead to problems if the output value contains the delimiter, which will not happen in this program but dont just copy this and expect it to work
f.close()
else:
print("would have set output", output_name, "to", output_value)


# removes special characters and replace with underscores, successive special characters are replaced with a single underscore
# convert to uppercase
# if the secret would end in an underscore, remove it
# format: SECRET_NAME:PROJECT_NAME/SECRET_NAME/VERSION
def parse_secret(secret, project_name, delim=DEFAULT_DELIMITER):
if delim in secret:
raise ValueError(f"Invalid secret definition: {delim} is a reserved keyword")
components = secret.split("/")

if len(components) > 2:
raise ValueError(
f"Invalid secret definition: {secret}, not in the format 'secret_name/<version>'"
)
secret_name = re.sub("[^0-9a-zA-Z]+", "_", components[0]).upper().rstrip("_")
if secret_name == "":
raise ValueError(
f"Invalid secret definition: {components[0]} is not a valid secret name"
)
out = f"{secret_name}:{project_name}/{components[0]}"
if len(components) == 2 and len(components[1]) != 0:
out += f"/{components[1]}"
return out


def main(
input_secrets: Annotated[str, typer.Argument(envvar="INPUT_SECRETS_LIST")],
gcp_project: Annotated[str, typer.Argument(envvar="INPUT_PROJECT_NAME")],
github_output_delimter: Annotated[str, typer.Argument()] = DEFAULT_DELIMITER,
):
# Deduplicate the input secrets
input_secrets = set(input_secrets.splitlines())

output = ""
for secret in input_secrets:
output += parse_secret(secret, gcp_project, github_output_delimter) + "\n"

set_github_action_output("secrets-list", output, github_output_delimter)


if __name__ == "__main__":
typer.run(main)
Loading
Loading