diff --git a/.env b/.env new file mode 100644 index 00000000000..14f6494f9bc --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PORT="8080" diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000000..6829b21a0ae --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,40 @@ +name: cd + +on: + push: + branches: [main] + +permissions: + contents: read + id-token: write + +jobs: + deploy: + name: Deploy + 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.22" + + - name: Build production app + run: ./scripts/buildprod.sh + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up gcloud + uses: google-github-actions/setup-gcloud@v2 + with: + project_id: ${{ secrets.GCP_PROJECT_ID }} + + - name: Build and push image to Artifact Registry + run: | + gcloud builds submit --tag us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/notely-ar-repo/notely:latest . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..5109fe305df --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +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.25.1" + + - name: Run unit tests + run: go test ./... -cover + + - name: Install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest + + - name: Run gosec + run: gosec -exclude=G104,G114,G117,G705,G706 ./... + + 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.25.1" + + - name: Check formatting + run: test -z $(go fmt ./...) + + - name: Install staticcheck + run: go install honnef.co/go/tools/cmd/staticcheck@latest + + - name: Run staticcheck + run: staticcheck ./... + diff --git a/README.md b/README.md index c2bec0368b7..ca724721c38 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![CI Status](https://github.com/Jeremy-J-Allen/learn-cicd-starter/actions/workflows/ci.yml/badge.svg) + # 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! + +Jeremy's version of Boot.dev's Notely app. diff --git a/ci.yml b/ci.yml new file mode 100644 index 00000000000..a67087470b8 --- /dev/null +++ b/ci.yml @@ -0,0 +1,51 @@ +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.25.1" + + - name: Run unit tests + run: go test ./... -cover + + - name: Install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest + + - name: Run gosec + 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.25.1" + + - name: Check formatting + run: test -z $(go fmt ./...) + + - name: Install staticcheck + run: go install honnef.co/go/tools/cmd/staticcheck@latest + + - name: Run staticcheck + run: staticcheck ./... + diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f969aacf638..972f9be3ae2 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -8,12 +8,14 @@ import ( var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") -// GetAPIKey - +// GetAPIKey extracts an API key from the Authorization header. +// Expected format: "ApiKey " func GetAPIKey(headers http.Header) (string, error) { authHeader := headers.Get("Authorization") if authHeader == "" { return "", ErrNoAuthHeaderIncluded } + splitAuth := strings.Split(authHeader, " ") if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { return "", errors.New("malformed authorization header") diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go new file mode 100644 index 00000000000..5f505bf1034 --- /dev/null +++ b/internal/auth/auth_test.go @@ -0,0 +1,20 @@ +package auth + +import ( + "net/http" + "testing" +) + +func TestGetAPIKey(t *testing.T) { + h := http.Header{} + h.Set("Authorization", "ApiKey testkey") + + key, err := GetAPIKey(h) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if key != "testkey" { + t.Fatalf("expected testkey, got %s", key) + } +} diff --git a/json.go b/json.go index 1e6e7985e18..c529c90c124 100644 --- a/json.go +++ b/json.go @@ -30,5 +30,8 @@ func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { return } w.WriteHeader(code) - w.Write(dat) + _, err = w.Write(dat) + if err != nil { + log.Printf("Critical error writing response: %s", err) + } } diff --git a/main.go b/main.go index 19d7366c5f7..ec86bb7bf95 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: time.Second * 5, } log.Printf("Serving on port: %s\n", port) diff --git a/vendor/github.com/go-chi/chi/chi.go b/vendor/github.com/go-chi/chi/chi.go index b7063dc297c..db1780b1c65 100644 --- a/vendor/github.com/go-chi/chi/chi.go +++ b/vendor/github.com/go-chi/chi/chi.go @@ -1,29 +1,29 @@ -// // Package chi is a small, idiomatic and composable router for building HTTP services. // // chi requires Go 1.10 or newer. // // Example: -// package main // -// import ( -// "net/http" +// package main +// +// import ( +// "net/http" // -// "github.com/go-chi/chi" -// "github.com/go-chi/chi/middleware" -// ) +// "github.com/go-chi/chi" +// "github.com/go-chi/chi/middleware" +// ) // -// func main() { -// r := chi.NewRouter() -// r.Use(middleware.Logger) -// r.Use(middleware.Recoverer) +// func main() { +// r := chi.NewRouter() +// r.Use(middleware.Logger) +// r.Use(middleware.Recoverer) // -// r.Get("/", func(w http.ResponseWriter, r *http.Request) { -// w.Write([]byte("root.")) -// }) +// r.Get("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("root.")) +// }) // -// http.ListenAndServe(":3333", r) -// } +// http.ListenAndServe(":3333", r) +// } // // See github.com/go-chi/chi/_examples/ for more in-depth examples. // @@ -47,12 +47,12 @@ // placeholder which will match / characters. // // Examples: -// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" -// "/user/{name}/info" matches "/user/jsmith/info" -// "/page/*" matches "/page/intro/latest" -// "/page/*/index" also matches "/page/intro/latest" -// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" // +// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" +// "/user/{name}/info" matches "/user/jsmith/info" +// "/page/*" matches "/page/intro/latest" +// "/page/*/index" also matches "/page/intro/latest" +// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" package chi import "net/http" diff --git a/vendor/github.com/go-chi/chi/context.go b/vendor/github.com/go-chi/chi/context.go index 8c97f214a93..bd30d52b4fd 100644 --- a/vendor/github.com/go-chi/chi/context.go +++ b/vendor/github.com/go-chi/chi/context.go @@ -112,13 +112,13 @@ func (x *Context) URLParam(key string) string { // // For example, // -// func Instrument(next http.Handler) http.Handler { -// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// next.ServeHTTP(w, r) -// routePattern := chi.RouteContext(r.Context()).RoutePattern() -// measure(w, r, routePattern) -// }) -// } +// func Instrument(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// next.ServeHTTP(w, r) +// routePattern := chi.RouteContext(r.Context()).RoutePattern() +// measure(w, r, routePattern) +// }) +// } func (x *Context) RoutePattern() string { routePattern := strings.Join(x.RoutePatterns, "") return replaceWildcards(routePattern) diff --git a/vendor/github.com/go-chi/cors/cors.go b/vendor/github.com/go-chi/cors/cors.go index 8df81636e3b..75196ab91c2 100644 --- a/vendor/github.com/go-chi/cors/cors.go +++ b/vendor/github.com/go-chi/cors/cors.go @@ -3,15 +3,15 @@ // // You can configure it by passing an option struct to cors.New: // -// c := cors.New(cors.Options{ -// AllowedOrigins: []string{"foo.com"}, -// AllowedMethods: []string{"GET", "POST", "DELETE"}, -// AllowCredentials: true, -// }) +// c := cors.New(cors.Options{ +// AllowedOrigins: []string{"foo.com"}, +// AllowedMethods: []string{"GET", "POST", "DELETE"}, +// AllowCredentials: true, +// }) // // Then insert the handler in the chain: // -// handler = c.Handler(handler) +// handler = c.Handler(handler) // // See Options documentation for more options. // diff --git a/vendor/github.com/google/uuid/dce.go b/vendor/github.com/google/uuid/dce.go index fa820b9d309..9302a1c1bb5 100644 --- a/vendor/github.com/google/uuid/dce.go +++ b/vendor/github.com/google/uuid/dce.go @@ -42,7 +42,7 @@ func NewDCESecurity(domain Domain, id uint32) (UUID, error) { // NewDCEPerson returns a DCE Security (Version 2) UUID in the person // domain with the id returned by os.Getuid. // -// NewDCESecurity(Person, uint32(os.Getuid())) +// NewDCESecurity(Person, uint32(os.Getuid())) func NewDCEPerson() (UUID, error) { return NewDCESecurity(Person, uint32(os.Getuid())) } @@ -50,7 +50,7 @@ func NewDCEPerson() (UUID, error) { // NewDCEGroup returns a DCE Security (Version 2) UUID in the group // domain with the id returned by os.Getgid. // -// NewDCESecurity(Group, uint32(os.Getgid())) +// NewDCESecurity(Group, uint32(os.Getgid())) func NewDCEGroup() (UUID, error) { return NewDCESecurity(Group, uint32(os.Getgid())) } diff --git a/vendor/github.com/google/uuid/hash.go b/vendor/github.com/google/uuid/hash.go index b404f4bec27..24ccde64649 100644 --- a/vendor/github.com/google/uuid/hash.go +++ b/vendor/github.com/google/uuid/hash.go @@ -39,7 +39,7 @@ func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { // NewMD5 returns a new MD5 (Version 3) UUID based on the // supplied name space and data. It is the same as calling: // -// NewHash(md5.New(), space, data, 3) +// NewHash(md5.New(), space, data, 3) func NewMD5(space UUID, data []byte) UUID { return NewHash(md5.New(), space, data, 3) } @@ -47,7 +47,7 @@ func NewMD5(space UUID, data []byte) UUID { // NewSHA1 returns a new SHA1 (Version 5) UUID based on the // supplied name space and data. It is the same as calling: // -// NewHash(sha1.New(), space, data, 5) +// NewHash(sha1.New(), space, data, 5) func NewSHA1(space UUID, data []byte) UUID { return NewHash(sha1.New(), space, data, 5) } diff --git a/vendor/github.com/google/uuid/node_js.go b/vendor/github.com/google/uuid/node_js.go index 24b78edc907..96090351a90 100644 --- a/vendor/github.com/google/uuid/node_js.go +++ b/vendor/github.com/google/uuid/node_js.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build js // +build js package uuid diff --git a/vendor/github.com/google/uuid/node_net.go b/vendor/github.com/google/uuid/node_net.go index 0cbbcddbd6e..e91358f7d9e 100644 --- a/vendor/github.com/google/uuid/node_net.go +++ b/vendor/github.com/google/uuid/node_net.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !js // +build !js package uuid diff --git a/vendor/github.com/google/uuid/null.go b/vendor/github.com/google/uuid/null.go index d7fcbf28651..06ecf9de2af 100644 --- a/vendor/github.com/google/uuid/null.go +++ b/vendor/github.com/google/uuid/null.go @@ -17,15 +17,14 @@ var jsonNull = []byte("null") // NullUUID implements the SQL driver.Scanner interface so // it can be used as a scan destination: // -// var u uuid.NullUUID -// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u) -// ... -// if u.Valid { -// // use u.UUID -// } else { -// // NULL value -// } -// +// var u uuid.NullUUID +// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u) +// ... +// if u.Valid { +// // use u.UUID +// } else { +// // NULL value +// } type NullUUID struct { UUID UUID Valid bool // Valid is true if UUID is not NULL diff --git a/vendor/github.com/google/uuid/version4.go b/vendor/github.com/google/uuid/version4.go index 7697802e4d1..62ac273815c 100644 --- a/vendor/github.com/google/uuid/version4.go +++ b/vendor/github.com/google/uuid/version4.go @@ -9,7 +9,7 @@ import "io" // New creates a new random UUID or panics. New is equivalent to // the expression // -// uuid.Must(uuid.NewRandom()) +// uuid.Must(uuid.NewRandom()) func New() UUID { return Must(NewRandom()) } @@ -17,7 +17,7 @@ func New() UUID { // NewString creates a new random UUID and returns it as a string or panics. // NewString is equivalent to the expression // -// uuid.New().String() +// uuid.New().String() func NewString() string { return Must(NewRandom()).String() } @@ -31,11 +31,11 @@ func NewString() string { // // A note about uniqueness derived from the UUID Wikipedia entry: // -// Randomly generated UUIDs have 122 random bits. One's annual risk of being -// hit by a meteorite is estimated to be one chance in 17 billion, that -// means the probability is about 0.00000000006 (6 × 10−11), -// equivalent to the odds of creating a few tens of trillions of UUIDs in a -// year and having one duplicate. +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. func NewRandom() (UUID, error) { if !poolEnabled { return NewRandomFromReader(rander) diff --git a/vendor/nhooyr.io/websocket/accept.go b/vendor/nhooyr.io/websocket/accept.go index 18536bdb2ca..b54471312ec 100644 --- a/vendor/nhooyr.io/websocket/accept.go +++ b/vendor/nhooyr.io/websocket/accept.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/vendor/nhooyr.io/websocket/close_notjs.go b/vendor/nhooyr.io/websocket/close_notjs.go index 4251311d2e6..8cafaa9dcfd 100644 --- a/vendor/nhooyr.io/websocket/close_notjs.go +++ b/vendor/nhooyr.io/websocket/close_notjs.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/vendor/nhooyr.io/websocket/compress_notjs.go b/vendor/nhooyr.io/websocket/compress_notjs.go index 809a272c3d1..e094513b354 100644 --- a/vendor/nhooyr.io/websocket/compress_notjs.go +++ b/vendor/nhooyr.io/websocket/compress_notjs.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/vendor/nhooyr.io/websocket/conn_notjs.go b/vendor/nhooyr.io/websocket/conn_notjs.go index 0c85ab7711b..3c1361c5b2b 100644 --- a/vendor/nhooyr.io/websocket/conn_notjs.go +++ b/vendor/nhooyr.io/websocket/conn_notjs.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/vendor/nhooyr.io/websocket/dial.go b/vendor/nhooyr.io/websocket/dial.go index 7a7787ff71f..7b91713e9c8 100644 --- a/vendor/nhooyr.io/websocket/dial.go +++ b/vendor/nhooyr.io/websocket/dial.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/vendor/nhooyr.io/websocket/doc.go b/vendor/nhooyr.io/websocket/doc.go index efa920e3b61..a2b873c722c 100644 --- a/vendor/nhooyr.io/websocket/doc.go +++ b/vendor/nhooyr.io/websocket/doc.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js // Package websocket implements the RFC 6455 WebSocket protocol. @@ -16,7 +17,7 @@ // // More documentation at https://nhooyr.io/websocket. // -// Wasm +// # Wasm // // The client side supports compiling to Wasm. // It wraps the WebSocket browser API. @@ -25,8 +26,8 @@ // // Some important caveats to be aware of: // -// - Accept always errors out -// - Conn.Ping is no-op -// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op -// - *http.Response from Dial is &http.Response{} with a 101 status code on success +// - Accept always errors out +// - Conn.Ping is no-op +// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op +// - *http.Response from Dial is &http.Response{} with a 101 status code on success package websocket // import "nhooyr.io/websocket" diff --git a/vendor/nhooyr.io/websocket/internal/wsjs/wsjs_js.go b/vendor/nhooyr.io/websocket/internal/wsjs/wsjs_js.go index 26ffb45625b..88e8f43f690 100644 --- a/vendor/nhooyr.io/websocket/internal/wsjs/wsjs_js.go +++ b/vendor/nhooyr.io/websocket/internal/wsjs/wsjs_js.go @@ -1,3 +1,4 @@ +//go:build js // +build js // Package wsjs implements typed access to the browser javascript WebSocket API. diff --git a/vendor/nhooyr.io/websocket/read.go b/vendor/nhooyr.io/websocket/read.go index ae05cf93eda..0f10afef62f 100644 --- a/vendor/nhooyr.io/websocket/read.go +++ b/vendor/nhooyr.io/websocket/read.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/vendor/nhooyr.io/websocket/write.go b/vendor/nhooyr.io/websocket/write.go index 2210cf817a3..9528e5acd7d 100644 --- a/vendor/nhooyr.io/websocket/write.go +++ b/vendor/nhooyr.io/websocket/write.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket