- Update Dockerfile: Go 1.25.5, fix build context paths - Use git archive instead of zip to exclude .git directory - Add context support for git/zip operations (timeout control) - Optimize git clone (--depth 1, --single-branch) - Improve error handling (%w wrapping) and logging consistency - Add case-insensitive HTTP header lookup for webhooks
226 lines
6.7 KiB
Go
226 lines
6.7 KiB
Go
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=<signature>"
|
|
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
|
|
}
|