From aed769943b2fa488d6bfa304f052257e0d000c8b Mon Sep 17 00:00:00 2001 From: matheusgomes28 Date: Sun, 18 Feb 2024 11:01:04 +0000 Subject: [PATCH] Adding golangci-lint Linter to CI (#28) --- .../actions/golangci-lint/action.yml | 25 ++++++++++++++ .github/workflows/build.yml | 2 +- .github/workflows/failfast.yml | 20 +++++++++++ .github/workflows/pipeline.yml | 7 ++-- admin-app/app.go | 29 +++++++++------- app/app.go | 27 ++++++++++----- app/cache.go | 24 +++++++------- app/contact.go | 33 +++++++++++++++---- app/post.go | 11 ++++--- app/renderer.go | 4 +-- cmd/urchin-admin/main.go | 3 +- cmd/urchin/main.go | 4 ++- common/app_settings.go | 16 ++++----- common/logger.go | 4 +-- common/post.go | 4 +-- database/database.go | 23 +++++++------ 16 files changed, 161 insertions(+), 75 deletions(-) create mode 100644 .github/workflows/actions/golangci-lint/action.yml create mode 100644 .github/workflows/failfast.yml diff --git a/.github/workflows/actions/golangci-lint/action.yml b/.github/workflows/actions/golangci-lint/action.yml new file mode 100644 index 0000000..b844c25 --- /dev/null +++ b/.github/workflows/actions/golangci-lint/action.yml @@ -0,0 +1,25 @@ +name: golangci-lint step +description: golangci-lint step +inputs: + linters: + description: Linter to run + required: true +runs: + using: composite + steps: + - name: Generating templ files + run: | + templ generate + shell: bash + + - name: prepping lint commands + id: set-command + run: | + export LINTERS="${{ inputs.linters }}" + export LINTER_CMD="golangci-lint run --disable-all -E ${LINTERS//;/ -E }" + echo "linter_cmd=${LINTER_CMD}" >> $GITHUB_OUTPUT + shell: bash + + - name: running the linter command + run: eval ${{ steps.set-command.outputs.linter_cmd }} + shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ee72ab..09ae49f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest container: - image: mattgomes28/urchin-golang:0.1 + image: mattgomes28/urchin-golang:0.2 options: --user 1001 steps: diff --git a/.github/workflows/failfast.yml b/.github/workflows/failfast.yml new file mode 100644 index 0000000..75bc301 --- /dev/null +++ b/.github/workflows/failfast.yml @@ -0,0 +1,20 @@ +name: Fail Fast + +on: [workflow_call] + +jobs: + linters: + name: Linters ๐Ÿง‘โ€๐Ÿ”ฌ + + runs-on: ubuntu-latest + container: + image: mattgomes28/urchin-golang:0.2 + options: --user 1001 + + steps: + - uses: actions/checkout@v3 + + - uses: ./.github/workflows/actions/golangci-lint + name: Running Linters ๐Ÿงช + with: + linters: errcheck;staticcheck;unused;gosimple;gofmt diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 9d934b4..e46c48c 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -7,11 +7,12 @@ on: branches: [ "main" ] jobs: - # failfast: - # uses: ./.github/workflows/failfast.yml + failfast: + uses: ./.github/workflows/failfast.yml + build: uses: ./.github/workflows/build.yml - # needs: failfast + needs: failfast # tests: # uses: ./.github/workflows/test.yml # needs: build diff --git a/admin-app/app.go b/admin-app/app.go index 1dc507f..adf65d8 100644 --- a/admin-app/app.go +++ b/admin-app/app.go @@ -40,7 +40,7 @@ func getPostHandler(database *database.Database) func(*gin.Context) { if err := c.ShouldBindUri(&post_binding); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "could not get post id", - "msg": err.Error(), + "msg": err.Error(), }) return } @@ -49,7 +49,7 @@ func getPostHandler(database *database.Database) func(*gin.Context) { if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "invalid post id type", - "msg": err.Error(), + "msg": err.Error(), }) return } @@ -59,14 +59,14 @@ func getPostHandler(database *database.Database) func(*gin.Context) { log.Warn().Msgf("could not get post from DB: %v", err) c.JSON(http.StatusNotFound, gin.H{ "error": "post id not found", - "msg": err.Error(), + "msg": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ - "id": post.Id, - "title": post.Title, + "id": post.Id, + "title": post.Title, "excerpt": post.Excerpt, "content": post.Content, }) @@ -83,7 +83,7 @@ func postPostHandler(database *database.Database) func(*gin.Context) { log.Warn().Msgf("could not get post from DB: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "invalid request body", - "msg": err.Error(), + "msg": err.Error(), }) return } @@ -97,7 +97,7 @@ func postPostHandler(database *database.Database) func(*gin.Context) { log.Error().Msgf("failed to add post: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "could not add post", - "msg": err.Error(), + "msg": err.Error(), }) return } @@ -119,7 +119,7 @@ func putPostHandler(database *database.Database) func(*gin.Context) { log.Warn().Msgf("could not get post from DB: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "invalid request body", - "msg": err.Error(), + "msg": err.Error(), }) return } @@ -134,7 +134,7 @@ func putPostHandler(database *database.Database) func(*gin.Context) { log.Error().Msgf("failed to change post: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "could not change post", - "msg": err.Error(), + "msg": err.Error(), }) return } @@ -156,7 +156,7 @@ func deletePostHandler(database *database.Database) func(*gin.Context) { log.Warn().Msgf("could not delete post: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "invalid request body", - "msg": err.Error(), + "msg": err.Error(), }) return } @@ -166,7 +166,7 @@ func deletePostHandler(database *database.Database) func(*gin.Context) { log.Error().Msgf("failed to delete post: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "could not delete post", - "msg": err.Error(), + "msg": err.Error(), }) return } @@ -186,7 +186,12 @@ func Run(app_settings common.AppSettings, database database.Database) error { r.POST("/posts", postPostHandler(&database)) r.PUT("/posts", putPostHandler(&database)) r.DELETE("/posts", deletePostHandler(&database)) - r.Run(fmt.Sprintf(":%s", app_settings.WebserverPort)) + + err := r.Run(fmt.Sprintf(":%s", app_settings.WebserverPort)) + if err != nil { + log.Error().Msgf("could not run app: %v", err) + return err + } return nil } diff --git a/app/app.go b/app/app.go index 9867906..3ddc312 100644 --- a/app/app.go +++ b/app/app.go @@ -20,19 +20,22 @@ type Generator = func(*gin.Context, common.AppSettings, *database.Database) ([]b func Run(app_settings common.AppSettings, database *database.Database) error { r := gin.Default() r.MaxMultipartMemory = 1 - + // All cache-able endpoints - cache := makeCache(4, time.Minute * 10) + cache := makeCache(4, time.Minute*10) addCachableHandler(r, "GET", "/", homeHandler, &cache, app_settings, database) addCachableHandler(r, "GET", "/contact", contactHandler, &cache, app_settings, database) addCachableHandler(r, "GET", "/post/:id", postHandler, &cache, app_settings, database) - + // DO not cache as it needs to handlenew form values r.POST("/contact-send", makeContactFormHandler()) - r.Static("/static", "./static") - r.Run(fmt.Sprintf(":%s", app_settings.WebserverPort)) + err := r.Run(fmt.Sprintf(":%s", app_settings.WebserverPort)) + if err != nil { + log.Error().Msgf("could not run app: %v", err) + return err + } return nil } @@ -46,7 +49,7 @@ func addCachableHandler(e *gin.Engine, method string, endpoint string, generator c.Data(http.StatusOK, "text/html; charset=utf-8", cached_endpoint.contents) return } - + // Before handler call (retrieve from cache) html_buffer, err := generator(c, app_settings, db) if err != nil { @@ -76,8 +79,8 @@ func addCachableHandler(e *gin.Engine, method string, endpoint string, generator } } -/// This function will act as the handler for -/// the home page +// / This function will act as the handler for +// / the home page func homeHandler(c *gin.Context, settings common.AppSettings, db *database.Database) ([]byte, error) { posts, err := db.GetPosts() if err != nil { @@ -87,6 +90,12 @@ func homeHandler(c *gin.Context, settings common.AppSettings, db *database.Datab // if not cached, create the cache index_view := views.MakeIndex(posts) html_buffer := bytes.NewBuffer(nil) - index_view.Render(c, html_buffer) + + err = index_view.Render(c, html_buffer) + if err != nil { + log.Error().Msgf("could not render index: %v", err) + return []byte{}, err + } + return html_buffer.Bytes(), nil } diff --git a/app/cache.go b/app/cache.go index c7b0ee9..b1e395a 100644 --- a/app/cache.go +++ b/app/cache.go @@ -10,11 +10,11 @@ import ( // Do not store over this amount // of MBs in the cache -const MAX_CACHE_SIZE_MB = 10; +const MAX_CACHE_SIZE_MB = 10 type EndpointCache struct { - name string - contents []byte + name string + contents []byte validUntil time.Time } @@ -23,21 +23,21 @@ func emptyEndpointCache() EndpointCache { } type Cache struct { - cacheMap shardedmap.ShardMap - cacheTimeout time.Duration + cacheMap shardedmap.ShardMap + cacheTimeout time.Duration estimatedSize atomic.Uint64 // in bytes } func (self *Cache) Store(name string, buffer []byte) error { // Only store to the cache if we have enough space left - afterSizeMB := float64(self.estimatedSize.Load() + uint64(len(buffer))) / 1000000; + afterSizeMB := float64(self.estimatedSize.Load()+uint64(len(buffer))) / 1000000 if afterSizeMB > MAX_CACHE_SIZE_MB { return fmt.Errorf("maximum size reached") } var cache_entry interface{} = EndpointCache{ - name: name, - contents: buffer, + name: name, + contents: buffer, validUntil: time.Now().Add(self.cacheTimeout), } self.cacheMap.Set(name, &cache_entry) @@ -55,8 +55,8 @@ func (self *Cache) Get(name string) (EndpointCache, error) { if cache_contents.validUntil.After(time.Now()) { return cache_contents, nil } else { - self.cacheMap.Delete(name) - return emptyEndpointCache(), fmt.Errorf("cached endpoint had expired") + self.cacheMap.Delete(name) + return emptyEndpointCache(), fmt.Errorf("cached endpoint had expired") } } @@ -69,8 +69,8 @@ func (self *Cache) Size() uint64 { func makeCache(n_shards int, expiry_duration time.Duration) Cache { return Cache{ - cacheMap: shardedmap.NewShardMap(n_shards), - cacheTimeout: expiry_duration, + cacheMap: shardedmap.NewShardMap(n_shards), + cacheTimeout: expiry_duration, estimatedSize: atomic.Uint64{}, } } diff --git a/app/contact.go b/app/contact.go index 61c84de..7b85b63 100644 --- a/app/contact.go +++ b/app/contact.go @@ -9,11 +9,19 @@ import ( "github.com/matheusgomes28/urchin/common" "github.com/matheusgomes28/urchin/database" "github.com/matheusgomes28/urchin/views" + "github.com/rs/zerolog/log" ) func makeContactFormHandler() func(*gin.Context) { return func(c *gin.Context) { - c.Request.ParseForm() + if err := c.Request.ParseForm(); err != nil { + log.Error().Msgf("could not parse form %v", err) + if err = render(c, http.StatusOK, views.MakeContactFailure("unknown", err.Error())); err != nil { + log.Error().Msgf("could not render %v", err) + } + return + } + email := c.Request.FormValue("email") name := c.Request.FormValue("name") message := c.Request.FormValue("message") @@ -21,22 +29,31 @@ func makeContactFormHandler() func(*gin.Context) { // Parse email _, err := mail.ParseAddress(email) if err != nil { - render(c, http.StatusOK, views.MakeContactFailure(email, err.Error())) + log.Error().Msgf("could not parse email: %v", err) + if err = render(c, http.StatusOK, views.MakeContactFailure(email, err.Error())); err != nil { + log.Error().Msgf("could not render: %v", err) + } return } // Make sure name and message is reasonable if len(name) > 200 { - render(c, http.StatusOK, views.MakeContactFailure(email, "name too long (200 chars max)")) + if err = render(c, http.StatusOK, views.MakeContactFailure(email, "name too long (200 chars max)")); err != nil { + log.Error().Msgf("could not render: %v", err) + } return - } + } if len(message) > 10000 { - render(c, http.StatusOK, views.MakeContactFailure(email, "message too long (1000 chars max)")) + if err = render(c, http.StatusOK, views.MakeContactFailure(email, "message too long (1000 chars max)")); err != nil { + log.Error().Msgf("could not render: %v", err) + } return } - render(c, http.StatusOK, views.MakeContactSuccess(email, name)) + if err = render(c, http.StatusOK, views.MakeContactSuccess(email, name)); err != nil { + log.Error().Msgf("could not render: %v", err) + } } } @@ -44,7 +61,9 @@ func makeContactFormHandler() func(*gin.Context) { func contactHandler(c *gin.Context, app_settings common.AppSettings, db *database.Database) ([]byte, error) { index_view := views.MakeContactPage() html_buffer := bytes.NewBuffer(nil) - index_view.Render(c, html_buffer) + if err := index_view.Render(c, html_buffer); err != nil { + log.Error().Msgf("could not render: %v", err) + } return html_buffer.Bytes(), nil } diff --git a/app/post.go b/app/post.go index ed8d324..433c307 100644 --- a/app/post.go +++ b/app/post.go @@ -11,6 +11,7 @@ import ( "github.com/matheusgomes28/urchin/common" "github.com/matheusgomes28/urchin/database" "github.com/matheusgomes28/urchin/views" + "github.com/rs/zerolog/log" ) type PostBinding struct { @@ -38,8 +39,8 @@ func postHandler(c *gin.Context, app_settings common.AppSettings, database *data if err := c.ShouldBindUri(&post_binding); err != nil { return nil, err } - - // Get the post with the ID + + // Get the post with the ID post_id, err := strconv.Atoi(post_binding.Id) if err != nil { return nil, err @@ -54,7 +55,9 @@ func postHandler(c *gin.Context, app_settings common.AppSettings, database *data post.Content = string(mdToHTML([]byte(post.Content))) post_view := views.MakePostPage(post.Title, post.Content) html_buffer := bytes.NewBuffer(nil) - post_view.Render(c, html_buffer) + if err = post_view.Render(c, html_buffer); err != nil { + log.Error().Msgf("could not render: %v", err) + } return html_buffer.Bytes(), nil -} \ No newline at end of file +} diff --git a/app/renderer.go b/app/renderer.go index c8cfa20..9ae4b1f 100644 --- a/app/renderer.go +++ b/app/renderer.go @@ -5,8 +5,8 @@ import ( "github.com/gin-gonic/gin" ) -/// This function will render the templ component into -/// a gin context's Response Writer +// / This function will render the templ component into +// / a gin context's Response Writer func render(c *gin.Context, status int, template templ.Component) error { c.Status(status) return template.Render(c.Request.Context(), c.Writer) diff --git a/cmd/urchin-admin/main.go b/cmd/urchin-admin/main.go index e692c8c..b51c94e 100644 --- a/cmd/urchin-admin/main.go +++ b/cmd/urchin-admin/main.go @@ -8,7 +8,6 @@ import ( "github.com/rs/zerolog/log" ) - func main() { // sets zerolog as the main logger @@ -35,4 +34,4 @@ func main() { if err != nil { log.Fatal().Msgf("could not run app: %v", err) } -} \ No newline at end of file +} diff --git a/cmd/urchin/main.go b/cmd/urchin/main.go index 5ec4719..5335563 100644 --- a/cmd/urchin/main.go +++ b/cmd/urchin/main.go @@ -27,5 +27,7 @@ func main() { log.Error().Msgf("could not create database connection: %v", err) } - app.Run(app_settings, &db_connection) + if err = app.Run(app_settings, &db_connection); err != nil { + log.Error().Msgf("could not run app: %v", err) + } } diff --git a/common/app_settings.go b/common/app_settings.go index 3854816..4f2b767 100644 --- a/common/app_settings.go +++ b/common/app_settings.go @@ -7,12 +7,12 @@ import ( ) type AppSettings struct { - DatabaseAddress string - DatabasePort int - DatabaseUser string + DatabaseAddress string + DatabasePort int + DatabaseUser string DatabasePassword string - DatabaseName string - WebserverPort string + DatabaseName string + WebserverPort string } func LoadSettings() (AppSettings, error) { @@ -41,7 +41,7 @@ func LoadSettings() (AppSettings, error) { if len(database_port_str) == 0 { return AppSettings{}, fmt.Errorf("URCHIN_DATABASE_PORT is not defined") } - + database_port, err := strconv.Atoi(database_port_str) if err != nil { return AppSettings{}, fmt.Errorf("URCHIN_DATABASE_PORT is not a valid integer: %v", err) @@ -57,7 +57,7 @@ func LoadSettings() (AppSettings, error) { DatabasePassword: database_password, DatabaseAddress: database_address, DatabasePort: database_port, - DatabaseName: database_name, - WebserverPort: webserver_port, + DatabaseName: database_name, + WebserverPort: webserver_port, }, nil } diff --git a/common/logger.go b/common/logger.go index 5a3cb17..f1a74b9 100644 --- a/common/logger.go +++ b/common/logger.go @@ -5,8 +5,8 @@ import ( "github.com/rs/zerolog/log" ) -/// TODO : take a prefix to know where the logs come -/// from. E..g "[URCHIN]" and "[URCHIN-ADMIN]" +// / TODO : take a prefix to know where the logs come +// / from. E..g "[URCHIN]" and "[URCHIN-ADMIN]" func SetupLogger() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix log.Info().Msg("Logger created") diff --git a/common/post.go b/common/post.go index baa3205..7acd8d1 100644 --- a/common/post.go +++ b/common/post.go @@ -1,8 +1,8 @@ package common type Post struct { - Title string + Title string Content string Excerpt string - Id int + Id int } diff --git a/database/database.go b/database/database.go index eac7c80..7edf72d 100644 --- a/database/database.go +++ b/database/database.go @@ -2,6 +2,7 @@ package database import ( "database/sql" + "errors" "fmt" "time" @@ -55,8 +56,8 @@ func (db *Database) GetPost(post_id int) (common.Post, error) { return post, nil } -/// This function adds a post to the database -func (db *Database) AddPost(title string, excerpt string, content string) (int, error) { +// / This function adds a post to the database +func (db *Database) AddPost(title string, excerpt string, content string) (int, error) { res, err := db.Connection.Exec("INSERT INTO posts(content, title, excerpt) VALUES(?, ?, ?)", content, title, excerpt) if err != nil { return -1, err @@ -74,16 +75,18 @@ func (db *Database) AddPost(title string, excerpt string, content string) (int, return int(id), nil } -/// This function changes a post based on the values -/// provided. Note that empty strings will mean that -/// the value will not be updated. +// / This function changes a post based on the values +// / provided. Note that empty strings will mean that +// / the value will not be updated. func (db *Database) ChangePost(id int, title string, excerpt string, content string) error { tx, err := db.Connection.Begin() if err != nil { return err } - defer tx.Rollback() - + defer func() { + err = errors.Join(tx.Rollback()) + }() + if len(title) > 0 { _, err := tx.Exec("UPDATE posts SET title = ? WHERE id = ?;", title, id) if err != nil { @@ -112,9 +115,9 @@ func (db *Database) ChangePost(id int, title string, excerpt string, content str return nil } -/// This function changes a post based on the values -/// provided. Note that empty strings will mean that -/// the value will not be updated. +// / This function changes a post based on the values +// / provided. Note that empty strings will mean that +// / the value will not be updated. func (db *Database) DeletePost(id int) error { if _, err := db.Connection.Exec("DELETE FROM posts WHERE id=?;", id); err != nil { return err