diff --git a/.env b/.env new file mode 100644 index 0000000000..b0cca29a37 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ + +PORT="8080" + + diff --git a/.github/workflows/apprunner.yaml b/.github/workflows/apprunner.yaml new file mode 100644 index 0000000000..ca752f68f4 --- /dev/null +++ b/.github/workflows/apprunner.yaml @@ -0,0 +1,10 @@ +version: 1.0 +runtime: go +runtime-version: 1.22 +build: + commands: + - go build -mod=vendor -o server . +run: + command: ./server + network: + port: 8080 \ No newline at end of file diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000000..aed137c9e5 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,59 @@ +# This workflow name is for display in GitHub's UI. +name: Dynamic CD for ECS + +# This specifies when the workflow should run. +# We'll trigger on pushes to the main branch and on pull requests. +# This helps with dynamic tagging and allows for checks on PRs. +on: + push: + branches: [main] + pull_request: + branches: [main] + +# A simple set of global environment variables for easy configuration. +env: + AWS_REGION: us-east-1 # The AWS region where your resources are located. + ECR_REPOSITORY: notely-repo # The name of your ECR repository. + ECS_CLUSTER: notely-cluster # The name of your ECS cluster. + ECS_SERVICE: notely-service # The name of your ECS service. + ECS_TASK_DEFINITION: notely-deployment.json # The path to your task definition file. + CONTAINER_NAME: notely-container # The name of the container in your task definition. + +jobs: + deploy: + name: Deploy to Amazon ECS + # This ensures the job only runs on push events, not pull requests. + if: github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + id-token: write # This is required for requesting the JWT for OIDC + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + # Use a GitHub secret for your IAM role ARN to keep it secure. + role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build and push Docker image + id: build-image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + # The image tag is now a combination of the repository and the git commit SHA. + # This makes every image tag unique and traceable. + IMAGE_TAG: ${{ github.sha }} + run: | + # The `run` command is a multi-line script. + # It builds the image and tags it with the dynamic ECR registry URL and the git SHA. + # It then pushes the image to ECR. + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..85b5b3402f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: ci + +on: + pull_request: + branches: [main] + +jobs: + tests: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.0" + + - name: Run All Tests + run: go test -cover ./... + + - name: Install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest + + - name: Gosec check + run: gosec ./... + + style: + name: Style + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.0" + + - name: Check formatting + run: | + go_fmt_result=$(find . -path ./vendor -prune -o -name '*.go' -print0 | xargs -0 gofmt -l) + if [[ -n "$go_fmt_result" ]]; then + echo "The following files are not formatted correctly:" + echo "$go_fmt_result" + exit 1 + fi + + - name: Install staticcheck + run: go install honnef.co/go/tools/cmd/staticcheck@latest + + - name: Run Staticcheck + run: staticcheck ./... + + diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 2092f54e78..0000000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -out -.env -learn-cicd-starter -notely diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..26d33521af --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000000..105ce2da2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/learn-cicd-starter .iml b/.idea/learn-cicd-starter .iml new file mode 100644 index 0000000000..2c80e12694 --- /dev/null +++ b/.idea/learn-cicd-starter .iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..6bcf0f6d9f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000..3c278e964b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..94a25f7f4c --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2be3d18b81..41ec99ecc5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,30 @@ -FROM --platform=linux/amd64 debian:stable-slim +# Use a specific Go version (1.22) as the build environment. +FROM golang:1.22-alpine AS builder -RUN apt-get update && apt-get install -y ca-certificates +# Set the working directory +WORKDIR /app -ADD notely /usr/bin/notely +# Copy go.mod and go.sum and download dependencies +COPY go.mod go.sum ./ +RUN go mod download -CMD ["notely"] +# Copy the source code +COPY . . + +# Build the application binary +RUN CGO_ENABLED=0 go build -o server . + +# Final stage: create a small, efficient image to run the binary +FROM alpine:latest + +# Set the working directory +WORKDIR /app + +# Copy the compiled binary from the builder stage +COPY --from=builder /app/server ./server + +# Expose the port your application listens on +EXPOSE 8080 + +# Command to run the application +CMD ["./server"] \ No newline at end of file diff --git a/README.md b/README.md index c2bec0368b..6443b54d33 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# learn-cicd-starter (Notely) +![Test Badge](https://github.com/sjamesjr/learn-cicd-starter/actions/workflows/ci.yml/badge.svg) + +go ver# learn-cicd-starter (Notely) This repo contains the starter code for the "Notely" application for the "Learn CICD" course on [Boot.dev](https://boot.dev). @@ -21,3 +23,5 @@ go build -o notely && ./notely *This starts the server in non-database mode.* It will serve a simple webpage at `http://localhost:8080`. You do *not* need to set up a database or any interactivity on the webpage yet. Instructions for that will come later in the course! + +Sam James's version of Boot.dev's Notely app \ No newline at end of file diff --git a/go1.21.2.darwin-amd64.pkg b/go1.21.2.darwin-amd64.pkg new file mode 100644 index 0000000000..8fe652e325 --- /dev/null +++ b/go1.21.2.darwin-amd64.pkg @@ -0,0 +1,2 @@ +Moved Permanently. + diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f969aacf63..e4ac3cabb4 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -14,7 +14,7 @@ func GetAPIKey(headers http.Header) (string, error) { if authHeader == "" { return "", ErrNoAuthHeaderIncluded } - splitAuth := strings.Split(authHeader, " ") + splitAuth := strings.Split(authHeader, " ") // Fix the code if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { return "", errors.New("malformed authorization header") } diff --git a/internal/auth/unittests.go b/internal/auth/unittests.go new file mode 100644 index 0000000000..a084ee414f --- /dev/null +++ b/internal/auth/unittests.go @@ -0,0 +1,39 @@ +package auth + +import ( + "errors" + "net/http" + "testing" +) + +func TestGetAPIKey_Success(t *testing.T) { + headers := http.Header{} + expectedKey := "secret-key-123" + headers.Set("Authorization", "ApiKey "+expectedKey) + + key, err := GetAPIKey(headers) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if key != expectedKey { + t.Errorf("expected key %q, got %q", expectedKey, key) + } +} + +func TestGetAPIKey_MissingHeader(t *testing.T) { + headers := http.Header{} + + key, err := GetAPIKey(headers) + if err == nil { + t.Fatal("expected error, got nil") + } + + if key != "" { + t.Errorf("expected empty key, got %q", key) + } + + if !errors.Is(err, ErrNoAuthHeaderIncluded) { + t.Errorf("expected error %v, got %v", ErrNoAuthHeaderIncluded, err) + } +} diff --git a/json.go b/json.go index 1e6e7985e1..7fd6ae29ec 100644 --- a/json.go +++ b/json.go @@ -30,5 +30,7 @@ func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { return } w.WriteHeader(code) - w.Write(dat) + if _, err := w.Write(dat); err != nil { + log.Printf("error writing response: %v", err) + } } diff --git a/main.go b/main.go index 19d7366c5f..15ee531c5d 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "log" "net/http" "os" + "time" "github.com/go-chi/chi" "github.com/go-chi/cors" @@ -89,8 +90,9 @@ func main() { router.Mount("/v1", v1Router) srv := &http.Server{ - Addr: ":" + port, - Handler: router, + Addr: ":" + port, + Handler: router, + ReadHeaderTimeout: 5 * time.Second, } log.Printf("Serving on port: %s\n", port) diff --git a/notely b/notely new file mode 100755 index 0000000000..50963bea3c Binary files /dev/null and b/notely differ