diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0ff4a2e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM docker.io/golang:1.24.2-bookworm as build +WORKDIR /app +# Copy dependencies list +COPY ./app/go.mod ./ +COPY ./app/go.sum ./ +# Build with optional lambda.norpc tag +COPY ./app/main.go ./ +RUN go build -tags lambda.norpc -o main main.go +# Copy artifacts to a clean image +FROM public.ecr.aws/lambda/provided:al2023 +COPY --from=build /app/main ${LAMBDA_TASK_ROOT} +ENTRYPOINT [ "main" ] \ No newline at end of file diff --git a/app/go.mod b/app/go.mod new file mode 100644 index 0000000..38bd1a9 --- /dev/null +++ b/app/go.mod @@ -0,0 +1,26 @@ +module n-daisuke-blog-deployment-source + +go 1.24.2 + +require ( + github.com/aws/aws-lambda-go v1.48.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.74 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect + github.com/aws/smithy-go v1.22.2 // indirect +) diff --git a/app/go.sum b/app/go.sum new file mode 100644 index 0000000..ae764b6 --- /dev/null +++ b/app/go.sum @@ -0,0 +1,40 @@ +github.com/aws/aws-lambda-go v1.48.0 h1:1aZUYsrJu0yo5fC4z+Rba1KhNImXcJcvHu763BxoyIo= +github.com/aws/aws-lambda-go v1.48.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= +github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= +github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.74 h1:+1lc5oMFFHlVBclPXQf/POqlvdpBzjLaN2c3ujDCcZw= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.74/go.mod h1:EiskBoFr4SpYnFIbw8UM7DP7CacQXDHEmJqLI1xpRFI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1 h1:4nm2G6A4pV9rdlWzGMPv4BNtQp22v1hg3yrtkYpeLl8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 h1:BRXS0U76Z8wfF+bnkilA2QwpIch6URlm++yPUt9QPmQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3/go.mod h1:bNXKFFyaiVvWuR6O16h/I1724+aXe/tAkA9/QS01t5k= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..b710825 --- /dev/null +++ b/app/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "context" + "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 +} + +var commandRunner = exec.Command + +func handleRequest(ctx context.Context, event json.RawMessage) error { + // 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 err + } + return nil +} + +func main() { + lambda.Start(handleRequest) +} + +func runDeploymentProcess(ctx context.Context) error { + + cfg, err := loadConfig() + if err != nil { + log.Fatalf("Configuration error: %v", err) + return err + } + + // Create a unique temp directory for this run + repoDir, err := os.MkdirTemp("", "repo-*") + if err != nil { + log.Fatalf("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.Fatalf("Failure in cloning: %v", err) + return err + } + + // 2. Create a ZIP archive of the repository + if err := createZipArchive(ctx, repoDir, zipFilePath); err != nil { + log.Fatalf("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.Fatalf("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.Fatalf("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 { + log.Fatalf("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 +} diff --git a/app/main_test.go b/app/main_test.go new file mode 100644 index 0000000..ca76fe6 --- /dev/null +++ b/app/main_test.go @@ -0,0 +1,226 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +func TestLoadConfig_Success(t *testing.T) { + // Set up environment variables for the test. + os.Setenv("REPO_URL", "https://example.com/repo.git") + os.Setenv("REPO_BRANCH", "main") + os.Setenv("S3_BUCKET", "my-s3-bucket") + os.Setenv("S3_KEY", "source.zip") + os.Setenv("AWS_REGION", "ap-northeast-1") + + // Call the function. + cfg, err := loadConfig() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + // Validate the configuration values. + if cfg.RepoURL != "https://example.com/repo.git" { + t.Errorf("unexpected RepoURL: got %s, want %s", cfg.RepoURL, "https://example.com/repo.git") + } + if cfg.RepoBranch != "main" { + t.Errorf("unexpected RepoBranch: got %s, want %s", cfg.RepoBranch, "main") + } + if cfg.S3Bucket != "my-s3-bucket" { + t.Errorf("unexpected S3Bucket: got %s, want %s", cfg.S3Bucket, "my-s3-bucket") + } + if cfg.S3Key != "source.zip" { + t.Errorf("unexpected S3Key: got %s, want %s", cfg.S3Key, "source.zip") + } + if cfg.AWSRegion != "ap-northeast-1" { + t.Errorf("unexpected AWSRegion: got %s, want %s", cfg.AWSRegion, "ap-northeast-1") + } +} + +func TestLoadConfig_MissingRepoURL(t *testing.T) { + // Clear the REPO_URL environment variable. + os.Unsetenv("REPO_URL") + + // Optionally, set up other required variables. + os.Setenv("S3_BUCKET", "my-s3-bucket") + + // Call the function. + _, err := loadConfig() + if err == nil { + t.Fatal("expected an error due to missing REPO_URL, got nil") + } +} + +func TestLoadConfig_MissingS3Bucket(t *testing.T) { + // Clear the S3_BUCKET environment variable. + os.Unsetenv("S3_BUCKET") + + // Optionally, set up other required variables. + os.Setenv("REPO_URL", "https://example.com/repo.git") + + // Call the function. + _, err := loadConfig() + if err == nil { + t.Fatal("expected an error due to missing S3_BUCKET, got nil") + } +} + +// TestHelperProcess is not a real test. It is invoked as a helper process +// when our fake command runner is used. +func TestHelperProcess(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + // If the variable is not set, this function should do nothing. + return + } + // If GO_EXIT_STATUS is set, convert it to an integer. + if exitStatus := os.Getenv("GO_EXIT_STATUS"); exitStatus != "" { + // You can exit with this status if it's nonzero. + // For example, if it's "1", then this simulates a failing command. + os.Exit(1) + } + // Otherwise, simulate success. + os.Exit(0) +} + +// fakeExecCommand returns a function that simulates exec.Command. +// The success parameter indicates whether the command should succeed. +func fakeExecCommand(success bool) func(name string, arg ...string) *exec.Cmd { + return func(name string, arg ...string) *exec.Cmd { + // We simulate the command by re-executing the test binary with special flags. + // The "--" signals the end of flags for our helper. + cs := []string{"-test.run=TestHelperProcess", "--", name} + cs = append(cs, arg...) + cmd := exec.Command(os.Args[0], cs...) + // Set an env variable that tells our TestHelperProcess to run. + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + if !success { + // Set a value to indicate failure. Our helper can inspect this if desired, + // or we can simply exit with a non-zero status. + cmd.Env = append(cmd.Env, "GO_EXIT_STATUS=1") + } + return cmd + } +} + +func TestCloneRepository_Success(t *testing.T) { + // Override commandRunner to simulate a successful git clone. + originalCommandRunner := commandRunner + commandRunner = fakeExecCommand(true) + defer func() { commandRunner = originalCommandRunner }() + + // Use a temporary directory for testing. + tempDir := t.TempDir() + + // Call cloneRepository. + err := cloneRepository(context.Background(), "https://example.com/repo.git", "main", tempDir) + if err != nil { + t.Fatalf("expected success, got error: %v", err) + } +} + +func TestCloneRepository_Failure(t *testing.T) { + // Override commandRunner to simulate a failing git clone. + originalCommandRunner := commandRunner + commandRunner = fakeExecCommand(false) + defer func() { commandRunner = originalCommandRunner }() + + tempDir := t.TempDir() + + // Call cloneRepository expecting an error. + err := cloneRepository(context.Background(), "https://example.com/repo.git", "main", tempDir) + if err == nil { + t.Fatal("expected an error, got nil") + } +} + +// TestCreateZipArchive_Success simulates a successful zip creation. +func TestCreateZipArchive_Success(t *testing.T) { + // Override the global commandRunner with our fake that simulates success. + originalCommandRunner := commandRunner + commandRunner = fakeExecCommand(true) + defer func() { commandRunner = originalCommandRunner }() + + // Use t.TempDir to create a temporary directory simulating the repo directory. + tempDir := t.TempDir() + // Define a zip file path within the temp directory. + zipFilePath := filepath.Join(tempDir, "source.zip") + + // Call createZipArchive. + if err := createZipArchive(context.Background(), tempDir, zipFilePath); err != nil { + t.Fatalf("expected success, got error: %v", err) + } +} + +// TestCreateZipArchive_Failure simulates a failing zip creation. +func TestCreateZipArchive_Failure(t *testing.T) { + // Override commandRunner to simulate a command failure. + originalCommandRunner := commandRunner + commandRunner = fakeExecCommand(false) + defer func() { commandRunner = originalCommandRunner }() + + tempDir := t.TempDir() + zipFilePath := filepath.Join(tempDir, "source.zip") + + // Call createZipArchive and expect an error. + if err := createZipArchive(context.Background(), tempDir, zipFilePath); err == nil { + t.Fatal("expected an error, got nil") + } +} + +// fakeUploader implements the Uploader interface. +type fakeUploader struct { + success bool +} + +func (f *fakeUploader) Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) { + if f.success { + // Simulate a successful upload with a fake location. + return &manager.UploadOutput{Location: "http://fake-s3/uploaded-file"}, nil + } + // Simulate a failure. + return nil, fmt.Errorf("fake upload error") +} + +func TestUploadToS3WithUploader_Success(t *testing.T) { + ctx := context.Background() + // Create a temporary directory and file to act as the ZIP file. + tempDir := t.TempDir() + zipFilePath := filepath.Join(tempDir, "source.zip") + content := []byte("dummy zip content") + if err := os.WriteFile(zipFilePath, content, 0644); err != nil { + t.Fatalf("failed to create temp zip file: %v", err) + } + + // Create a fake uploader that simulates success. + u := &fakeUploader{success: true} + + err := uploadToS3WithUploader(ctx, zipFilePath, "fake-bucket", "source.zip", u) + if err != nil { + t.Fatalf("expected success, got error: %v", err) + } +} + +func TestUploadToS3WithUploader_Failure(t *testing.T) { + ctx := context.Background() + tempDir := t.TempDir() + zipFilePath := filepath.Join(tempDir, "source.zip") + content := []byte("dummy zip content") + if err := os.WriteFile(zipFilePath, content, 0644); err != nil { + t.Fatalf("failed to create temp zip file: %v", err) + } + + // Create a fake uploader that simulates a failure. + u := &fakeUploader{success: false} + + err := uploadToS3WithUploader(ctx, zipFilePath, "fake-bucket", "source.zip", u) + if err == nil { + t.Fatal("expected an error, got nil") + } +} diff --git a/go.mod b/go.mod deleted file mode 100644 index 69f9e9c..0000000 --- a/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module n-daisuke-blog-deployment-source - -go 1.24.2 diff --git a/main.go b/main.go deleted file mode 100644 index e69de29..0000000