From 2ecfe7f8acaa2bda12bd69c4377b4359a4e2c79b Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Sat, 12 Oct 2024 23:07:49 +0500 Subject: [PATCH] Basic HTTP servers (#2) and various improvements over docs. --- .gitea/issue_template/bug-report.yaml | 1 - .gitea/issue_template/feature-request.yaml | 1 - CHANGELOG.md | 7 ++ CONTRIBUTING.md | 6 ++ DCO | 1 - README.md | 4 +- go.mod | 31 +++++++ go.sum | 89 ++++++++++++++++++ server/cmd/featurer/main.go | 2 + server/internal/services/core/http.go | 37 ++++++++ .../services/core/http/generic_handlers.go | 29 ++++++ server/internal/services/core/http/http.go | 90 +++++++++++++++++++ server/internal/services/core/http/logger.go | 29 ++++++ .../services/core/http/middlewares.go | 23 +++++ server/internal/services/core/http/replies.go | 10 +++ server/internal/services/core/http/servers.go | 56 ++++++++++++ server/localdev/featurer/docker-compose.yaml | 6 ++ 17 files changed, 417 insertions(+), 5 deletions(-) create mode 100644 go.sum create mode 100644 server/internal/services/core/http.go create mode 100644 server/internal/services/core/http/generic_handlers.go create mode 100644 server/internal/services/core/http/http.go create mode 100644 server/internal/services/core/http/logger.go create mode 100644 server/internal/services/core/http/middlewares.go create mode 100644 server/internal/services/core/http/replies.go create mode 100644 server/internal/services/core/http/servers.go diff --git a/.gitea/issue_template/bug-report.yaml b/.gitea/issue_template/bug-report.yaml index 0282943..1b7a861 100644 --- a/.gitea/issue_template/bug-report.yaml +++ b/.gitea/issue_template/bug-report.yaml @@ -31,4 +31,3 @@ body: options: - label: I have read the "Submitting issues" section in [CONTRIBUTING.md](https://code.pztrn.name/apps/featurer/CONTRIBUTING.md) required: true - visible: [form] diff --git a/.gitea/issue_template/feature-request.yaml b/.gitea/issue_template/feature-request.yaml index cfd21c5..d08dce8 100644 --- a/.gitea/issue_template/feature-request.yaml +++ b/.gitea/issue_template/feature-request.yaml @@ -24,4 +24,3 @@ body: options: - label: I have read the "Submitting issues" section in [CONTRIBUTING.md](https://code.pztrn.name/apps/featurer/CONTRIBUTING.md) required: true - visible: [form] diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b6d01..daf3103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,4 +25,11 @@ In the very end of this file you should place links to diffs between versions, l ## [Unreleased] +The very first release. + +### Added + +- HTTP servers (API and CMS for future use). +- Data storage. + > [Unreleased]: https://code.pztrn.name/apps/featurer/compare/v0.0.1...main diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 607af61..2718620 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,10 @@ This badge could place only people that present in [maintainers list](MAINTAINER ## Code +### Before you start contributing code + +Please fill bug report or feature request first. Pull requests without filled issue won't be accepted. + ### Linting Please use [golangci-lint](https://golangci-lint.run/) with configuration from this repository. @@ -46,3 +50,5 @@ Featurer uses next branching agreement: 1. `main` branch is a subject of constant changes. 2. Fork repository, make changes, create pull request. 3. Releases are cut from release branches named `release/MAJOR.MINOR`. + +These agreement will be enforced after first contributor appears! :). diff --git a/DCO b/DCO index 49b8cb0..b348588 100644 --- a/DCO +++ b/DCO @@ -6,7 +6,6 @@ Copyright (C) 2004, 2006 The Linux Foundation and its contributors. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: diff --git a/README.md b/README.md index a00caac..5cc4e50 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Features toggling server. ## Documentation -Complete documentation is stored in [docs](/docs/INDEX.md) directory. +Complete documentation is stored in [docs](docs/INDEX.md) directory. -If you want to contribute to project - please take a look at [CONTRIBUTING](/CONTRIBUTING.md) file. +If you want to contribute to project - please take a look at [CONTRIBUTING](CONTRIBUTING.md) file. ## Licensing diff --git a/go.mod b/go.mod index 7dd476e..22a71b1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,34 @@ module go.dev.pztrn.name/featurer go 1.23 + +require github.com/gin-gonic/gin v1.10.0 + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7f08abb --- /dev/null +++ b/go.sum @@ -0,0 +1,89 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/server/cmd/featurer/main.go b/server/cmd/featurer/main.go index 7f39aeb..ede1b6b 100644 --- a/server/cmd/featurer/main.go +++ b/server/cmd/featurer/main.go @@ -5,6 +5,7 @@ import ( "go.dev.pztrn.name/featurer/server/internal/application" "go.dev.pztrn.name/featurer/server/internal/services/core/datastore" + "go.dev.pztrn.name/featurer/server/internal/services/core/http" ) func main() { @@ -23,6 +24,7 @@ func main() { // Initializing core services first. checkError(datastore.Initialize(app)) + checkError(http.Initialize(app)) // Then - features services. diff --git a/server/internal/services/core/http.go b/server/internal/services/core/http.go new file mode 100644 index 0000000..a66337a --- /dev/null +++ b/server/internal/services/core/http.go @@ -0,0 +1,37 @@ +package core + +import ( + "errors" + + "github.com/gin-gonic/gin" +) + +const ( + // ServerNameAPI is a HTTP server name for API. + ServerNameAPI = "api" + // ServerNameCMS is a HTTP server name for CMS. + ServerNameCMS = "cms" + // ServiceNameHTTP is a name for service responsible for HTTP servers controlloing. + ServiceNameHTTP = "http servers" +) + +var ( + // ErrHTTP указывает на ошибку в сервисе управления HTTP серверами. + ErrHTTP = errors.New("HTTP server") + // ErrHTTPAssetsBundleAlreadyRegistered говорит о попытке зарегистрировать бандл с ассетами с уже использованным + // именем. + ErrHTTPAssetsBundleAlreadyRegistered = errors.New("assets bundle already registered") + // ErrHTTPCMSAddressInvalid говорит о неправильно заданном адресе для прослушки, который был получен из + // переменных окружения. + ErrHTTPCMSAddressInvalid = errors.New("CMS server address invalid") + // ErrHTTPServerNotFound возникает при попытке получить неизвестный HTTP сервер. + ErrHTTPServerNotFound = errors.New("HTTP server not found") + // ErrHTTPServiceIsInvalid говорит о неправильной имплементации сервиса управления HTTP серверами. + ErrHTTPServiceIsInvalid = errors.New("service implementation is invalid") +) + +// HTTP это интерфейс для сервиса управления HTTP серверами. +type HTTP interface { + RegisterHandler(serverName, method, path string, handler func(*gin.Context)) error + ReplyJSON(ctx *gin.Context, statusCode int, data interface{}) +} diff --git a/server/internal/services/core/http/generic_handlers.go b/server/internal/services/core/http/generic_handlers.go new file mode 100644 index 0000000..0d8f456 --- /dev/null +++ b/server/internal/services/core/http/generic_handlers.go @@ -0,0 +1,29 @@ +package http + +import ( + "fmt" + + "github.com/gin-gonic/gin" + + "go.dev.pztrn.name/featurer/server/internal/services/core" +) + +func (h *http) RegisterHandler(serverName, method, path string, handler func(*gin.Context)) error { + h.serversMutex.RLock() + defer h.serversMutex.RUnlock() + + srv, found := h.servers[serverName] + if !found { + return fmt.Errorf( + "%w: registering handler '%s %s': %w", + core.ErrHTTP, + method, + path, + core.ErrHTTPServerNotFound, + ) + } + + _ = srv.Handle(method, path, handler) + + return nil +} diff --git a/server/internal/services/core/http/http.go b/server/internal/services/core/http/http.go new file mode 100644 index 0000000..53afc9c --- /dev/null +++ b/server/internal/services/core/http/http.go @@ -0,0 +1,90 @@ +package http + +import ( + "fmt" + "log/slog" + stdhttp "net/http" + "os" + "strings" + "sync" + + "go.dev.pztrn.name/featurer/server/internal/application" + "go.dev.pztrn.name/featurer/server/internal/services/core" + + "github.com/gin-gonic/gin" +) + +const ( + subsystem = "HTTP servers" +) + +var _ = core.HTTP(&http{}) + +type http struct { + app *application.Application + servers map[string]*gin.Engine + httpServers map[string]*stdhttp.Server + serversMutex sync.RWMutex +} + +// Initialize initializes service. +func Initialize(app *application.Application) error { + httpSrv := &http{ + app: app, + } + + if err := app.RegisterService(httpSrv); err != nil { + return fmt.Errorf("%w: register service: %w", core.ErrHTTP, err) + } + + return nil +} + +func (h *http) ConnectDependencies() error { + return nil +} + +func (h *http) GetName() string { + return core.ServiceNameHTTP +} + +func (h *http) Initialize() error { + slog.Info("Initializing service...", "service", subsystem) + + h.servers = make(map[string]*gin.Engine) + h.httpServers = make(map[string]*stdhttp.Server) + + serversNames := []string{core.ServerNameAPI, core.ServerNameCMS} + + for _, name := range serversNames { + addr, found := os.LookupEnv("FEATURER_" + strings.ToUpper(name) + "_SERVER_ADDRESS") + if !found { + return fmt.Errorf("%w: getting address for server '%s' from env: not found", core.ErrHTTP, name) + } + + h.createServer(name, addr) + } + + return nil +} + +func (h *http) LaunchStartupTasks() error { + h.startServers() + + return nil +} + +func (h *http) Shutdown() error { + h.serversMutex.Lock() + defer h.serversMutex.Unlock() + + for name, server := range h.httpServers { + slog.Info("Stopping HTTP server...", "service", subsystem, "server", name) + + if err := server.Shutdown(h.app.GetContext()); err != nil { + slog.Error("Failed to stop HTTP server!", "service", subsystem, "server", name, "error", err.Error()) + } + } + + return nil +} diff --git a/server/internal/services/core/http/logger.go b/server/internal/services/core/http/logger.go new file mode 100644 index 0000000..fb950fe --- /dev/null +++ b/server/internal/services/core/http/logger.go @@ -0,0 +1,29 @@ +package http + +import ( + "log/slog" + "time" + + "github.com/gin-gonic/gin" +) + +func (h *http) requestLogger(serverName string) gin.HandlerFunc { + return func(ctx *gin.Context) { + startTime := time.Now() + + ctx.Next() + + slog.Info( + "HTTP request processed", + "service", subsystem, + "server", serverName, + "client-ip", ctx.ClientIP(), + "user-agent", ctx.Request.UserAgent(), + "path", ctx.Request.Method+" "+ctx.Request.URL.String(), + "request-size", ctx.Request.ContentLength, + "response-code", ctx.Writer.Status(), + "response-length", ctx.Writer.Size(), + "response-time", time.Since(startTime).String(), + ) + } +} diff --git a/server/internal/services/core/http/middlewares.go b/server/internal/services/core/http/middlewares.go new file mode 100644 index 0000000..4c221e5 --- /dev/null +++ b/server/internal/services/core/http/middlewares.go @@ -0,0 +1,23 @@ +package http + +import ( + "fmt" + + "go.dev.pztrn.name/featurer/server/internal/services/core" + + "github.com/gin-gonic/gin" +) + +func (h *http) RegisterMiddleware(serverName string, middleware gin.HandlerFunc) error { + h.serversMutex.RLock() + defer h.serversMutex.RUnlock() + + router, found := h.servers[serverName] + if !found { + return fmt.Errorf("%w: registering middleware: %w", core.ErrHTTP, core.ErrHTTPServerNotFound) + } + + _ = router.Use(middleware) + + return nil +} diff --git a/server/internal/services/core/http/replies.go b/server/internal/services/core/http/replies.go new file mode 100644 index 0000000..f816ce4 --- /dev/null +++ b/server/internal/services/core/http/replies.go @@ -0,0 +1,10 @@ +package http + +import ( + "github.com/gin-gonic/gin" +) + +func (h *http) ReplyJSON(ctx *gin.Context, statusCode int, data interface{}) { + ctx.Writer.Header().Add("Content-Type", "application/json") + ctx.JSON(statusCode, data) +} diff --git a/server/internal/services/core/http/servers.go b/server/internal/services/core/http/servers.go new file mode 100644 index 0000000..74f6668 --- /dev/null +++ b/server/internal/services/core/http/servers.go @@ -0,0 +1,56 @@ +package http + +import ( + "log/slog" + stdhttp "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func (h *http) createServer(name, address string) { + engine := gin.New() + //nolint:gomnd,mnd + httpServer := &stdhttp.Server{ + Addr: address, + Handler: engine.Handler(), + // ToDo: move into configuration. + ReadTimeout: time.Second * 5, + WriteTimeout: time.Second * 10, + } + + h.serversMutex.Lock() + h.servers[name] = engine + h.httpServers[name] = httpServer + h.serversMutex.Unlock() + + // ToDo: trusted proxies. + + engine.Use(h.requestLogger(name)) +} + +func (h *http) startServers() { + h.serversMutex.RLock() + defer h.serversMutex.RUnlock() + + for srvName, httpServer := range h.httpServers { + go func(name string, srv *stdhttp.Server) { + slog.Info( + "Starting HTTP server...", + "service", subsystem, + "server-name", name, + "server-address", srv.Addr, + ) + + if err := srv.ListenAndServe(); err != nil { + slog.Error( + "Failed to start HTTP server!", + "service", subsystem, + "server-name", name, + "server-address", srv.Addr, + "error", err.Error(), + ) + } + }(srvName, httpServer) + } +} diff --git a/server/localdev/featurer/docker-compose.yaml b/server/localdev/featurer/docker-compose.yaml index cdfdb0e..7011fe2 100644 --- a/server/localdev/featurer/docker-compose.yaml +++ b/server/localdev/featurer/docker-compose.yaml @@ -5,9 +5,15 @@ services: build: context: ../../../ dockerfile: server/Dockerfile.featurer + ports: + - "15000:5000" + - "15001:5001" networks: featurer: ipv4_address: 248.248.0.2 + environment: + FEATURER_API_SERVER_ADDRESS: "0.0.0.0:5000" + FEATURER_CMS_SERVER_ADDRESS: "0.0.0.0:5001" cap_add: - SYS_PTRACE