package main import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/service/s3" ) type Config struct { RepoURL string RepoBranch string S3Bucket string S3Key string AWSRegion string } type Response struct { StatusCode int `json:"statusCode"` Headers map[string]string `json:"headers"` Body string `json:"body"` } var commandRunner = exec.Command var commandRunnerContext = exec.CommandContext // verifySignature computes an HMAC using the provided secret and compares it to the incoming signature. func verifySignature(secret, body, signatureHeader string) bool { // Assuming the header is in the format "sha256=" const prefix = "sha256=" if len(signatureHeader) < len(prefix) || signatureHeader[:len(prefix)] != prefix { return false } receivedSig := signatureHeader[len(prefix):] mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(body)) expectedSig := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(receivedSig), []byte(expectedSig)) } func getHeader(headers map[string]string, key string) string { for k, v := range headers { if strings.EqualFold(k, key) { return v } } return "" } func handleRequest(ctx context.Context, event json.RawMessage) (Response, error) { // For demonstration, assume the event JSON includes a "body" and "headers" map. var req struct { Body string `json:"body"` Headers map[string]string `json:"headers"` } if err := json.Unmarshal(event, &req); err != nil { log.Printf("Error unmarshalling event: %v", err) return Response{StatusCode: 400, Headers: map[string]string{"Content-Type": "application/json"}, Body: "{\"message\":\"Bad Request\"}"}, err } secret := os.Getenv("WEBHOOK_SECRET") if secret == "" { log.Println("WEBHOOK_SECRET is not set") return Response{StatusCode: 500, Headers: map[string]string{"Content-Type": "application/json"}, Body: "{\"message\":\"Server configuration error\"}"}, fmt.Errorf("WEBHOOK_SECRET is not set") } signature := getHeader(req.Headers, "X-Hub-Signature-256") if signature == "" || !verifySignature(secret, req.Body, signature) { log.Println("Signature verification failed") return Response{StatusCode: 401, Headers: map[string]string{"Content-Type": "application/json"}, Body: "{\"message\":\"Unauthorized\"}"}, fmt.Errorf("signature verification failed") } // Call your existing process (for example, runDeploymentProcess) if err := runDeploymentProcess(ctx); err != nil { // Log the error; you may also report it via CloudWatch alarms log.Printf("Deployment process failed: %v", err) return Response{ StatusCode: 500, Headers: map[string]string{"Content-Type": "application/json"}, Body: fmt.Sprintf("{\"message\": \"Deployment process failed: %v\"}", err), }, err } return Response{ StatusCode: 200, Headers: map[string]string{"Content-Type": "application/json"}, Body: "{\"message\": \"Success\"}", }, nil } func main() { lambda.Start(handleRequest) } func runDeploymentProcess(ctx context.Context) error { cfg, err := loadConfig() if err != nil { return fmt.Errorf("load config: %w", err) } repoDir, err := os.MkdirTemp("", "repo-*") if err != nil { return fmt.Errorf("create temp directory: %w", err) } defer os.RemoveAll(repoDir) zipFilePath := filepath.Join(repoDir, "source.zip") // 1. Clone the repository if err := cloneRepository(ctx, cfg.RepoURL, cfg.RepoBranch, repoDir); err != nil { return err } // 2. Create a ZIP archive of the repository (without .git) if err := createZipArchive(ctx, repoDir, zipFilePath); err != nil { return err } // 3. Upload the ZIP file to S3 awsCfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(cfg.AWSRegion)) if err != nil { return fmt.Errorf("load AWS config: %w", err) } s3Client := s3.NewFromConfig(awsCfg) uploader := manager.NewUploader(s3Client) if err := uploadToS3WithUploader(ctx, zipFilePath, cfg.S3Bucket, cfg.S3Key, uploader); err != nil { return err } log.Println("Deployment process completed successfully") return nil } func loadConfig() (*Config, error) { repoURL := os.Getenv("REPO_URL") if repoURL == "" { return nil, fmt.Errorf("REPO_URL environment variable not set") } repoBranch := os.Getenv("REPO_BRANCH") if repoBranch == "" { repoBranch = "main" } s3Bucket := os.Getenv("S3_BUCKET") if s3Bucket == "" { return nil, fmt.Errorf("S3_BUCKET environment variable not set") } s3Key := os.Getenv("S3_KEY") if s3Key == "" { s3Key = "source.zip" } awsRegion := os.Getenv("AWS_REGION") if awsRegion == "" { awsRegion = "ap-northeast-1" } return &Config{ RepoURL: repoURL, RepoBranch: repoBranch, S3Bucket: s3Bucket, S3Key: s3Key, AWSRegion: awsRegion, }, nil } func cloneRepository(ctx context.Context, repoURL, repoBranch, repoDir string) error { log.Printf("Cloning repository (branch=%s)...", repoBranch) cloneCmd := commandRunnerContext(ctx, "git", "clone", "--depth", "1", "--single-branch", "--branch", repoBranch, repoURL, repoDir) cloneCmd.Stdout = os.Stdout cloneCmd.Stderr = os.Stderr cloneCmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") if err := cloneCmd.Run(); err != nil { return fmt.Errorf("git clone: %w", err) } log.Println("Repository cloned successfully") return nil } func createZipArchive(ctx context.Context, repoDir, zipFilePath string) error { log.Println("Creating ZIP archive (using git archive)...") archiveCmd := commandRunnerContext(ctx, "git", "-C", repoDir, "archive", "--format=zip", "--output", zipFilePath, "HEAD") archiveCmd.Stdout = os.Stdout archiveCmd.Stderr = os.Stderr if err := archiveCmd.Run(); err != nil { return fmt.Errorf("git archive: %w", err) } log.Printf("ZIP archive created at %s", zipFilePath) return nil } type Uploader interface { Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) } func uploadToS3WithUploader(ctx context.Context, zipPath, bucket, key string, uploader Uploader) error { f, err := os.Open(zipPath) if err != nil { return fmt.Errorf("open zip file: %w", err) } defer f.Close() log.Printf("Uploading %s to s3://%s/%s...", zipPath, bucket, key) result, err := uploader.Upload(ctx, &s3.PutObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key), Body: f, }) if err != nil { return fmt.Errorf("upload to S3: %w", err) } log.Printf("Successfully uploaded: %s", result.Location) return nil }