From a54b424af09c52e9cc6c8db3f3519910a9ed37e4 Mon Sep 17 00:00:00 2001 From: Edmund Miller Date: Mon, 22 Sep 2025 17:02:12 +0200 Subject: [PATCH 1/4] fix: resolve S3 publishing failures and publishDir configuration casting errors - Add multipart upload permissions to S3 IAM policy for large files >5GB - Remove problematic closures from publishDir tags configuration - Update policy version hash to trigger compute environment recreation - Fixes workflow failures with S3 copy operations and casting errors --- .../seqerakit/configs/nextflow-arm.config | 17 + .../seqerakit/configs/nextflow-cpu.config | 17 + .../seqerakit/configs/nextflow-gpu.config | 18 + .../src/infrastructure/credentials.py | 508 ++++++++++++++++++ 4 files changed, 560 insertions(+) create mode 100644 pulumi/AWSMegatests/seqerakit/configs/nextflow-arm.config create mode 100644 pulumi/AWSMegatests/seqerakit/configs/nextflow-cpu.config create mode 100644 pulumi/AWSMegatests/seqerakit/configs/nextflow-gpu.config create mode 100644 pulumi/AWSMegatests/src/infrastructure/credentials.py diff --git a/pulumi/AWSMegatests/seqerakit/configs/nextflow-arm.config b/pulumi/AWSMegatests/seqerakit/configs/nextflow-arm.config new file mode 100644 index 00000000..6a944ba1 --- /dev/null +++ b/pulumi/AWSMegatests/seqerakit/configs/nextflow-arm.config @@ -0,0 +1,17 @@ +// Nextflow configuration for ARM CPU compute environments +// Includes base configuration and ARM-specific settings + +includeConfig 'nextflow-base.config' + +process { + publishDir = [ + path: { params.outdir }, + mode: 'copy', + tags: [ + 'compute_env': 'aws_ireland_fusionv2_nvme_cpu_ARM_snapshots', + 'architecture': 'arm64', + 'fusion': 'enabled', + 'fusionSnapshots': 'enabled' + ] + ] +} \ No newline at end of file diff --git a/pulumi/AWSMegatests/seqerakit/configs/nextflow-cpu.config b/pulumi/AWSMegatests/seqerakit/configs/nextflow-cpu.config new file mode 100644 index 00000000..de3a74b5 --- /dev/null +++ b/pulumi/AWSMegatests/seqerakit/configs/nextflow-cpu.config @@ -0,0 +1,17 @@ +// Nextflow configuration for CPU compute environments +// Includes base configuration and CPU-specific settings + +includeConfig 'nextflow-base.config' + +process { + publishDir = [ + path: { params.outdir }, + mode: 'copy', + tags: [ + 'compute_env': 'aws_ireland_fusionv2_nvme_cpu_snapshots', + 'architecture': 'x86_64', + 'fusion': 'enabled', + 'fusionSnapshots': 'enabled' + ] + ] +} \ No newline at end of file diff --git a/pulumi/AWSMegatests/seqerakit/configs/nextflow-gpu.config b/pulumi/AWSMegatests/seqerakit/configs/nextflow-gpu.config new file mode 100644 index 00000000..b9ca2330 --- /dev/null +++ b/pulumi/AWSMegatests/seqerakit/configs/nextflow-gpu.config @@ -0,0 +1,18 @@ +// Nextflow configuration for GPU compute environments +// Includes base configuration and GPU-specific settings + +includeConfig 'nextflow-base.config' + +process { + publishDir = [ + path: { params.outdir }, + mode: 'copy', + tags: [ + 'compute_env': 'aws_ireland_fusionv2_nvme_gpu_snapshots', + 'architecture': 'x86_64', + 'gpu': 'enabled', + 'fusion': 'enabled', + 'fusionSnapshots': 'enabled' + ] + ] +} \ No newline at end of file diff --git a/pulumi/AWSMegatests/src/infrastructure/credentials.py b/pulumi/AWSMegatests/src/infrastructure/credentials.py new file mode 100644 index 00000000..d133dec3 --- /dev/null +++ b/pulumi/AWSMegatests/src/infrastructure/credentials.py @@ -0,0 +1,508 @@ +"""TowerForge IAM credentials management module. + +This module creates and manages IAM resources for TowerForge AWS operations, +including policies for Forge operations, Launch operations, and S3 bucket access. +It also uploads the credentials to Seqera Platform for use by compute environments. +""" + +import json +import hashlib +from typing import Optional, Tuple, Dict, Any + +import pulumi +import pulumi_aws as aws +import pulumi_seqera as seqera + +from ..utils.constants import ( + TOWERFORGE_USER_NAME, + TOWERFORGE_POLICY_NAMES, + TOWERFORGE_CREDENTIAL_NAME, + TOWERFORGE_CREDENTIAL_DESCRIPTION, + TIMEOUTS, +) + + +class CredentialError(Exception): + """Exception raised when credential operations fail.""" + + pass + + +def _create_forge_policy_document() -> Dict[str, Any]: + """Create TowerForge Forge Policy document with comprehensive permissions.""" + return { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "TowerForge0", + "Effect": "Allow", + "Action": [ + "ssm:GetParameters", + "iam:CreateInstanceProfile", + "iam:DeleteInstanceProfile", + "iam:AddRoleToInstanceProfile", + "iam:RemoveRoleFromInstanceProfile", + "iam:CreateRole", + "iam:DeleteRole", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:DeleteRolePolicy", + "iam:PassRole", + "iam:TagRole", + "iam:TagInstanceProfile", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + "iam:GetRole", + "batch:CreateComputeEnvironment", + "batch:UpdateComputeEnvironment", + "batch:DeleteComputeEnvironment", + "batch:CreateJobQueue", + "batch:UpdateJobQueue", + "batch:DeleteJobQueue", + "batch:DescribeComputeEnvironments", + "batch:DescribeJobQueues", + "fsx:CreateFileSystem", + "fsx:DeleteFileSystem", + "fsx:DescribeFileSystems", + "fsx:TagResource", + "ec2:DescribeSecurityGroups", + "ec2:DescribeAccountAttributes", + "ec2:DescribeSubnets", + "ec2:DescribeLaunchTemplates", + "ec2:DescribeLaunchTemplateVersions", + "ec2:CreateLaunchTemplate", + "ec2:DeleteLaunchTemplate", + "ec2:DescribeKeyPairs", + "ec2:DescribeVpcs", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceTypeOfferings", + "ec2:GetEbsEncryptionByDefault", + "efs:CreateFileSystem", + "efs:DeleteFileSystem", + "efs:DescribeFileSystems", + "efs:CreateMountTarget", + "efs:DeleteMountTarget", + "efs:DescribeMountTargets", + "efs:ModifyFileSystem", + "efs:PutLifecycleConfiguration", + "efs:TagResource", + "elasticfilesystem:CreateFileSystem", + "elasticfilesystem:DeleteFileSystem", + "elasticfilesystem:DescribeFileSystems", + "elasticfilesystem:CreateMountTarget", + "elasticfilesystem:DeleteMountTarget", + "elasticfilesystem:DescribeMountTargets", + "elasticfilesystem:UpdateFileSystem", + "elasticfilesystem:PutLifecycleConfiguration", + "elasticfilesystem:TagResource", + ], + "Resource": "*", + }, + { + "Sid": "TowerLaunch0", + "Effect": "Allow", + "Action": [ + "s3:Get*", + "s3:List*", + "batch:DescribeJobQueues", + "batch:CancelJob", + "batch:SubmitJob", + "batch:ListJobs", + "batch:TagResource", + "batch:DescribeComputeEnvironments", + "batch:TerminateJob", + "batch:DescribeJobs", + "batch:RegisterJobDefinition", + "batch:DescribeJobDefinitions", + "ecs:DescribeTasks", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceAttribute", + "ecs:DescribeContainerInstances", + "ec2:DescribeInstanceStatus", + "ec2:DescribeImages", + "logs:Describe*", + "logs:Get*", + "logs:List*", + "logs:StartQuery", + "logs:StopQuery", + "logs:TestMetricFilter", + "logs:FilterLogEvents", + "ses:SendRawEmail", + "secretsmanager:ListSecrets", + ], + "Resource": "*", + }, + ], + } + + +def _create_launch_policy_document() -> Dict[str, Any]: + """Create TowerForge Launch Policy document with limited permissions.""" + return { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "TowerLaunch0", + "Effect": "Allow", + "Action": [ + "batch:DescribeJobQueues", + "batch:CancelJob", + "batch:SubmitJob", + "batch:ListJobs", + "batch:TagResource", + "batch:DescribeComputeEnvironments", + "batch:TerminateJob", + "batch:DescribeJobs", + "batch:RegisterJobDefinition", + "batch:DescribeJobDefinitions", + "ecs:DescribeTasks", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceAttribute", + "ecs:DescribeContainerInstances", + "ec2:DescribeInstanceStatus", + "ec2:DescribeImages", + "logs:Describe*", + "logs:Get*", + "logs:List*", + "logs:StartQuery", + "logs:StopQuery", + "logs:TestMetricFilter", + "logs:FilterLogEvents", + "ses:SendRawEmail", + "secretsmanager:ListSecrets", + ], + "Resource": "*", + } + ], + } + + +def _create_s3_policy_document(bucket_arn: str) -> Dict[str, Any]: + """Create S3 bucket access policy document with multipart upload support. + + Includes permissions for: + - Basic bucket operations (list, get location) + - Object operations (get, put, tag, delete) + - Multipart upload operations (for large files >5GB) + + Args: + bucket_arn: ARN of the S3 bucket to grant access to + + Returns: + Dict[str, Any]: S3 policy document with enhanced permissions + """ + return { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:ListBucket", + "s3:GetBucketLocation", + "s3:ListBucketMultipartUploads" + ], + "Resource": [bucket_arn], + }, + { + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectTagging", + "s3:DeleteObject", + "s3:AbortMultipartUpload", + "s3:ListMultipartUploadParts" + ], + "Resource": [f"{bucket_arn}/*"], + "Effect": "Allow", + }, + ], + } + + +def create_seqera_credentials( + seqera_provider: seqera.Provider, + workspace_id: float, + access_key_id: pulumi.Output[str], + access_key_secret: pulumi.Output[str], +) -> seqera.Credential: + """Upload TowerForge AWS credentials to Seqera Platform. + + Args: + seqera_provider: Configured Seqera provider instance + workspace_id: Seqera Platform workspace ID + access_key_id: AWS access key ID from TowerForge IAM user + access_key_secret: AWS secret access key from TowerForge IAM user + + Returns: + seqera.Credential: Seqera credential resource with credentials_id + + Raises: + CredentialError: If credential upload fails + """ + pulumi.log.info("Uploading TowerForge credentials to Seqera Platform") + + # Create AWS credentials configuration for Seqera Platform + aws_keys = seqera.CredentialKeysArgs( + aws=seqera.CredentialKeysAwsArgs( + access_key=access_key_id, + secret_key=access_key_secret, + # Note: assume_role_arn is optional and not needed for direct IAM user credentials + ) + ) + + # Upload credentials to Seqera Platform + seqera_credential = seqera.Credential( + "towerforge-aws-credential", + name=TOWERFORGE_CREDENTIAL_NAME, + description=TOWERFORGE_CREDENTIAL_DESCRIPTION, + provider_type="aws", + workspace_id=workspace_id, + keys=aws_keys, + opts=pulumi.ResourceOptions( + provider=seqera_provider, + # Ensure credentials are uploaded after IAM access key is created + custom_timeouts=pulumi.CustomTimeouts( + create=TIMEOUTS["seqera_credential_create"], + update=TIMEOUTS["seqera_credential_update"], + delete=TIMEOUTS["seqera_credential_delete"], + ), + ), + ) + + return seqera_credential + + +def _create_iam_policies( + aws_provider: aws.Provider, s3_bucket +) -> Tuple[aws.iam.Policy, aws.iam.Policy, aws.iam.Policy]: + """Create IAM policies for TowerForge operations. + + Args: + aws_provider: Configured AWS provider instance + s3_bucket: S3 bucket resource for policy attachment + + Returns: + Tuple of (forge_policy, launch_policy, s3_policy) + """ + # TowerForge Forge Policy - Comprehensive permissions for resource creation + forge_policy = aws.iam.Policy( + "towerforge-forge-policy", + name=TOWERFORGE_POLICY_NAMES["forge"], + description="IAM policy for TowerForge to create and manage AWS Batch resources", + policy=json.dumps(_create_forge_policy_document()), + opts=pulumi.ResourceOptions(provider=aws_provider), + ) + + # TowerForge Launch Policy - Limited permissions for pipeline execution + launch_policy = aws.iam.Policy( + "towerforge-launch-policy", + name=TOWERFORGE_POLICY_NAMES["launch"], + description="IAM policy for TowerForge to launch and monitor pipeline executions", + policy=json.dumps(_create_launch_policy_document()), + opts=pulumi.ResourceOptions(provider=aws_provider), + ) + + # TowerForge S3 Bucket Access Policy - Access to specified S3 bucket + s3_policy = aws.iam.Policy( + "towerforge-s3-policy", + name=TOWERFORGE_POLICY_NAMES["s3"], + description=s3_bucket.bucket.apply( + lambda bucket_name: f"IAM policy for TowerForge to access {bucket_name} S3 bucket" + ), + policy=s3_bucket.arn.apply( + lambda arn: json.dumps(_create_s3_policy_document(arn)) + ), + opts=pulumi.ResourceOptions(provider=aws_provider, depends_on=[s3_bucket]), + ) + + return forge_policy, launch_policy, s3_policy + + +def _generate_policy_hash( + forge_policy: aws.iam.Policy, + launch_policy: aws.iam.Policy, + s3_policy: aws.iam.Policy, +) -> str: + """Generate a hash of IAM policies to detect changes. + + Args: + forge_policy: TowerForge Forge policy + launch_policy: TowerForge Launch policy + s3_policy: TowerForge S3 policy + + Returns: + str: SHA256 hash of the combined policy documents + """ + # Create a deterministic hash of all policy documents + forge_doc = _create_forge_policy_document() + launch_doc = _create_launch_policy_document() + + # Combine all policy documents for hashing + combined_policies = json.dumps( + { + "forge": forge_doc, + "launch": launch_doc, + # Note: S3 policy is bucket-specific, so we'll use a placeholder for consistent hashing + # Version updated to include multipart upload permissions + "s3": {"bucket_dependent": True, "version": "v2-multipart"}, + }, + sort_keys=True, + ) + + return hashlib.sha256(combined_policies.encode()).hexdigest() + + +def create_towerforge_credentials( + aws_provider: aws.Provider, + s3_bucket, + seqera_provider: seqera.Provider, + workspace_id: float, +) -> Tuple[ + pulumi.Output[str], pulumi.Output[str], pulumi.Output[str], seqera.Credential, str +]: + """Create TowerForge IAM resources and upload to Seqera Platform. + + Creates IAM policies, user, and access keys for TowerForge operations, + then uploads the credentials to Seqera Platform for use by compute environments. + Based on https://github.com/seqeralabs/nf-tower-aws + + Args: + aws_provider: Configured AWS provider instance + s3_bucket: S3 bucket resource for policy attachment + seqera_provider: Configured Seqera provider instance + workspace_id: Seqera Platform workspace ID + + Returns: + Tuple: (access_key_id, access_key_secret, seqera_credentials_id, seqera_credential_resource, iam_policy_hash) + """ + # Create IAM policies + forge_policy, launch_policy, s3_policy = _create_iam_policies( + aws_provider, s3_bucket + ) + + # Generate policy version hash for compute environment recreation on policy changes + iam_policy_hash = _generate_policy_hash(forge_policy, launch_policy, s3_policy) + + # Create TowerForge IAM User + towerforge_user = aws.iam.User( + "towerforge-user", + name=TOWERFORGE_USER_NAME, + opts=pulumi.ResourceOptions(provider=aws_provider), + ) + + # Attach policies to the TowerForge user + forge_attachment = aws.iam.UserPolicyAttachment( + "towerforge-forge-policy-attachment", + user=towerforge_user.name, + policy_arn=forge_policy.arn, + opts=pulumi.ResourceOptions( + provider=aws_provider, depends_on=[towerforge_user, forge_policy] + ), + ) + + launch_attachment = aws.iam.UserPolicyAttachment( + "towerforge-launch-policy-attachment", + user=towerforge_user.name, + policy_arn=launch_policy.arn, + opts=pulumi.ResourceOptions( + provider=aws_provider, + depends_on=[towerforge_user, launch_policy], + ), + ) + + s3_attachment = aws.iam.UserPolicyAttachment( + "towerforge-s3-policy-attachment", + user=towerforge_user.name, + policy_arn=s3_policy.arn, + opts=pulumi.ResourceOptions( + provider=aws_provider, depends_on=[towerforge_user, s3_policy] + ), + ) + + # Create access keys for the TowerForge user + towerforge_access_key = aws.iam.AccessKey( + "towerforge-access-key", + user=towerforge_user.name, + opts=pulumi.ResourceOptions( + provider=aws_provider, + depends_on=[forge_attachment, launch_attachment, s3_attachment], + additional_secret_outputs=["secret"], + ), + ) + + # Upload the credentials to Seqera Platform + seqera_credential = create_seqera_credentials( + seqera_provider=seqera_provider, + workspace_id=workspace_id, + access_key_id=towerforge_access_key.id, + access_key_secret=towerforge_access_key.secret, + ) + + # Return the access key credentials, Seqera credentials ID, credential resource, and policy hash + return ( + towerforge_access_key.id, + towerforge_access_key.secret, + seqera_credential.credentials_id, + seqera_credential, + iam_policy_hash, + ) + + +def get_towerforge_resources( + aws_provider: aws.Provider, + s3_bucket, + seqera_provider: Optional[seqera.Provider] = None, + workspace_id: Optional[float] = None, +) -> Dict[str, Any]: + """Create TowerForge resources and return resource information for exports. + + This function creates the TowerForge IAM resources and returns a dictionary + containing resource information for Pulumi exports. + + Args: + aws_provider: Configured AWS provider instance + s3_bucket: S3 bucket resource for policy attachment + seqera_provider: Optional Seqera provider for credential upload + workspace_id: Optional workspace ID for Seqera Platform + + Returns: + Dict[str, Any]: Resource information for Pulumi exports + + Raises: + ValueError: If required parameters are missing + """ + # Create the credentials (this will create all the resources) + if seqera_provider and workspace_id: + ( + access_key_id, + access_key_secret, + seqera_credentials_id, + seqera_credential, + iam_policy_hash, + ) = create_towerforge_credentials( + aws_provider, s3_bucket, seqera_provider, workspace_id + ) + else: + # Fallback for backward compatibility - this will raise an error since signature changed + raise ValueError( + "get_towerforge_resources now requires seqera_provider and workspace_id parameters. " + "Please update your code to use the new signature or call create_towerforge_credentials directly." + ) + + return { + "user": { + "name": TOWERFORGE_USER_NAME, + "arn": f"arn:aws:iam::{{aws_account_id}}:user/{TOWERFORGE_USER_NAME}", # Will be populated by Pulumi + }, + "access_key_id": access_key_id, + "access_key_secret": access_key_secret, + "seqera_credentials_id": seqera_credentials_id, + "policies": { + "forge_policy_name": TOWERFORGE_POLICY_NAMES["forge"], + "launch_policy_name": TOWERFORGE_POLICY_NAMES["launch"], + "s3_policy_name": TOWERFORGE_POLICY_NAMES["s3"], + }, + } From 57ecf91947622691662b1877e072cedd187f0c0d Mon Sep 17 00:00:00 2001 From: Edmund Miller Date: Mon, 22 Sep 2025 17:02:12 +0200 Subject: [PATCH 2/4] init: Add co2-reporting project --- pulumi/co2_reports/.envrc | 22 ++ pulumi/co2_reports/.python-version | 1 + pulumi/co2_reports/Pulumi.AWSMegatests.yaml | 4 + pulumi/co2_reports/Pulumi.yaml | 11 + pulumi/co2_reports/README.md | 192 ++++++++++++++ pulumi/co2_reports/SETUP_INSTRUCTIONS.md | 39 +++ pulumi/co2_reports/__main__.py | 165 ++++++++++++ pulumi/co2_reports/pyproject.toml | 12 + pulumi/co2_reports/uv.lock | 266 ++++++++++++++++++++ 9 files changed, 712 insertions(+) create mode 100644 pulumi/co2_reports/.envrc create mode 100644 pulumi/co2_reports/.python-version create mode 100644 pulumi/co2_reports/Pulumi.AWSMegatests.yaml create mode 100644 pulumi/co2_reports/Pulumi.yaml create mode 100644 pulumi/co2_reports/README.md create mode 100644 pulumi/co2_reports/SETUP_INSTRUCTIONS.md create mode 100644 pulumi/co2_reports/__main__.py create mode 100644 pulumi/co2_reports/pyproject.toml create mode 100644 pulumi/co2_reports/uv.lock diff --git a/pulumi/co2_reports/.envrc b/pulumi/co2_reports/.envrc new file mode 100644 index 00000000..2744f12b --- /dev/null +++ b/pulumi/co2_reports/.envrc @@ -0,0 +1,22 @@ +# Environment configuration for CO2 Reports Pulumi project +# This file loads AWS credentials from 1Password + +export OP_ACCOUNT=nf-core + +# Load 1Password integration for direnv +source_url "https://github.com/tmatilai/direnv-1password/raw/v1.0.1/1password.sh" \ + "sha256-4dmKkmlPBNXimznxeehplDfiV+CvJiIzg7H1Pik4oqY=" + +# Load AWS credentials from 1Password +from_op AWS_ACCESS_KEY_ID="op://Dev/Pulumi-AWS-key/access key id" +from_op AWS_SECRET_ACCESS_KEY="op://Dev/Pulumi-AWS-key/secret access key" + +# AWS Configuration +export AWS_REGION="eu-north-1" +export AWS_DEFAULT_REGION="eu-north-1" + +# Load 1Password service account token for Pulumi +# from_op OP_SERVICE_ACCOUNT_TOKEN="op://Employee/doroenisttgrfcmzihhunyizg4/credential" + +# Load Pulumi passphrase from 1Password +from_op PULUMI_CONFIG_PASSPHRASE="op://Employee/Pulumi Passphrase/password" diff --git a/pulumi/co2_reports/.python-version b/pulumi/co2_reports/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/pulumi/co2_reports/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/pulumi/co2_reports/Pulumi.AWSMegatests.yaml b/pulumi/co2_reports/Pulumi.AWSMegatests.yaml new file mode 100644 index 00000000..05b0a63e --- /dev/null +++ b/pulumi/co2_reports/Pulumi.AWSMegatests.yaml @@ -0,0 +1,4 @@ +encryptionsalt: v1:Dd5GnLRGGJQ=:v1:HWX/n0HL3VxDJrWR:ouykGXCccBLXFd6kAKWW0uHByWK/qw== +config: + aws:region: eu-north-1 + github:owner: nf-core diff --git a/pulumi/co2_reports/Pulumi.yaml b/pulumi/co2_reports/Pulumi.yaml new file mode 100644 index 00000000..beab2319 --- /dev/null +++ b/pulumi/co2_reports/Pulumi.yaml @@ -0,0 +1,11 @@ +name: co2-reports +runtime: + name: python + options: + toolchain: uv + virtualenv: .venv +description: For hosting nf-core CO2 footprint reports +config: + pulumi:tags: + value: + pulumi:template: aws-python diff --git a/pulumi/co2_reports/README.md b/pulumi/co2_reports/README.md new file mode 100644 index 00000000..ae772bfb --- /dev/null +++ b/pulumi/co2_reports/README.md @@ -0,0 +1,192 @@ +# CO2 Reports - Pulumi Infrastructure + +Infrastructure-as-Code for the nf-core CO2 footprint reports S3 bucket using Pulumi. + +## Overview + +This Pulumi project creates and manages AWS infrastructure for storing CO2 footprint reports generated by nf-test runs in the nf-core/modules repository. + +**Created Resources:** + +- 📦 S3 bucket `nf-core-co2-reports` (eu-north-1) +- 👤 IAM user `nf-core-co2-reports-ci` with write access +- 🔑 GitHub Actions secrets for nf-core/modules repository + +## Quick Start + +### Prerequisites + +1. AWS credentials from 1Password (`Dev` vault → `Pulumi-AWS-key`) +2. 1Password service account token +3. uv installed (`brew install uv` or similar) + +### Deployment + +```bash +# Set AWS credentials +export AWS_ACCESS_KEY_ID="" +export AWS_SECRET_ACCESS_KEY="" +export AWS_REGION="eu-north-1" + +# Optional: Set 1Password token (or script will prompt) +export OP_SERVICE_ACCOUNT_TOKEN="" + +# Run deployment script +cd ~/src/nf-core/ops/pulumi/co2_reports +./DEPLOY.sh +``` + +### Manual Deployment + +If you prefer manual control: + +```bash +cd ~/src/nf-core/ops/pulumi/co2_reports + +# Initialize stack +uv run pulumi stack init AWSMegatests + +# Configure +uv run pulumi config set aws:region eu-north-1 +uv run pulumi config set github:owner nf-core +uv run pulumi config set pulumi-onepassword:service_account_token --secret + +# Deploy +uv run pulumi preview # Preview changes +uv run pulumi up # Deploy +``` + +## Infrastructure Details + +### S3 Bucket + +- **Name**: `nf-core-co2-reports` +- **Region**: `eu-north-1` +- **Encryption**: AES256 server-side encryption +- **Versioning**: Enabled +- **Public Access**: Blocked +- **Purpose**: Store CO2 footprint trace files from nf-test runs + +### Report Organization + +Reports are organized by: +``` +s3://nf-core-co2-reports/ + └── modules/ + └── YYYY-MM-DD/ + └── branch-name/ + └── profile/ + └── shard/ + ├── co2footprint_trace.txt + └── co2footprint_trace_*.txt +``` + +### IAM Permissions + +The CI user has permissions for: +- `s3:PutObject` - Upload reports +- `s3:GetObject` - Download reports (for verification) +- `s3:ListBucket` - List bucket contents +- `s3:GetBucketLocation` - Get bucket region + +### GitHub Secrets + +The following secrets are automatically created in the `nf-core/modules` repository: + +- `CO2_REPORTS_AWS_ACCESS_KEY_ID` +- `CO2_REPORTS_AWS_SECRET_ACCESS_KEY` +- `CO2_REPORTS_AWS_REGION` + +## Usage in GitHub Actions + +After deployment, update your GitHub workflow: + +```yaml +- name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.CO2_REPORTS_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.CO2_REPORTS_AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.CO2_REPORTS_AWS_REGION }} + +- name: Upload CO2 footprint reports to S3 + run: | + aws s3 cp co2footprint_trace.txt s3://nf-core-co2-reports/... +``` + +## Management + +### View Stack Outputs + +```bash +cd ~/src/nf-core/ops/pulumi/co2_reports +uv run pulumi stack output +``` + +### Update Infrastructure + +```bash +# Make changes to __main__.py +# Preview changes +uv run pulumi preview + +# Apply changes +uv run pulumi up +``` + +### Destroy Infrastructure + +```bash +# ⚠️ WARNING: This will delete the bucket and all reports! +uv run pulumi destroy +``` + +## Project Structure + +``` +co2_reports/ +├── Pulumi.yaml # Project configuration +├── __main__.py # Infrastructure definition +├── pyproject.toml # Python dependencies +├── DEPLOY.sh # Deployment script +├── README.md # This file +└── .venv/ # Virtual environment +``` + +## Troubleshooting + +### Error: "operation error S3: GetObject" + +This means Pulumi can't access the S3 backend. Ensure AWS credentials are set: + +```bash +export AWS_ACCESS_KEY_ID="" +export AWS_SECRET_ACCESS_KEY="" +``` + +### Error: "No such bookmark: AWSMegatests" + +The stack hasn't been initialized yet. Run: + +```bash +uv run pulumi stack init AWSMegatests +``` + +### GitHub Secrets Not Created + +Check that: +1. The 1Password service account token is correct +2. The GitHub token in 1Password has proper permissions +3. The repository name is correct (`modules`, not `nf-core/modules`) + +## Related Documentation + +- [nf-co2footprint Plugin](https://github.com/nextflow-io/nf-co2footprint) +- [GitHub Issue #9291](https://github.com/nf-core/modules/issues/9291) +- [Pulumi AWS Documentation](https://www.pulumi.com/registry/packages/aws/) + +## Support + +For issues or questions: +- Open an issue in [nf-core/modules](https://github.com/nf-core/modules/issues) +- Contact the nf-core infrastructure team diff --git a/pulumi/co2_reports/SETUP_INSTRUCTIONS.md b/pulumi/co2_reports/SETUP_INSTRUCTIONS.md new file mode 100644 index 00000000..00020941 --- /dev/null +++ b/pulumi/co2_reports/SETUP_INSTRUCTIONS.md @@ -0,0 +1,39 @@ +# CO2 Reports Pulumi Setup Instructions + +## Commands to run: + +```bash +# 1. Initialize the stack +cd ~/src/nf-core/ops/pulumi/co2_reports +uv run pulumi stack init AWSMegatests + +# 2. Configure the stack +uv run pulumi config set aws:region us-east-1 +uv run pulumi config set github:owner nf-core + +# 3. Set the 1Password service account token (get this from 1Password) +uv run pulumi config set pulumi-onepassword:service_account_token --secret + +# 4. Preview the changes +uv run pulumi preview + +# 5. Deploy the infrastructure +uv run pulumi up + +# 6. Verify the GitHub secrets were created +# Check in GitHub: https://github.com/nf-core/modules/settings/secrets/actions +``` + +## What this will create: + +1. S3 bucket: `nf-core-co2-reports` +2. IAM user: `nf-core-co2-reports-ci` +3. IAM policy for write access to the bucket +4. GitHub Actions secrets in nf-core/modules: + - CO2_REPORTS_AWS_ACCESS_KEY_ID + - CO2_REPORTS_AWS_SECRET_ACCESS_KEY + - CO2_REPORTS_AWS_REGION + +## After deployment: + +Update the GitHub workflow in nf-core/modules to use the new bucket and credentials. diff --git a/pulumi/co2_reports/__main__.py b/pulumi/co2_reports/__main__.py new file mode 100644 index 00000000..b05a0aeb --- /dev/null +++ b/pulumi/co2_reports/__main__.py @@ -0,0 +1,165 @@ +"""CO2 Reports S3 Infrastructure Management + +A Pulumi program for managing the nf-core CO2 footprint reports S3 bucket. + +This project creates and manages AWS infrastructure for storing CO2 footprint +tracking data from nf-test runs in the nf-core/modules repository. +""" + +import pulumi +import pulumi_aws as aws +import pulumi_github as github +import pulumi_onepassword as onepassword + +# Configure the 1Password provider +onepassword_config = pulumi.Config("pulumi-onepassword") +onepassword_provider = onepassword.Provider( + "onepassword-provider", + service_account_token=onepassword_config.require_secret("service_account_token"), +) + +# Get GitHub token from 1Password +github_token_item = onepassword.get_item_output( + vault="Dev", + title="GitHub nf-core PA Token Pulumi", + opts=pulumi.InvokeOptions(provider=onepassword_provider), +) + +# Configure AWS provider +aws_provider = aws.Provider("aws-provider") + +# Configure GitHub provider +github_provider = github.Provider( + "github-provider", + token=github_token_item.credential, + owner=pulumi.Config("github").get("owner"), +) + +# Create S3 bucket for CO2 reports +co2_reports_bucket = aws.s3.Bucket( + "co2-reports-bucket", + bucket="nf-core-co2-reports", + server_side_encryption_configuration=aws.s3.BucketServerSideEncryptionConfigurationArgs( + rule=aws.s3.BucketServerSideEncryptionConfigurationRuleArgs( + apply_server_side_encryption_by_default=aws.s3.BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefaultArgs( + sse_algorithm="AES256", + ), + ), + ), + versioning=aws.s3.BucketVersioningArgs( + enabled=True, + ), + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Block public access to the bucket +co2_reports_bucket_public_access_block = aws.s3.BucketPublicAccessBlock( + "co2-reports-bucket-public-access-block", + bucket=co2_reports_bucket.id, + block_public_acls=True, + block_public_policy=True, + ignore_public_acls=True, + restrict_public_buckets=True, + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Create IAM user for CI/CD +ci_user = aws.iam.User( + "co2-reports-ci-user", + name="nf-core-co2-reports-ci", + path="/", + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Create access keys for the CI user +ci_user_access_key = aws.iam.AccessKey( + "co2-reports-ci-access-key", + user=ci_user.name, + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Create IAM policy for bucket access +bucket_access_policy = aws.iam.Policy( + "co2-reports-bucket-access-policy", + name="nf-core-co2-reports-bucket-access", + description="Write access to nf-core-co2-reports S3 bucket for CI/CD", + policy=co2_reports_bucket.arn.apply( + lambda bucket_arn: f"""{{ + "Version": "2012-10-17", + "Statement": [ + {{ + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:GetObject", + "s3:ListBucket", + "s3:GetBucketLocation" + ], + "Resource": [ + "{bucket_arn}", + "{bucket_arn}/*" + ] + }} + ] + }}""" + ), + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Attach policy to CI user +aws.iam.UserPolicyAttachment( + "ci-user-bucket-policy-attachment", + user=ci_user.name, + policy_arn=bucket_access_policy.arn, + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Create GitHub Actions secrets for the modules repository +aws_access_key_secret = github.ActionsSecret( + "co2-reports-aws-access-key-id-secret", + repository="modules", + secret_name="CO2_REPORTS_AWS_ACCESS_KEY_ID", + plaintext_value=ci_user_access_key.id, + opts=pulumi.ResourceOptions(provider=github_provider), +) + +aws_secret_access_key_secret = github.ActionsSecret( + "co2-reports-aws-secret-access-key-secret", + repository="modules", + secret_name="CO2_REPORTS_AWS_SECRET_ACCESS_KEY", + plaintext_value=ci_user_access_key.secret, + opts=pulumi.ResourceOptions(provider=github_provider), +) + +aws_region_secret = github.ActionsSecret( + "co2-reports-aws-region-secret", + repository="modules", + secret_name="CO2_REPORTS_AWS_REGION", + plaintext_value=pulumi.Config("aws").get("region") or "us-east-1", + opts=pulumi.ResourceOptions(provider=github_provider), +) + +# Exports +pulumi.export("bucket_name", co2_reports_bucket.bucket) +pulumi.export("bucket_arn", co2_reports_bucket.arn) +pulumi.export("ci_user_name", ci_user.name) +pulumi.export("ci_user_access_key_id", ci_user_access_key.id) +pulumi.export( + "ci_user_secret_access_key", pulumi.Output.secret(ci_user_access_key.secret) +) +pulumi.export( + "github_secrets_created", + { + "CO2_REPORTS_AWS_ACCESS_KEY_ID": aws_access_key_secret.secret_name, + "CO2_REPORTS_AWS_SECRET_ACCESS_KEY": aws_secret_access_key_secret.secret_name, + "CO2_REPORTS_AWS_REGION": aws_region_secret.secret_name, + }, +) +pulumi.export( + "usage", + { + "s3_uri": "s3://nf-core-co2-reports/", + "upload_path_pattern": "s3://nf-core-co2-reports/modules/YYYY-MM-DD/branch-name/profile/shard/", + }, +) diff --git a/pulumi/co2_reports/pyproject.toml b/pulumi/co2_reports/pyproject.toml new file mode 100644 index 00000000..a97a50f9 --- /dev/null +++ b/pulumi/co2_reports/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "co2-reports" +version = "0.1.0" +description = "Pulumi infrastructure for nf-core CO2 reports" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "pulumi>=3.205.0", + "pulumi-aws>=7.10.0", + "pulumi-github>=6.8.0", + "pulumi-onepassword>=1.1.3", +] diff --git a/pulumi/co2_reports/uv.lock b/pulumi/co2_reports/uv.lock new file mode 100644 index 00000000..6691a242 --- /dev/null +++ b/pulumi/co2_reports/uv.lock @@ -0,0 +1,266 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "arpeggio" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/58/ba011f3cf8291804ce80f9d81289ac15f0319a27f9d7e3c124aa5e4981cc/Arpeggio-2.0.3.tar.gz", hash = "sha256:9e85ad35cfc6c938676817c7ae9a1000a7c72a34c71db0c687136c460d12b85e", size = 766566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/4d/53b8186b41842f7a5e971b1d1c28e678364dcf841e4170f5d14d38ac1e2a/Arpeggio-2.0.3-py2.py3-none-any.whl", hash = "sha256:9374d9c531b62018b787635f37fd81c9a6ee69ef2d28c5db3cd18791b1f7db2f", size = 54656 }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, +] + +[[package]] +name = "co2-reports" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pulumi" }, + { name = "pulumi-aws" }, + { name = "pulumi-github" }, + { name = "pulumi-onepassword" }, +] + +[package.metadata] +requires-dist = [ + { name = "pulumi", specifier = ">=3.205.0" }, + { name = "pulumi-aws", specifier = ">=7.10.0" }, + { name = "pulumi-github", specifier = ">=6.8.0" }, + { name = "pulumi-onepassword", specifier = ">=1.1.3" }, +] + +[[package]] +name = "debugpy" +version = "1.8.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522 }, + { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417 }, + { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130 }, + { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053 }, + { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386 }, + { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100 }, + { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002 }, + { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047 }, + { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899 }, + { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254 }, + { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203 }, + { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493 }, + { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210 }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668 }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718 }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627 }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167 }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267 }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963 }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484 }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777 }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014 }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750 }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003 }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716 }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522 }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558 }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990 }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387 }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668 }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928 }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983 }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727 }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799 }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417 }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219 }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826 }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550 }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564 }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236 }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795 }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214 }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961 }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462 }, +] + +[[package]] +name = "parver" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arpeggio" }, + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/e5/1c774688a90f0b76e872e30f6f1ba3f5e14056cd0d96a684047d4a986226/parver-0.5.tar.gz", hash = "sha256:b9fde1e6bb9ce9f07e08e9c4bea8d8825c5e78e18a0052d02e02bf9517eb4777", size = 26908 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/4c/f98024021bef4d44dce3613feebd702c7ad8883f777ff8488384c59e9774/parver-0.5-py3-none-any.whl", hash = "sha256:2281b187276c8e8e3c15634f62287b2fb6fe0efe3010f739a6bd1e45fa2bf2b2", size = 15172 }, +] + +[[package]] +name = "pip" +version = "25.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622 }, +] + +[[package]] +name = "protobuf" +version = "5.29.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963 }, + { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818 }, + { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091 }, + { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824 }, + { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942 }, + { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823 }, +] + +[[package]] +name = "pulumi" +version = "3.205.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "debugpy" }, + { name = "dill" }, + { name = "grpcio" }, + { name = "pip" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "semver" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/71/ad01388f8fb1729fdef1d01ec4cb8e313a19f99ce6b3dccf243ea72c2239/pulumi-3.205.0-py3-none-any.whl", hash = "sha256:9c2469606e978c6694c5a656bd9d986e940076710e607786e1c9d6e5cbd4b81f", size = 383509 }, +] + +[[package]] +name = "pulumi-aws" +version = "7.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parver" }, + { name = "pulumi" }, + { name = "semver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/d5/2152cca6b51b0ed47b375ae4e306e25bdbfc93c797344c4e0975f2a29e22/pulumi_aws-7.10.0.tar.gz", hash = "sha256:07112752c2533d6b7c1fa505a9e3bd39036c8738f48f6dcf98be02026627fe5c", size = 8322711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/c7/5969b9b16dc4761eecc6abe23d983745a5de1431821e6317673e87768710/pulumi_aws-7.10.0-py3-none-any.whl", hash = "sha256:3bc541f1a065df5198ceca51379bf97090984f28a6ea0356e04f5be30e885ad0", size = 11293100 }, +] + +[[package]] +name = "pulumi-github" +version = "6.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parver" }, + { name = "pulumi" }, + { name = "semver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/06/8c428822ebc00e8726f3b3ce392784b27253688a44506824c4c4046f3c0f/pulumi_github-6.8.0.tar.gz", hash = "sha256:16722f99775e1ab1761b602e99f327adb76396c58c95a5cbac11845546b7c8bc", size = 214693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/72/7c3cf7d0ba9193429863bbfe265154d00d6e5d7a92df1baedaae59745fe0/pulumi_github-6.8.0-py3-none-any.whl", hash = "sha256:be4ce509438dd08ffffe274036fd6f13c1d95acc8604d62a8c26defaac63600b", size = 402071 }, +] + +[[package]] +name = "pulumi-onepassword" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parver" }, + { name = "pulumi" }, + { name = "semver" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/e3/15ba07147bf1ad602cd6234bb5ff09c29f9206d5ee0bf6845657c593264a/pulumi_onepassword-1.1.3.tar.gz", hash = "sha256:2ebbefa9463d8a9c6faf65491cad418c186d3a604360e78eaa319ef09c2478ba", size = 17968 } + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +] + +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] From fd565560ecbbfca56ac168e77be19e331f8255aa Mon Sep 17 00:00:00 2001 From: Edmund Miller Date: Wed, 29 Oct 2025 16:41:34 +0100 Subject: [PATCH 3/4] fix(pulumi): resolve 1Password config conflict and S3 deprecation warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add account="" to 1Password provider to prevent CLI account detection conflict - Refactor S3 bucket to use separate BucketServerSideEncryptionConfigurationV2 resource - Refactor S3 bucket to use separate BucketVersioningV2 resource - Remove deprecated inline server_side_encryption_configuration parameter - Remove deprecated inline versioning parameter Fixes "Config conflict: serviceAccountToken and account are set" error by explicitly disabling account detection when using service account tokens. Resolves AWS deprecation warnings by moving encryption and versioning configuration to dedicated resources as recommended by AWS provider. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pulumi/co2_reports/Pulumi.AWSMegatests.yaml | 2 ++ pulumi/co2_reports/__main__.py | 27 ++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/pulumi/co2_reports/Pulumi.AWSMegatests.yaml b/pulumi/co2_reports/Pulumi.AWSMegatests.yaml index 05b0a63e..d980b594 100644 --- a/pulumi/co2_reports/Pulumi.AWSMegatests.yaml +++ b/pulumi/co2_reports/Pulumi.AWSMegatests.yaml @@ -2,3 +2,5 @@ encryptionsalt: v1:Dd5GnLRGGJQ=:v1:HWX/n0HL3VxDJrWR:ouykGXCccBLXFd6kAKWW0uHByWK/ config: aws:region: eu-north-1 github:owner: nf-core + pulumi-onepassword:service_account_token: + secure: v1:/9UqNK7J/BSgkoof:PLSCC8wOdrIkvfK0pdodnyOid/HSwFb5Ra6+x0zVOiN+1ZbulPub0T1tRTdyCG4yqJa8dvcnS62kPRu9CCm10tPu+/g0HCVhUaR8DLB5lO5HtWn/OA28xd7OluDhp5e2JRYFV7HWUpnfTV9rlwpxntWAaCnnsSIw53yyiQLl0cMnf/V51VOo+e2YkqZJg/fyPtpeVS28FRZvjxFUIV5nxtaCw7K/5kXhtJOf6ZR160sH9bkkhNzzqQQ53JLY02rgFqBLAQ5dPRZMH6cB6HJiznq/zgEo2331/M6+TCqzyCi1spIvfhC7i3GV1KtVNaKJSD8tVEcscHJHR4hFElsJ5yHVVT/BWYL/A/4QjktY/hRKheDeLbQfkL5zQzbpm8DkNEwBv/x+691uhAJy387nK8+MdACLjT4w5Z7kmn4S+LOi8bPIXvTdpsWdijwT9Rbv4rr7p25nhoB4T+/ikDXWGSW/Lj5eVgcKVZ8KcY2MhhYGQ/LItWvrPO4K3kjZnglOjzU3CX5wuCrki4uHhKGgpWT+/XTgSe4nerp9iFZPs42yneEMz1icYfPkXOybDuV+4TPB2rn7g1Yp1k2+1ILe8aw82Pzw2DEDcFjyVTTHsCytBGKFlOT/MxWmgiY8BCJvEBLzOrDPHqfWFTeyIVdVw62nnFMZwVoDxl68aCGbAWCsBr2sS3uTZAak7RP5cXYa9zEIb6d4dQ6PiUl/V6q4yi+yawde7F7St8qd+MTBvBgfieEXwzwwejb2IGmkI6uKD2mSOUk+Dyb4DymuuYatFf6QUlCQEBlyaLZmA1SIGSMrASKmY54VffU6C4LLZvQc8Sxa20wzuYIQ+RO/o2EGQ2Kq9b2teOVMah6kWHVFrNA46+3cNmUSMXxuoiyjsQo0KMzWY8ZpJ2qXDhCJLYdlsVKY4W3i2rS4smjGbHxpnhC6sUgH3LaJBZFHWk+OGAI35y1AVocquUzipCsJGrMD9SiZla6mOKD9Cwv3kdRQQFbSdmH1hF829lBk6o+uXrhpfxHiyD+GgsuVLTzpOTiZigehafUx22ALfrGmF28vK+uxU4rc0DYsJMzyE+/bmmIzyE6H8NRsCbaYp5XyAUCMm4jq+MoZrR0bPde96yGNNwPP/xEasIOu5S/+QoVCQN8BkeArIGY8T3M= diff --git a/pulumi/co2_reports/__main__.py b/pulumi/co2_reports/__main__.py index b05a0aeb..5d0309ea 100644 --- a/pulumi/co2_reports/__main__.py +++ b/pulumi/co2_reports/__main__.py @@ -16,6 +16,7 @@ onepassword_provider = onepassword.Provider( "onepassword-provider", service_account_token=onepassword_config.require_secret("service_account_token"), + account="", # Explicitly disable CLI account detection to avoid conflicts ) # Get GitHub token from 1Password @@ -39,15 +40,29 @@ co2_reports_bucket = aws.s3.Bucket( "co2-reports-bucket", bucket="nf-core-co2-reports", - server_side_encryption_configuration=aws.s3.BucketServerSideEncryptionConfigurationArgs( - rule=aws.s3.BucketServerSideEncryptionConfigurationRuleArgs( - apply_server_side_encryption_by_default=aws.s3.BucketServerSideEncryptionConfigurationRuleApplyServerSideEncryptionByDefaultArgs( + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Configure server-side encryption for the bucket +co2_reports_bucket_encryption = aws.s3.BucketServerSideEncryptionConfigurationV2( + "co2-reports-bucket-encryption", + bucket=co2_reports_bucket.id, + rules=[ + aws.s3.BucketServerSideEncryptionConfigurationV2RuleArgs( + apply_server_side_encryption_by_default=aws.s3.BucketServerSideEncryptionConfigurationV2RuleApplyServerSideEncryptionByDefaultArgs( sse_algorithm="AES256", ), ), - ), - versioning=aws.s3.BucketVersioningArgs( - enabled=True, + ], + opts=pulumi.ResourceOptions(provider=aws_provider), +) + +# Enable versioning for the bucket +co2_reports_bucket_versioning = aws.s3.BucketVersioningV2( + "co2-reports-bucket-versioning", + bucket=co2_reports_bucket.id, + versioning_configuration=aws.s3.BucketVersioningV2VersioningConfigurationArgs( + status="Enabled", ), opts=pulumi.ResourceOptions(provider=aws_provider), ) From 212cf3412806d6cfaeef6d4f6495baf62ebf86f3 Mon Sep 17 00:00:00 2001 From: Edmund Miller Date: Wed, 29 Oct 2025 17:10:16 +0100 Subject: [PATCH 4/4] sec(pulumi): restrict S3 IAM policy to modules/ prefix only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update IAM policy to restrict CI user access to modules/* prefix only - Split policy into separate bucket-level and object-level statements - Add StringLike condition for s3:prefix on ListBucket operations - Restrict PutObject and GetObject to modules/* resource path only - Remove s3:PutObjectAcl permission (not needed for basic uploads) - Update policy description to reflect modules/ prefix restriction Security improvements: - CI user cannot access bucket root or other prefixes - Can only list/read/write within s3://nf-core-co2-reports/modules/* - Minimal permissions following principle of least privilege - S3 versioning still preserves all file versions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pulumi/co2_reports/__main__.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/pulumi/co2_reports/__main__.py b/pulumi/co2_reports/__main__.py index 5d0309ea..3b600a01 100644 --- a/pulumi/co2_reports/__main__.py +++ b/pulumi/co2_reports/__main__.py @@ -97,7 +97,7 @@ bucket_access_policy = aws.iam.Policy( "co2-reports-bucket-access-policy", name="nf-core-co2-reports-bucket-access", - description="Write access to nf-core-co2-reports S3 bucket for CI/CD", + description="Write access to modules/ prefix in nf-core-co2-reports S3 bucket for CI/CD", policy=co2_reports_bucket.arn.apply( lambda bucket_arn: f"""{{ "Version": "2012-10-17", @@ -105,16 +105,23 @@ {{ "Effect": "Allow", "Action": [ - "s3:PutObject", - "s3:PutObjectAcl", - "s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation" ], - "Resource": [ - "{bucket_arn}", - "{bucket_arn}/*" - ] + "Resource": "{bucket_arn}", + "Condition": {{ + "StringLike": {{ + "s3:prefix": ["modules/*"] + }} + }} + }}, + {{ + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:GetObject" + ], + "Resource": "{bucket_arn}/modules/*" }} ] }}"""