From 3a0f9579431043d6e33f390a7aff5dac72fff704 Mon Sep 17 00:00:00 2001 From: Dave Arnold Date: Wed, 23 Apr 2025 14:27:31 -0700 Subject: [PATCH] Add GitHubClient methods for cloning and committing repository contents; implement integration tests for Lambda handler --- eks_automation/app.py | 194 ++++++++++++++++++++++++++++++++++++++ tests/test_integration.py | 62 ++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 tests/test_integration.py diff --git a/eks_automation/app.py b/eks_automation/app.py index 3a139cd..12ea354 100644 --- a/eks_automation/app.py +++ b/eks_automation/app.py @@ -425,6 +425,149 @@ def update_repository_topics(self, repo_name, topics): error_message = f"Failed to update topics for {repo_name}: {response.status_code} - {response.text}" logger.error(error_message) raise Exception(error_message) + + def clone_repository_contents(self, source_repo, target_dir, branch=None): + """Clone a repository's contents to a local directory using GitHub API + + Args: + source_repo (str): Name of the source repository + target_dir (str): Target directory to download files to + branch (str, optional): Branch to clone from. If None, uses default branch. + + Returns: + str: The branch name that was cloned + """ + # Create the target directory if it doesn't exist + os.makedirs(target_dir, exist_ok=True) + + try: + if branch: + target_branch = branch + # Try to get the branch's reference directly + tree_sha = self.get_reference_sha(source_repo, f"heads/{target_branch}") + else: + # If no branch specified, use default branch + target_branch = self.get_default_branch(source_repo) + tree_sha = self.get_reference_sha(source_repo, f"heads/{target_branch}") + except Exception as e: + logger.warning(f"Failed to get reference for {branch or 'default branch'}: {str(e)}") + target_branch = branch or "main" + # If we can't get the reference, the branch might not exist yet + tree = {"tree": []} + self.download_repository_files(source_repo, tree, target_dir) + return target_branch + + # Get the full tree for the branch + logger.info(f"Getting file tree from {source_repo} for branch {target_branch}") + tree = self.get_tree(source_repo, tree_sha, recursive=True) + + # Download all files + logger.info(f"Downloading all files from {source_repo} using ref: heads/{target_branch}") + self.download_repository_files(source_repo, tree, target_dir) + + return target_branch + + def commit_repository_contents(self, repo_name, work_dir, commit_message, branch=None): + """Commit all files from a directory to a repository + + Args: + repo_name (str): Name of the repository + work_dir (str): Directory containing the files to commit + commit_message (str): Commit message + branch (str, optional): Branch to commit to. If None, uses default branch. + + Returns: + str: The branch name that was committed to + """ + # First, get the current state of the target repository + try: + target_branch = branch or self.get_default_branch(repo_name) + except Exception: + # If we can't get the default branch, it might be a new repo + target_branch = branch or "main" + + # Upload all files to the repository + tree_items = [] + + # Add all files from the work directory to the repository + for root, _, files in os.walk(work_dir): + for file in files: + file_path = os.path.join(root, file) + repo_path = os.path.relpath(file_path, work_dir) + + # Skip .git directory if it exists + if ".git" in repo_path.split(os.path.sep): + continue + + # Read file content + with open(file_path, "rb") as f: + file_content = f.read() + + # Create blob for the file + blob_sha = self.create_blob(repo_name, file_content) + + # Add to tree items + tree_items.append({ + "path": repo_path, + "mode": "100644", # Regular file + "type": "blob", + "sha": blob_sha + }) + + # Try to get the latest commit SHA from the base branch + base_branch = "main" # Always use main as base when creating new branches + try: + base_commit_sha = self.get_reference_sha(repo_name, f"heads/{base_branch}") + base_commit = self.get_commit(repo_name, base_commit_sha) + base_tree_sha = base_commit["tree"]["sha"] + except Exception: + # If we can't get the reference, assume it's a new repo with no commits + base_tree_sha = None + + # Create a new tree with all the files + new_tree_sha = self.create_tree(repo_name, tree_items, base_tree_sha) + + # Create a commit with the new tree + if base_tree_sha: + # If we have a base tree, include the parent commit + new_commit_sha = self.create_commit( + repo_name, + commit_message, + new_tree_sha, + [base_commit_sha] + ) + else: + # If it's a new repo, create the first commit + new_commit_sha = self.create_commit( + repo_name, + commit_message, + new_tree_sha, + [] + ) + + # Update or create the reference to point to the new commit + try: + # Try to update existing branch + self.update_reference( + repo_name, + f"heads/{target_branch}", + new_commit_sha + ) + except Exception: + # If the branch doesn't exist, create it + try: + self.create_reference( + repo_name, + f"refs/heads/{target_branch}", + new_commit_sha + ) + except Exception as e: + # If we still can't create the branch, something is wrong + error_message = f"Failed to create or update branch {target_branch} for {repo_name}: {str(e)}" + logger.error(error_message) + raise Exception(error_message) + + return target_branch def operate_github(new_repo_name, eks_settings): """Write EKS settings to config.json and create/update repository using GitHub API @@ -546,3 +689,54 @@ def remove_readonly(func, path, _): """ os.chmod(path, stat.S_IWRITE) func(path) + +# pylint: disable=unused-argument +def lambda_handler(event, context): + """Main Lambda handler function + + Args: + event (dict): Dict containing the Lambda function event data + context (dict): Lambda runtime context + + Returns: + dict: Dict containing status message + """ + logger.info(f"Lambda function invoked with RequestId: {context.aws_request_id}") + logger.info(f"Remaining time in milliseconds: {context.get_remaining_time_in_millis()}") + logger.info(f"Received event: {json.dumps(event, indent=2)}") + + input_data = event.get("body") + if isinstance(input_data, str): + input_data = json.loads(input_data) + logger.info(f"Extracted input data from event body: {json.dumps(input_data, indent=2)}") + + project_name = input_data.get("project_name") + eks_settings = input_data.get("eks_settings") + logger.info(f"Project name: {project_name}") + logger.info(f"EKS settings to be applied: {json.dumps(eks_settings, indent=2)}") + + if not project_name: + logger.error("Missing project name in input") + return { + "statusCode": 400, + "body": json.dumps({"error": "Missing project name"}) + } + + try: + logger.info(f"Starting GitHub operations for project: {project_name}") + operate_github(project_name, eks_settings) + logger.info("GitHub operations completed successfully") + except Exception as e: # pylint: disable=broad-exception-caught + logger.error(f"Error in operate_github: {str(e)}") + logger.error(f"Stack trace: {traceback.format_exc()}") + return { + "statusCode": 400, + "body": json.dumps({"error": str(e)}) + } + + logger.info("Lambda execution completed successfully") + return { + "statusCode": 200, + "headers": {"Access-Control-Allow-Origin": "*"}, + "body": json.dumps({"result": "Success"}) + } diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..5dcce4b --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,62 @@ +import os +import json +import pytest +import uuid +from eks_automation.app import lambda_handler + +# Test environment variables +os.environ["SECRET_NAME"] = "github-token" # Uses AWS Secrets Manager +os.environ["GITHUB_API"] = "https://api.github.com" +os.environ["GITHUB_ORG_NAME"] = "your-org-name" # Replace with test org +os.environ["TEMPLATE_REPO_NAME"] = "template-eks-cluster" +os.environ["TEMPLATE_SOURCE_VERSION"] = "main" # Or specific tag/SHA for testing + +@pytest.fixture +def test_event(): + """Create test event with unique repository name""" + repo_name = f"test-eks-cluster-{uuid.uuid4().hex[:8]}" + return { + "body": { + "project_name": repo_name, + "eks_settings": { + "cluster_name": "test-cluster", + "kubernetes_version": "1.27", + "region": "us-west-2", + "vpc_config": { + "vpc_id": "vpc-test123", + "subnet_ids": ["subnet-test1", "subnet-test2"] + }, + "nodegroups": [{ + "name": "test-ng", + "instance_types": ["t3.medium"], + "desired_size": 2, + "min_size": 1, + "max_size": 3 + }] + } + } + } + +@pytest.fixture +def lambda_context(): + """Mock Lambda context object""" + class MockContext: + def __init__(self): + self.aws_request_id = "test-request-id" + def get_remaining_time_in_millis(self): + return 30000 + return MockContext() + +def test_lambda_handler_creates_repository(test_event, lambda_context): + """Test that Lambda handler creates repository with correct settings""" + # Execute Lambda handler + response = lambda_handler(test_event, lambda_context) + + assert response["statusCode"] == 200 + assert "Success" in response["body"] + + # Additional assertions could verify: + # - Repository was created in GitHub + # - Config file contains correct settings + # - Topics were set correctly + # But these require GitHub API access