n-daisuke-blog-deployment-s.../app/main.go

224 lines
6.6 KiB
Go

package main
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"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
// 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 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 := req.Headers["X-Hub-Signature-256"] // adjust this header name as appropriate.
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 {
log.Printf("Configuration error: %v", err)
return err
}
// Create a unique temp directory for this run
repoDir, err := os.MkdirTemp("", "repo-*")
if err != nil {
log.Printf("Error creating temporary directory: %v", err)
return 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 {
log.Printf("Failure in cloning: %v", err)
return err
}
// 2. Create a ZIP archive of the repository
if err := createZipArchive(ctx, repoDir, zipFilePath); err != nil {
log.Printf("Failure in creating ZIP archive: %v", err)
return err
}
// 3. Upload the ZIP file to S3
cfg_s3, err := config.LoadDefaultConfig(ctx, config.WithRegion(cfg.AWSRegion))
if err != nil {
log.Printf("Error loading configuration: %v", err)
return err
}
s3Client := s3.NewFromConfig(cfg_s3)
uploader := manager.NewUploader(s3Client)
if err := uploadToS3WithUploader(ctx, zipFilePath, cfg.S3Bucket, cfg.S3Key, uploader); err != nil {
log.Printf("Failure in uploading to S3: %v", err)
return err
}
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(_ context.Context, repoURL, repoBranch, repoDir string) error {
cloneCmd := commandRunner("git", "clone", "--branch", repoBranch, repoURL, repoDir)
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
fmt.Printf("Cloning repository %s (branch %s)...\n", repoURL, repoBranch)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("error cloning repository: %v", err)
}
fmt.Println("Repository cloned successfully.")
return nil
}
func createZipArchive(_ context.Context, repoDir, zipFilePath string) error {
zipCmd := commandRunner("zip", "-r", zipFilePath, ".")
zipCmd.Dir = repoDir // Change to the cloned repo directory
zipCmd.Stdout = os.Stdout
zipCmd.Stderr = os.Stderr
fmt.Println("Creating ZIP archive of the repository...")
if err := zipCmd.Run(); err != nil {
return fmt.Errorf("error creating ZIP archive: %v", err)
}
fmt.Printf("ZIP archive created at %s.\n", 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 {
// Open the ZIP file
f, err := os.Open(zipPath)
if err != nil {
return fmt.Errorf("Error opening ZIP file: %v", err)
}
defer f.Close()
// Upload the file to S3.
fmt.Printf("Uploading %s to s3://%s/%s...\n", 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("failed to upload file: %v", err)
}
fmt.Printf("Successfully uploaded to %s\n", result.Location)
return nil
}