-
Notifications
You must be signed in to change notification settings - Fork 1
feat: update promotion scripts to accommodate Airflow v1 and v2 (for Airflow 3 upgrade) #454
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
Open
botanical
wants to merge
36
commits into
main
Choose a base branch
from
feat/airflow3-flag
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
cc23b0b
feat: create airflow api functions based on version
botanical 0794589
fix: functions and formatting
botanical d03fdc2
fix: update promotion scripts to use airflow functions
botanical e158b0b
fix: comment for airflows
botanical e891646
fix: modify based on feedback
botanical eb49ffe
feat: create workflow for testing
botanical fe5ccd5
feat: update env vars to emphasize testing
botanical eed6ca9
fix: add pythonpath to env
botanical 545ac9e
fix: add test collections
botanical 3bc9168
fix: add environment to pick up github envs
botanical 14f98f6
fix: add oauth2 support for aiflow 3
botanical c9538bd
fix: add keycloak env vars and call to token endpoint
botanical 31577a2
fix: typos and fail gh jobs if status code not 200
botanical 9580f88
fix: attempt to log error message for login
botanical 1f38629
fix: token endpoint
botanical 75820d5
fix: update grant type, remove username and password for oauth2
botanical 67758f4
fix: oauth2 headers
botanical 6546e82
fix: check keycloak status before extracting token
botanical 089ecdc
fix: logging, trigger another run
botanical b18635b
fix: logging for jq
botanical 0596b35
fix: linting issues
botanical 225bbdb
fix: consolidate logging
botanical 10a88af
fix: update to generate jwt token
botanical 4e281ed
fix: add pyjwt to requirements.txt
botanical eda3347
fix: add not before claim
botanical 3f986da
fix: add aud claim
botanical 46f7dba
fix: update audience
botanical a3eee71
fix: try using different audience
botanical a1f4787
fix: get sub from env vars
botanical 1bbb426
fix: pass in jwt sub
botanical 11776b3
fix: logical date setting
botanical b25f3e8
fix: add run id and fix call to build request body
botanical 3f50928
fix: cleanup commented out code
botanical 5087977
fix: update based on feedback and add some docs
botanical 46588f4
Apply batched suggestions from code review
botanical 714e57f
fix: update docs a bit
botanical File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| name: Test promotion script | ||
|
|
||
| on: | ||
| push: | ||
| branches: ['feat/airflow3-flag'] | ||
| paths: | ||
| # Run the workflow only if files inside this path are updated | ||
| - 'scripts/**' | ||
| - 'tests/**' | ||
| - '.github/workflows/test-promotion-script.yml' | ||
|
|
||
| jobs: | ||
| test-promotion-script: | ||
| runs-on: ubuntu-latest | ||
| environment: staging | ||
| steps: | ||
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 | ||
| with: | ||
| python-version: '3.9' | ||
|
|
||
| - name: Install dependencies | ||
| run: pip install -r ./scripts/requirements.txt | ||
|
|
||
| - name: Test promotion script (Airflow 3) | ||
| env: | ||
| STAGING_SM2A_API_URL: ${{ vars.TESTING_AIRFLOW3_SM2A_API_URL }} | ||
| STAGING_SM2A_ADMIN_USERNAME: ${{ secrets.TESTING_AIRFLOW3_SM2A_ADMIN_USERNAME}} | ||
| STAGING_SM2A_ADMIN_PASSWORD: ${{ secrets.TESTING_AIRFLOW3_SM2A_ADMIN_PASSWORD}} | ||
| STAGING_AIRFLOW_API_VERSION: "3" | ||
| AIRFLOW_JWT_SECRET: ${{ secrets.AIRFLOW_JWT_SECRET }} | ||
| AIRFLOW_JWT_SUB: ${{ secrets.AIRFLOW_JWT_SUB }} | ||
| PYTHONPATH: . | ||
| run: | | ||
| response=$(python3 scripts/promote_collection.py tests/test_collection.json staging) | ||
| status_code=$(echo "$response" | jq -r '.statusCode' | head -n1) | ||
| echo "Status Code: $status_code" | ||
|
|
||
| # Update status message based on response code | ||
| if [[ $status_code -eq 200 ]] || [[ $status_code -eq 201 ]]; then | ||
| echo "Airflow 3 test passed" | ||
| else | ||
| error_msg=$(echo "$response" | jq -r '.error') | ||
| echo "$collection_id failed to publish ❌" | ||
| echo "Error: $error_msg" | ||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Test promotion script (Airflow 2) | ||
| env: | ||
| STAGING_SM2A_API_URL: ${{ vars.TESTING_AIRFLOW2_SM2A_API_URL }} | ||
| STAGING_SM2A_ADMIN_USERNAME: ${{ secrets.TESTING_AIRFLOW2_SM2A_ADMIN_USERNAME}} | ||
| STAGING_SM2A_ADMIN_PASSWORD: ${{ secrets.TESTING_AIRFLOW2_SM2A_ADMIN_PASSWORD}} | ||
| STAGING_AIRFLOW_API_VERSION: "2" | ||
| PYTHONPATH: . | ||
| run: | | ||
| response=$(python3 scripts/promote_collection.py tests/test_collection_tenant.json staging) | ||
| status_code=$(echo "$response" | jq -r '.statusCode' | head -n1) | ||
| echo "Status Code: $status_code" | ||
|
|
||
| # Update status message based on response code | ||
| if [[ $status_code -eq 200 ]] || [[ $status_code -eq 201 ]]; then | ||
| echo "Airflow 2 test passed" | ||
| else | ||
| echo "$collection_id failed to publish ❌" | ||
| exit 1 | ||
| fi | ||
|
jjfrench marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
|
|
||
| # README | ||
|
|
||
| ## Environment Variables Reference for Collection and Dataset Promotion | ||
|
anayeaye marked this conversation as resolved.
|
||
|
|
||
| | Variable | Used by | Default | Notes | | ||
| | -- | -- | -- | -- | | ||
| | STAGING_AIRFLOW_API_VERSION | Staging Promotion | 2 | The Airflow Version for the Staging Environment. For Airflow 2 = API v1 + Basic auth, Airflow 3 = API v2 + JWT | | ||
| | PRODUCTION_AIRFLOW_API_VERSION | Production Promotion | 2 | Same as above | | ||
| | AIRFLOW_JWT_SECRET | Any Promotion step that uses Airflow 3 | none | Conditionally required when the resolved API version is 3. Must match the secret that the Airflow 3 deployment signs with. Can be found in Secrets Manager | | ||
| | AIRFLOW_JWT_SUB | Airflow 3 only | none | Conditionally required when the resolved API verison is 3. The subject claim for the token (the Integer ID from Airflow's user table) | | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| """ | ||
| Supports Airflow 2 and 3 (API v1 / v2) | ||
| """ | ||
|
|
||
| import os | ||
| import uuid | ||
| import json | ||
| import http.client | ||
|
|
||
| from typing import Dict, Any | ||
| from base64 import b64encode | ||
|
|
||
|
|
||
| class AirflowAPIError(Exception): | ||
| def __init__(self, msg, status_code=None): | ||
| super().__init__(msg) | ||
| self.status_code = status_code | ||
|
|
||
| pass | ||
|
|
||
|
|
||
| def _build_request_body( | ||
| conf: Dict[str, Any], dag_id: str, note: str = "", api_version: str = "2" | ||
| ) -> Dict[str, Any]: | ||
| """Build the request body for the DagRun""" | ||
| run_id = os.getenv("GITHUB_RUN_ID") | ||
| if run_id: | ||
| run_url = ( | ||
| f"{os.getenv('GITHUB_SERVER_URL')}/{os.getenv('GITHUB_REPOSITORY')}" | ||
| f"/actions/runs/{run_id}" | ||
| ) | ||
| default_note = f"Run from GitHub Actions: {run_url}" | ||
| else: | ||
| default_note = "Run from GitHub Actions veda-data workflow" | ||
|
|
||
| body = { | ||
| "conf": conf, | ||
| "dag_run_id": f"{dag_id}-{uuid.uuid4()}", | ||
| "note": note or default_note, | ||
| } | ||
|
botanical marked this conversation as resolved.
|
||
| if api_version != "2": | ||
| body["logical_date"] = None | ||
| return body | ||
|
|
||
|
|
||
| def _generate_jwt_token(secret: str, sub: str, expiration_time: int = 3600) -> str: | ||
| """Generate a HS512 JWT Token for Airflow API authentication""" | ||
| import jwt | ||
| import time | ||
|
|
||
| payload = { | ||
| "iss": "airflow", | ||
| "sub": sub, | ||
| "aud": "apache-airflow", | ||
| "nbf": int(time.time()), | ||
| "iat": int(time.time()), | ||
| "exp": int(time.time()) + expiration_time, | ||
| } | ||
| return jwt.encode(payload, secret, algorithm="HS512") | ||
|
|
||
|
|
||
| def trigger_dag_run( | ||
| base_api_url: str, | ||
| dag_id: str, | ||
| conf: Dict[str, Any], | ||
| username: str, | ||
| password: str, | ||
| api_version: str = None, | ||
| ) -> Dict[str, Any]: | ||
| """ | ||
| Trigger a DAG run with version-aware API handling | ||
|
|
||
| Raises: AirflowAPIError if it fails | ||
| """ | ||
|
|
||
| if api_version is None: | ||
| api_version = os.getenv("AIRFLOW_API_VERSION") or "2" | ||
|
|
||
| api_path = f"/api/v{'1' if api_version == '2' else '2'}/dags/{dag_id}/dagRuns" | ||
| request_body = _build_request_body(conf, dag_id, api_version=api_version) | ||
|
|
||
| if api_version == "2": | ||
| api_token = b64encode(f"{username}:{password}".encode()).decode() | ||
| headers = { | ||
| "Content-Type": "application/json", | ||
| "Authorization": "Basic " + api_token, | ||
| } | ||
| else: | ||
| jwt_secret = os.getenv("AIRFLOW_JWT_SECRET") | ||
| if not jwt_secret: | ||
| raise AirflowAPIError("AIRFLOW_JWT_SECRET environment variable not set") | ||
| jwt_sub = os.getenv("AIRFLOW_JWT_SUB") | ||
| if not jwt_sub: | ||
| raise AirflowAPIError("AIRFLOW_JWT_SUB environment variable not set") | ||
| access_token = _generate_jwt_token(jwt_secret, jwt_sub) | ||
| headers = { | ||
| "Content-Type": "application/json", | ||
| "Authorization": f"Bearer {access_token}", | ||
| } | ||
|
|
||
| try: | ||
| http_conn = http.client.HTTPSConnection(base_api_url) | ||
| http_conn.request("POST", api_path, json.dumps(request_body), headers) | ||
| response = http_conn.getresponse() | ||
| response_data = response.read() | ||
| http_conn.close() | ||
|
|
||
| if response.status >= 400: | ||
| raise AirflowAPIError( | ||
| f"Airflow API v{api_version} returns " | ||
| f"{response.status}: {response_data.decode()}", | ||
| status_code=response.status, | ||
| ) | ||
|
|
||
| return { | ||
| "statusCode": response.status, | ||
| "body": response_data.decode(), | ||
| } | ||
|
|
||
| except AirflowAPIError: | ||
| raise | ||
| except Exception as e: | ||
| raise AirflowAPIError(f"Failed to trigger the DAG run: {str(e)}") | ||
|
jjfrench marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can delete this file once this PR is reviewed!