From 2c13e3f3803134b0295eb822621ab56fc4bb528e Mon Sep 17 00:00:00 2001 From: "Stanislav N. aka pztrn" Date: Mon, 15 Sep 2025 09:33:25 +0500 Subject: [PATCH] Basic server app with WS connection. --- .vscode/tasks.json | 2 +- .../services/core/database/database.go | 5 +- commons/ids.go | 7 ++ go.mod | 14 ++- go.sum | 51 +++++++---- server/cmd/bunkerd/main.go | 24 +++++ server/internal/application/logger.go | 27 +++++- server/internal/services/core/database.go | 44 +++++++++ .../services/core/database/connection.go | 83 +++++++++++++++++ .../services/core/database/database.go | 73 +++++++++++++++ .../services/core/database/migrations.go | 82 +++++++++++++++++ .../services/core/database/queries.go | 69 ++++++++++++++ .../services/core/database/transaction.go | 81 +++++++++++++++++ server/internal/services/core/httpserver.go | 14 +++ .../services/core/httpserver/httpserver.go | 91 +++++++++++++++++++ .../services/core/httpserver/server.go | 85 +++++++++++++++++ server/internal/services/core/options.go | 14 +++ .../services/core/options/migrations.go | 17 ++++ .../core/options/migrations/0001_initial.sql | 12 +++ .../internal/services/core/options/options.go | 79 ++++++++++++++++ server/localdevzone/Taskfile.yml | 4 + .../bunkerd/docker-compose.debug.yaml | 6 +- .../localdevzone/bunkerd/docker-compose.yaml | 6 +- server/localdevzone/postgresql/Taskfile.yml | 40 ++++++++ .../postgresql/docker-compose.yaml | 25 +++++ 25 files changed, 928 insertions(+), 27 deletions(-) create mode 100644 commons/ids.go create mode 100644 server/internal/services/core/database.go create mode 100644 server/internal/services/core/database/connection.go create mode 100644 server/internal/services/core/database/database.go create mode 100644 server/internal/services/core/database/migrations.go create mode 100644 server/internal/services/core/database/queries.go create mode 100644 server/internal/services/core/database/transaction.go create mode 100644 server/internal/services/core/httpserver.go create mode 100644 server/internal/services/core/httpserver/httpserver.go create mode 100644 server/internal/services/core/httpserver/server.go create mode 100644 server/internal/services/core/options.go create mode 100644 server/internal/services/core/options/migrations.go create mode 100644 server/internal/services/core/options/migrations/0001_initial.sql create mode 100644 server/internal/services/core/options/options.go create mode 100644 server/localdevzone/postgresql/Taskfile.yml create mode 100644 server/localdevzone/postgresql/docker-compose.yaml diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 73b8117..8a15af1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,7 +4,7 @@ { "label": "Run bunkerd debug build", "type": "shell", - "command": "task server:localdev:bunkerd:build-debug && task server:localdev:bunkerd:restart-debug", + "command": "task server:localdev:bunkerd:stop; task server:localdev:bunkerd:build-debug && task server:localdev:bunkerd:restart-debug", "group": { "kind": "test", "isDefault": true diff --git a/client/internal/services/core/database/database.go b/client/internal/services/core/database/database.go index 82adc7e..ebcc452 100644 --- a/client/internal/services/core/database/database.go +++ b/client/internal/services/core/database/database.go @@ -34,7 +34,7 @@ func Initialize(app *application.Application) error { } if err := app.RegisterService(db); err != nil { - return fmt.Errorf("%w: %w", core.ErrMainWindow, err) + return fmt.Errorf("%w: %w", core.ErrDatabase, err) } return nil @@ -71,7 +71,7 @@ func (d *database) ConnectDependencies() error { mainWindow, valid := mainWindowRaw.(core.MainWindow) if !valid { - return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrMainWindowIsInvalid) + return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrDatabaseIsInvalid) } d.mainWindow = mainWindow @@ -106,7 +106,6 @@ func (d *database) Name() string { } func (d *database) LaunchStartupTasks() error { - // Запускаем миграции. if err := d.applyMigrations(); err != nil { return fmt.Errorf("launch startup tasks: %w", err) } diff --git a/commons/ids.go b/commons/ids.go new file mode 100644 index 0000000..4829849 --- /dev/null +++ b/commons/ids.go @@ -0,0 +1,7 @@ +// File "ids.go" stores all static identificators that is used across Bunker server. +package commons + +const ( + // SystemUserID is an UUID for "System" user. With this ID server's settings are stored in database. + SystemUserID = "00000001-0000-0000-0000-000000000000" +) diff --git a/go.mod b/go.mod index 739cdf6..d5867b0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.0 require ( fyne.io/fyne/v2 v2.6.3 + github.com/jackc/pgx/v5 v5.7.6 github.com/jmoiron/sqlx v1.4.0 github.com/pressly/goose/v3 v3.25.0 github.com/shirou/gopsutil/v3 v3.24.5 @@ -13,6 +14,7 @@ require ( require ( fyne.io/systray v1.11.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect + github.com/coder/websocket v1.8.14 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fredbi/uri v1.1.0 // indirect @@ -30,6 +32,9 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/hack-pad/go-indexeddb v0.3.2 // indirect github.com/hack-pad/safejs v0.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/kr/text v0.2.0 // indirect @@ -53,12 +58,13 @@ require ( github.com/yuin/goldmark v1.7.8 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.42.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/image v0.24.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index a13374e..70a61e5 100644 --- a/go.sum +++ b/go.sum @@ -6,7 +6,10 @@ fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -53,12 +56,22 @@ github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQb github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -77,8 +90,6 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -89,6 +100,8 @@ github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjG github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA= github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= @@ -103,6 +116,9 @@ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiY github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= @@ -115,31 +131,34 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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= modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= diff --git a/server/cmd/bunkerd/main.go b/server/cmd/bunkerd/main.go index 5e074bb..8d1131a 100644 --- a/server/cmd/bunkerd/main.go +++ b/server/cmd/bunkerd/main.go @@ -1,10 +1,14 @@ package main import ( + "log/slog" "os" "bunker/commons" "bunker/server/internal/application" + "bunker/server/internal/services/core/database" + "bunker/server/internal/services/core/httpserver" + "bunker/server/internal/services/core/options" ) func main() { @@ -26,6 +30,16 @@ func main() { os.Exit(1) } + checkError(lgr, database.Initialize(app)) + checkError(lgr, options.Initialize(app)) + checkError(lgr, httpserver.Initialize(app)) + + if err := app.Start(); err != nil { + lgr.Error("Failed to start bunkerd", "error", err.Error()) + + os.Exit(2) + } + lgr.Info("bunkerd started.") <-app.ShutdownChan() @@ -37,3 +51,13 @@ func main() { os.Exit(1) } } + +func checkError(logger *slog.Logger, err error) { + if err == nil { + return + } + + logger.Error("Failed to initialize bunkerd.", "error", err.Error()) + + os.Exit(2) +} diff --git a/server/internal/application/logger.go b/server/internal/application/logger.go index 4c95b32..261074a 100644 --- a/server/internal/application/logger.go +++ b/server/internal/application/logger.go @@ -3,12 +3,37 @@ package application import ( "log/slog" "os" + "strings" +) + +const ( + defaultLogLevel slog.Level = slog.LevelInfo + + logLevelEnvVar = "BUNKERD_LOG_LEVEL" ) func (a *Application) initializeLogger() { + logLevel := defaultLogLevel + + logLevelAsString, found := os.LookupEnv(logLevelEnvVar) + if found { + switch strings.ToLower(logLevelAsString) { + case "debug": + logLevel = slog.LevelDebug + case "info": + logLevel = slog.LevelInfo + case "warn": + logLevel = slog.LevelWarn + case "error": + logLevel = slog.LevelError + } + } + + slog.Warn("Setting log level.", "level", logLevel.String()) + a.baseLogger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ AddSource: true, - Level: slog.LevelDebug, + Level: logLevel, })) a.appLogger = a.baseLogger.With("module", "application") diff --git a/server/internal/services/core/database.go b/server/internal/services/core/database.go new file mode 100644 index 0000000..8d9a432 --- /dev/null +++ b/server/internal/services/core/database.go @@ -0,0 +1,44 @@ +package core + +import ( + "context" + "errors" + "io/fs" + + "github.com/jmoiron/sqlx" +) + +// ServiceNameDatabase is a name for database service. +const ServiceNameDatabase = "core/database" + +var ( + // ErrDatabase indicates that error appeared somewhere in database service. + ErrDatabase = errors.New("database service") + // ErrDatabaseIsInvalid indicates that database service implementation is invalid. + ErrDatabaseIsInvalid = errors.New("database service implementation is invalid") +) + +// Database is an interface for database service. +type Database interface { + // Exec is a proxy for ExecContext from sqlx. + Exec(ctx context.Context, query string, params ...interface{}) error + // Get is a proxy for GetContext from sqlx. + Get(ctx context.Context, target interface{}, query string, params ...interface{}) error + // NamedExec is a proxy for NamedExecContext from sqlx. + NamedExec(ctx context.Context, query string, param interface{}) error + // RegisterMigrations registers migrations for applying from other services. Migrations should reside + // in "migrations" directory in passed filesystem. + RegisterMigrations(moduleName string, fs fs.FS) error + // Select is a proxy for SelectContext from sqlx. + Select(ctx context.Context, target interface{}, query string, params ...interface{}) error + // Transaction is a wrapper for transactions processing which wraps sqlx's transactions. + Transaction(ctx context.Context) (DatabaseTransaction, error) +} + +// DatabaseTransaction is an interface for database transactions controllers implementations. +type DatabaseTransaction interface { + Apply(steps ...TransactionFunc) error +} + +// TransactionFunc is a function that is used in transactions to mangle with data. +type TransactionFunc func(*sqlx.Tx) error diff --git a/server/internal/services/core/database/connection.go b/server/internal/services/core/database/connection.go new file mode 100644 index 0000000..935f758 --- /dev/null +++ b/server/internal/services/core/database/connection.go @@ -0,0 +1,83 @@ +package database + +import ( + "errors" + "fmt" + "net/url" + "os" + "strconv" + "strings" + + "github.com/jmoiron/sqlx" + // postgresql driver. + _ "github.com/jackc/pgx/v5/stdlib" +) + +const ( + databaseDSNEnvVar = "BUNKERD_DATABASE_DSN" + databaseMaxIdleConnsEnvVar = "BUNKERD_DATABASE_MAX_IDLE_CONNS" + databaseMaxOpenedConnsEnvVar = "BUNKERD_DATABASE_MAX_OPENED_CONNS" +) + +var ( + errDSNInvalid = errors.New("BUNKERD_DATABASE_DSN environment variable is empty or invalid") + errNoMaxIdleConns = errors.New("no BUNKERD_DATABASE_MAX_IDLE_CONNS defined") + errNoMaxOpenedConns = errors.New("no BUNKERD_DATABASE_MAX_OPENED_CONNS defined") + errPostgresOnlySupported = errors.New("only PostgreSQL database is currently supported") +) + +func (d *database) initializeConnection() error { + // Getting database DSN from environment as well as other required settings. + dsn, found := os.LookupEnv(databaseDSNEnvVar) + if !found { + return fmt.Errorf("initialize connection: getting database DSN: %w", errDSNInvalid) + } + + maxOpenedConnsRaw, found := os.LookupEnv(databaseMaxOpenedConnsEnvVar) + if !found { + return fmt.Errorf("initialize connection: getting maximum number of opened conections: %w", errNoMaxOpenedConns) + } + + maxOpenedConns, err := strconv.ParseInt(maxOpenedConnsRaw, 10, 64) + if err != nil { + return fmt.Errorf("initialize connection: parsing maximum number of opened conections: %w", err) + } + + maxIdleConnsRaw, found := os.LookupEnv(databaseMaxIdleConnsEnvVar) + if !found { + return fmt.Errorf("initialize connection: getting maximum number of idle conections: %w", errNoMaxIdleConns) + } + + maxIdleConns, err := strconv.ParseInt(maxIdleConnsRaw, 10, 64) + if err != nil { + return fmt.Errorf("initialize connection: parsing maximum number of opened conections: %w", err) + } + + // While database/sql (and sqlx) supports all possible DSN formats, we will force user to use DSN in form + // "proto://user:passowrd@host:port/dbname" as it is easier to parse. + if _, err := url.Parse(dsn); err != nil { + return fmt.Errorf("initialize connection: validate DSN: %w", err) + } + + // Currently we're support only postgresql, but this may change in future. + if !strings.HasPrefix(dsn, "postgres://") { + return fmt.Errorf("initialize connection: validate DSN: %w", errPostgresOnlySupported) + } + + proto := strings.Split(dsn, ":")[0] + if proto == "postgres" { + proto = "pgx" + } + + db, err := sqlx.Open(proto, dsn) + if err != nil { + return fmt.Errorf("initialize connection: open database: %w", err) + } + + d.db = db + + d.db.SetMaxOpenConns(int(maxOpenedConns)) + d.db.SetMaxIdleConns(int(maxIdleConns)) + + return nil +} diff --git a/server/internal/services/core/database/database.go b/server/internal/services/core/database/database.go new file mode 100644 index 0000000..c6c22bd --- /dev/null +++ b/server/internal/services/core/database/database.go @@ -0,0 +1,73 @@ +package database + +import ( + "fmt" + "io/fs" + "log/slog" + + "bunker/server/internal/application" + "bunker/server/internal/services/core" + + "github.com/jmoiron/sqlx" +) + +var _ = core.Database(&database{}) + +type database struct { + app *application.Application + db *sqlx.DB + logger *slog.Logger + migrations map[string]fs.FS + version int64 +} + +// Initialize initializes service. +func Initialize(app *application.Application) error { + db := &database{ + app: app, + } + + if err := app.RegisterService(db); err != nil { + return fmt.Errorf("%w: %w", core.ErrDatabase, err) + } + + return nil +} + +func (d *database) Configure() error { + if err := d.initializeConnection(); err != nil { + return fmt.Errorf("configure: %w", err) + } + + return nil +} + +func (d *database) ConnectDependencies() error { + return nil +} + +func (d *database) Initialize() error { + d.logger = d.app.NewLogger("service", core.ServiceNameDatabase) + + d.logger.Info("Initializing...") + + d.migrations = make(map[string]fs.FS, 0) + + return nil +} + +func (d *database) Name() string { + return core.ServiceNameDatabase +} + +func (d *database) LaunchStartupTasks() error { + if err := d.applyMigrations(); err != nil { + return fmt.Errorf("launch startup tasks: %w", err) + } + + return nil +} + +func (d *database) Shutdown() error { + return nil +} diff --git a/server/internal/services/core/database/migrations.go b/server/internal/services/core/database/migrations.go new file mode 100644 index 0000000..fef4e1b --- /dev/null +++ b/server/internal/services/core/database/migrations.go @@ -0,0 +1,82 @@ +package database + +import ( + "errors" + "fmt" + "io/fs" + "log/slog" + "sort" + "strings" + + "bunker/commons" + "bunker/server/internal/services/core" + + "github.com/pressly/goose/v3" +) + +var errMigrationsAlreadyRegistered = errors.New("migrations already registered") + +func (d *database) applyMigrations() error { + d.logger.Info("Migrating database...") + + modules := make([]string, 0) + + for module := range d.migrations { + modules = append(modules, module) + } + + sort.Strings(modules) + + _ = goose.SetDialect(string(goose.DialectPostgres)) + + gooseLogger := commons.NewGooseLogger(d.logger) + goose.SetLogger(gooseLogger) + + for _, module := range modules { + d.logger.Info("Migrating database for module...", "module", module) + + goose.SetBaseFS(d.migrations[module]) + goose.SetTableName(strings.ReplaceAll(module, "/", "_") + "_migrations") + + if err := goose.Up(d.db.DB, "migrations"); err != nil { + return fmt.Errorf("%w: applying migrations for module '%s': %w", core.ErrDatabase, module, err) + } + + moduleDBVersion, err := goose.GetDBVersion(d.db.DB) + if err != nil { + return fmt.Errorf("%w: get database version for module '%s': %w", core.ErrDatabase, module, err) + } + + d.version += moduleDBVersion + + d.logger.Info( + "Database for module migrated to latest version", + "module", module, + "module_db_version", moduleDBVersion, + "db_version", d.version, + ) + } + + d.logger.Info("Database migrated.", "version", d.version) + + return nil +} + +func (d *database) RegisterMigrations(moduleName string, fSys fs.FS) error { + slog.Debug("Registering migrations for service.", "service", moduleName) + + if _, found := d.migrations[moduleName]; found { + return fmt.Errorf( + "%w: RegisterMigrations: module '%s': %w", + core.ErrDatabase, + moduleName, + errMigrationsAlreadyRegistered, + ) + } + + d.migrations[moduleName] = fSys + + slog.Debug("Migrations for service successfully registered.", "service", moduleName) + + return nil +} diff --git a/server/internal/services/core/database/queries.go b/server/internal/services/core/database/queries.go new file mode 100644 index 0000000..ddacfed --- /dev/null +++ b/server/internal/services/core/database/queries.go @@ -0,0 +1,69 @@ +package database + +import ( + "context" + "fmt" + "strings" + + "bunker/server/internal/services/core" +) + +// Exec is a proxy for ExecContext from sqlx. +func (d *database) Exec(ctx context.Context, query string, params ...interface{}) error { + if strings.Contains(query, "?") { + query = d.db.Rebind(query) + } + + d.logger.Debug("Executing query.", "query", query, "params", fmt.Sprintf("%+v", params)) + + if _, err := d.db.ExecContext(ctx, query, params...); err != nil { + return fmt.Errorf("%w: failed to Exec(): %w", core.ErrDatabase, err) + } + + return nil +} + +// Get is a proxy for GetContext from sqlx. +func (d *database) Get(ctx context.Context, target interface{}, query string, params ...interface{}) error { + if strings.Contains(query, "?") { + query = d.db.Rebind(query) + } + + d.logger.Debug("Getting single data from database with query.", "query", query, "params", fmt.Sprintf("%+v", params)) + + if err := d.db.GetContext(ctx, target, query, params...); err != nil { + return fmt.Errorf("%w: failed to Get(): %w", core.ErrDatabase, err) + } + + return nil +} + +// NamedExec is a proxy for NamedExecContext from sqlx. +func (d *database) NamedExec(ctx context.Context, query string, param interface{}) error { + if strings.Contains(query, "?") { + query = d.db.Rebind(query) + } + + d.logger.Debug("Executing named query.", "query", query, "params", fmt.Sprintf("%+v", param)) + + if _, err := d.db.NamedExecContext(ctx, query, param); err != nil { + return fmt.Errorf("%w: failed to NamedExec(): %w", core.ErrDatabase, err) + } + + return nil +} + +// Select is a proxy for SelectContext from sqlx. +func (d *database) Select(ctx context.Context, target interface{}, query string, params ...interface{}) error { + if strings.Contains(query, "?") { + query = d.db.Rebind(query) + } + + d.logger.Debug("Selecting from database with query.", "query", query, "params", fmt.Sprintf("%+v", params)) + + if err := d.db.SelectContext(ctx, target, query, params...); err != nil { + return fmt.Errorf("%w: failed to Select(): %w", core.ErrDatabase, err) + } + + return nil +} diff --git a/server/internal/services/core/database/transaction.go b/server/internal/services/core/database/transaction.go new file mode 100644 index 0000000..2e8f3a6 --- /dev/null +++ b/server/internal/services/core/database/transaction.go @@ -0,0 +1,81 @@ +package database + +import ( + "context" + "fmt" + "log/slog" + + "bunker/server/internal/services/core" + + "github.com/jmoiron/sqlx" +) + +type transaction struct { + transaction *sqlx.Tx + logger *slog.Logger +} + +func (d *database) Transaction(ctx context.Context) (core.DatabaseTransaction, error) { + txn, err := d.db.BeginTxx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("%w: starting transaction: %w", core.ErrDatabase, err) + } + + txHandler := &transaction{ + transaction: txn, + logger: d.logger.With("module", "transactioner"), + } + + return txHandler, nil +} + +func (t *transaction) Apply(steps ...core.TransactionFunc) error { + for stepNumber, stepFunc := range steps { + if err := stepFunc(t.transaction); err != nil { + t.logger.Error( + "Error occurred.", + "step", stepNumber, + "error", err.Error(), + "module", "core/database", + "subsystem", "transaction", + ) + + if rollbackErr := t.transaction.Rollback(); rollbackErr != nil { + t.logger.Error( + "Transaction rollback failed.", + "error", err.Error(), + "module", "core/database", + "subsystem", "transaction", + ) + + return fmt.Errorf("%w: transaction rollback: %w", core.ErrDatabase, rollbackErr) + } + + return err + } + } + + if err := t.transaction.Commit(); err != nil { + t.logger.Error( + "Transaction commit failed.", + "error", err.Error(), + "module", "core/database", + "subsystem", "transaction", + ) + + if rollbackErr := t.transaction.Rollback(); rollbackErr != nil { + t.logger.Error( + "Transaction rollback failed.", + "error", err.Error(), + "module", "core/database", + "subsystem", "transaction", + ) + + return fmt.Errorf("%w: transaction rollback: %w", core.ErrDatabase, rollbackErr) + } + + return fmt.Errorf("%w: transaction commit: %w", core.ErrDatabase, err) + } + + return nil +} diff --git a/server/internal/services/core/httpserver.go b/server/internal/services/core/httpserver.go new file mode 100644 index 0000000..301ae93 --- /dev/null +++ b/server/internal/services/core/httpserver.go @@ -0,0 +1,14 @@ +package core + +import ( + "errors" +) + +// ServiceNameHTTPServer is a name for HTTP server service. +const ServiceNameHTTPServer = "core/http_server" + +// ErrHTTPServerIsInvalid appears when HTTP server service implementation is invalid. +var ErrHTTPServerIsInvalid = errors.New("HTTP server service implementation is invalid") + +// HTTPServer is an interface for HTTP server service. +type HTTPServer interface{} diff --git a/server/internal/services/core/httpserver/httpserver.go b/server/internal/services/core/httpserver/httpserver.go new file mode 100644 index 0000000..bc4c12c --- /dev/null +++ b/server/internal/services/core/httpserver/httpserver.go @@ -0,0 +1,91 @@ +package httpserver + +import ( + "errors" + "fmt" + "log/slog" + "net/http" + + "bunker/server/internal/application" + "bunker/server/internal/services/core" +) + +var ( + _ = core.HTTPServer(&httpServer{}) + + errHTTPServer = errors.New("HTTP server core service") +) + +type httpServer struct { + app *application.Application + logger *slog.Logger + db core.Database + httpSrv *http.Server +} + +// Initialize initializes service. +func Initialize(app *application.Application) error { + httpSrv := &httpServer{ + app: app, + } + + if err := app.RegisterService(httpSrv); err != nil { + return fmt.Errorf("%w: %w", errHTTPServer, err) + } + + return nil +} + +func (h *httpServer) Configure() error { + h.logger.Debug("Configuring service...") + + if err := h.configureHTTPServer(); err != nil { + return fmt.Errorf("configure: %w", err) + } + + return nil +} + +func (h *httpServer) ConnectDependencies() error { + databaseRaw := h.app.Service(core.ServiceNameDatabase) + if databaseRaw == nil { + return fmt.Errorf("connect dependencies: get database service: %w", application.ErrServiceNotFound) + } + + database, valid := databaseRaw.(core.Database) + if !valid { + return fmt.Errorf("connect dependencies: type assert database service: %w", core.ErrDatabaseIsInvalid) + } + + h.db = database + + return nil +} + +func (h *httpServer) Initialize() error { + h.logger = h.app.NewLogger("service", core.ServiceNameHTTPServer) + + h.logger.Info("Initializing...") + + return nil +} + +func (h *httpServer) Name() string { + return core.ServiceNameHTTPServer +} + +func (h *httpServer) LaunchStartupTasks() error { + h.logger.Debug("Launching startup tasks...") + + go h.startHTTPServer() + + return nil +} + +func (h *httpServer) Shutdown() error { + if err := h.stopHTTPServer(); err != nil { + return fmt.Errorf("%w: Shutdown: %w", errHTTPServer, err) + } + + return nil +} diff --git a/server/internal/services/core/httpserver/server.go b/server/internal/services/core/httpserver/server.go new file mode 100644 index 0000000..befffe8 --- /dev/null +++ b/server/internal/services/core/httpserver/server.go @@ -0,0 +1,85 @@ +package httpserver + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "os" + "time" + + "github.com/coder/websocket" +) + +const httpServerAddrEnvVar = "BUNKERD_HTTP_ADDRESS" + +var ( + errHTTPServerAddrInvalid = errors.New("BUNKERD_HTTP_ADDRESS environment variable contains invalid address to " + + "listen, should be 'host:port'") + errHTTPServerAddrNotFound = errors.New("BUNKERD_HTTP_ADDRESS environment variable empty") +) + +func (h *httpServer) configureHTTPServer() error { + httpSrvAddr, found := os.LookupEnv(httpServerAddrEnvVar) + if !found { + return fmt.Errorf("configure HTTP server: get address from environment variable: %w", errHTTPServerAddrNotFound) + } + + host, port, err := net.SplitHostPort(httpSrvAddr) + if err != nil { + return fmt.Errorf("configure HTTP server: validate HTTP server address: %w", err) + } + + if httpSrvAddr != host+":"+port { + return fmt.Errorf("configure HTTP server: validate HTTP server address: %w", errHTTPServerAddrInvalid) + } + + mux := new(http.ServeMux) + mux.HandleFunc("GET /api/v1/socket", h.handleWebsocketRequest) + + h.httpSrv = &http.Server{ + Addr: httpSrvAddr, + Handler: mux, + ReadHeaderTimeout: time.Second * 3, + } + + return nil +} + +func (h *httpServer) handleWebsocketRequest(w http.ResponseWriter, r *http.Request) { + wsConn, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + OnPingReceived: func(_ context.Context, _ []byte) bool { + return true + }, + }) + if err != nil { + h.logger.Error("Failed to accept WS connection!", "error", err.Error()) + + return + } + + defer func() { + if err := wsConn.CloseNow(); err != nil { + h.logger.Warn("Failed to close WS connection in defer!", "error", err.Error()) + } + }() +} + +func (h *httpServer) startHTTPServer() { + h.logger.Info("Starting listening for HTTP requests.", "address", h.httpSrv.Addr) + + if err := h.httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + h.logger.Warn("Error when listening to ", "error", err.Error()) + } +} + +func (h *httpServer) stopHTTPServer() error { + h.logger.Info("Stopping HTTP server...") + + if err := h.httpSrv.Shutdown(h.app.ContextWithTimeout(time.Second * 3)); err != nil { + return fmt.Errorf("stopping HTTP server: %w", err) + } + + return nil +} diff --git a/server/internal/services/core/options.go b/server/internal/services/core/options.go new file mode 100644 index 0000000..80198ff --- /dev/null +++ b/server/internal/services/core/options.go @@ -0,0 +1,14 @@ +package core + +import ( + "errors" +) + +// ServiceNameOptions is a name for options service which controls options storage. +const ServiceNameOptions = "core/options" + +// ErrOptionsIsInvalid appears when options service implementation is invalid. +var ErrOptionsIsInvalid = errors.New("options service implementation is invalid") + +// Options is an interface for options service. +type Options interface{} diff --git a/server/internal/services/core/options/migrations.go b/server/internal/services/core/options/migrations.go new file mode 100644 index 0000000..b96a460 --- /dev/null +++ b/server/internal/services/core/options/migrations.go @@ -0,0 +1,17 @@ +package options + +import ( + "embed" + "fmt" +) + +//go:embed migrations +var migrations embed.FS + +func (o *options) registerMigrations() error { + if err := o.db.RegisterMigrations("core/options", migrations); err != nil { + return fmt.Errorf("register migrations: %w", err) + } + + return nil +} diff --git a/server/internal/services/core/options/migrations/0001_initial.sql b/server/internal/services/core/options/migrations/0001_initial.sql new file mode 100644 index 0000000..3d115e6 --- /dev/null +++ b/server/internal/services/core/options/migrations/0001_initial.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE IF NOT EXISTS options ( + id UUID NOT NULL PRIMARY KEY, + user_id UUID NOT NULL, + key VARCHAR(1024) NOT NULL, + value VARCHAR(8192) +); + +CREATE UNIQUE INDEX IF NOT EXISTS options_user_id_key_idx ON options(user_id, key); + +-- +goose Down +DROP TABLE IF EXISTS options; diff --git a/server/internal/services/core/options/options.go b/server/internal/services/core/options/options.go new file mode 100644 index 0000000..5346535 --- /dev/null +++ b/server/internal/services/core/options/options.go @@ -0,0 +1,79 @@ +package options + +import ( + "errors" + "fmt" + "log/slog" + + "bunker/server/internal/application" + "bunker/server/internal/services/core" +) + +var ( + _ = core.Options(&options{}) + + errOptions = errors.New("options core service") +) + +type options struct { + app *application.Application + logger *slog.Logger + db core.Database +} + +// Initialize initializes service. +func Initialize(app *application.Application) error { + opts := &options{ + app: app, + } + + if err := app.RegisterService(opts); err != nil { + return fmt.Errorf("%w: %w", errOptions, err) + } + + return nil +} + +func (o *options) Configure() error { + if err := o.registerMigrations(); err != nil { + return fmt.Errorf("configure: %w", err) + } + + return nil +} + +func (o *options) ConnectDependencies() error { + databaseRaw := o.app.Service(core.ServiceNameDatabase) + if databaseRaw == nil { + return fmt.Errorf("connect dependencies: get database service: %w", application.ErrServiceNotFound) + } + + database, valid := databaseRaw.(core.Database) + if !valid { + return fmt.Errorf("connect dependencies: type assert database service: %w", core.ErrDatabaseIsInvalid) + } + + o.db = database + + return nil +} + +func (o *options) Initialize() error { + o.logger = o.app.NewLogger("service", core.ServiceNameOptions) + + o.logger.Info("Initializing...") + + return nil +} + +func (o *options) Name() string { + return core.ServiceNameOptions +} + +func (o *options) LaunchStartupTasks() error { + return nil +} + +func (o *options) Shutdown() error { + return nil +} diff --git a/server/localdevzone/Taskfile.yml b/server/localdevzone/Taskfile.yml index 7c17fe9..964d31f 100644 --- a/server/localdevzone/Taskfile.yml +++ b/server/localdevzone/Taskfile.yml @@ -4,16 +4,20 @@ version: "3" includes: bunkerd: ./bunkerd common: ./common + postgresql: ./postgresql tasks: down: desc: "Removes development environment." cmds: - task: bunkerd:down + - task: bunkerd:down-debug + - task: postgresql:down - task: common:network-down up: desc: "Creates development environment." cmds: - task: common:network-up + - task: postgresql:up - task: bunkerd:up diff --git a/server/localdevzone/bunkerd/docker-compose.debug.yaml b/server/localdevzone/bunkerd/docker-compose.debug.yaml index b410276..5aeef55 100644 --- a/server/localdevzone/bunkerd/docker-compose.debug.yaml +++ b/server/localdevzone/bunkerd/docker-compose.debug.yaml @@ -14,7 +14,11 @@ services: bunkerd: ipv4_address: 247.247.0.3 environment: - BUNKERD_DATABASE_DSN: " " + BUNKERD_DATABASE_MAX_IDLE_CONNS: 5 + BUNKERD_DATABASE_MAX_OPENED_CONNS: 20 + BUNKERD_DATABASE_DSN: "postgres://postgres:postgres@247.247.0.4:5432/bunkerd?sslmode=disable" + BUNKERD_HTTP_ADDRESS: "0.0.0.0:53400" + BUNKERD_LOG_LEVEL: "debug" cap_add: - SYS_PTRACE diff --git a/server/localdevzone/bunkerd/docker-compose.yaml b/server/localdevzone/bunkerd/docker-compose.yaml index 5610a11..b7379e4 100644 --- a/server/localdevzone/bunkerd/docker-compose.yaml +++ b/server/localdevzone/bunkerd/docker-compose.yaml @@ -13,7 +13,11 @@ services: bunkerd: ipv4_address: 247.247.0.2 environment: - BUNKERD_DATABASE_DSN: " " + BUNKERD_DATABASE_MAX_IDLE_CONNS: 5 + BUNKERD_DATABASE_MAX_OPENED_CONNS: 20 + BUNKERD_DATABASE_DSN: "postgres://postgres:postgres@247.247.0.4:5432/bunkerd?sslmode=disable" + BUNKERD_HTTP_ADDRESS: "0.0.0.0:53400" + BUNKERD_LOG_LEVEL: "debug" cap_add: - SYS_PTRACE diff --git a/server/localdevzone/postgresql/Taskfile.yml b/server/localdevzone/postgresql/Taskfile.yml new file mode 100644 index 0000000..516c3de --- /dev/null +++ b/server/localdevzone/postgresql/Taskfile.yml @@ -0,0 +1,40 @@ +--- +version: "3" + +tasks: + down: + desc: "Deletes PostgreSQL and it's data from Docker (down)." + dir: "./server/localdevzone/postgresql" + cmds: + - docker compose -p bunkerd-postgres -f docker-compose.yaml down --volumes + + logs: + desc: "Show PostgreSQL logs." + dir: "./server/localdevzone/postgresql" + cmds: + - docker compose -p bunkerd-postgres -f docker-compose.yaml logs -f + + restart: + desc: "Restarts PostgreSQL." + dir: "./server/localdevzone/postgresql" + cmds: + - docker compose -p bunkerd-postgres -f docker-compose.yaml restart + + start: + desc: "Starts PostgreSQL in Docker." + dir: "./server/localdevzone/postgresql" + cmds: + - docker compose -p bunkerd-postgres -f docker-compose.yaml start + + stop: + desc: "Stops PostgreSQL in Docker." + dir: "./server/localdevzone/postgresql" + cmds: + - docker compose -p bunkerd-postgres -f docker-compose.yaml stop + + up: + desc: "Starts PostgreSQL in Docker (up -d)." + dir: "./server/localdevzone/postgresql" + cmds: + - task: :common:network-up + - docker compose -p bunkerd-postgres -f docker-compose.yaml up -d diff --git a/server/localdevzone/postgresql/docker-compose.yaml b/server/localdevzone/postgresql/docker-compose.yaml new file mode 100644 index 0000000..bcf1f45 --- /dev/null +++ b/server/localdevzone/postgresql/docker-compose.yaml @@ -0,0 +1,25 @@ +--- +services: + postgresql: + container_name: bunkerd-postgresql + image: postgres:17.6-alpine + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=bunkerd + - PGDATA=/var/lib/postgresql/data + ports: + - "127.0.0.1:53500:5432" + volumes: + - "bunkerd_localdevzone_postgresql:/var/lib/postgresql/data" + networks: + bunkerd: + ipv4_address: 247.247.0.4 + +volumes: + bunkerd_localdevzone_postgresql: + external: false + +networks: + bunkerd: + external: true