16 Commits

Author SHA1 Message Date
Renovate-Clanker c8b239881f chore(deps): update docker/setup-buildx-action action to v4 2026-06-13 08:08:19 +00:00
Renovate-Clanker a5f7d8dcb3 Merge pull request 'fix(deps): update module github.com/lestrrat-go/httprc/v3 to v3.0.6' (#40) from renovate/github.com-lestrrat-go-httprc-v3-3.x into main
SonarQube Scan / SonarQube Trigger (push) Failing after 20m15s
2026-06-08 02:21:33 +07:00
Renovate-Clanker 0fe675adb2 fix(deps): update module github.com/lestrrat-go/httprc/v3 to v3.0.6 2026-06-07 19:21:26 +00:00
Renovate-Clanker bbf8255530 Merge pull request 'fix(deps): update module github.com/jackc/pgx/v5 to v5.10.0' (#39) from renovate/github.com-jackc-pgx-v5-5.x into main
SonarQube Scan / SonarQube Trigger (push) Failing after 1m30s
2026-06-04 22:23:09 +07:00
Renovate-Clanker 129f38df32 fix(deps): update module github.com/jackc/pgx/v5 to v5.10.0 2026-06-04 15:23:03 +00:00
Renovate-Clanker 98b2b6a564 Merge pull request 'chore(deps): update golang docker tag to v1.26.4' (#38) from renovate/golang-1.x into main
SonarQube Scan / SonarQube Trigger (push) Failing after 7m43s
2026-06-04 22:23:00 +07:00
Renovate-Clanker 87001d1ac3 chore(deps): update golang docker tag to v1.26.4 2026-06-04 15:22:58 +00:00
Renovate-Clanker deb1c20d7d Merge pull request 'fix(deps): update module google.golang.org/grpc to v1.81.1' (#37) from renovate/google.golang.org-grpc-1.x into main
SonarQube Scan / SonarQube Trigger (push) Successful in 3m33s
2026-05-15 01:06:25 +07:00
Renovate-Clanker cbda775e3d fix(deps): update module google.golang.org/grpc to v1.81.1 2026-05-14 18:06:17 +00:00
Renovate-Clanker 81232857d7 Merge pull request 'chore(deps): update golang docker tag to v1.26.3' (#36) from renovate/golang-1.x into main
SonarQube Scan / SonarQube Trigger (push) Successful in 3m15s
2026-05-08 01:05:34 +07:00
Renovate-Clanker c80b9bacdc chore(deps): update golang docker tag to v1.26.3 2026-05-07 18:05:30 +00:00
Renovate-Clanker 47a806baf3 Merge pull request 'fix(deps): update module github.com/lestrrat-go/jwx/v3 to v3.1.1' (#35) from renovate/github.com-lestrrat-go-jwx-v3-3.x into main
SonarQube Scan / SonarQube Trigger (push) Successful in 3m31s
2026-05-07 08:05:29 +07:00
Renovate-Clanker dd11559bd1 fix(deps): update module github.com/lestrrat-go/jwx/v3 to v3.1.1 2026-05-07 01:05:21 +00:00
Renovate-Clanker 1ea108d03e Merge pull request 'fix(deps): update module google.golang.org/grpc to v1.81.0' (#34) from renovate/google.golang.org-grpc-1.x into main
SonarQube Scan / SonarQube Trigger (push) Successful in 3m17s
2026-05-04 13:02:21 +07:00
Renovate-Clanker 0a98e7e765 fix(deps): update module google.golang.org/grpc to v1.81.0 2026-05-04 06:02:13 +00:00
bagas 09a350565e Refactor: Centralize App Startup and Config Loading (#33)
SonarQube Scan / SonarQube Trigger (push) Successful in 3m49s
Reviewed-on: #33
2026-05-01 13:07:22 +07:00
14 changed files with 215 additions and 102 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.26.2-alpine AS go_builder
FROM golang:1.26.4-alpine AS go_builder
ARG VERSION=dev
ARG BUILD_DATE=unknown
+5 -5
View File
@@ -4,11 +4,11 @@ go 1.26.0
require (
git.fossy.my.id/bagas/tunnel-please-grpc v1.5.0
github.com/jackc/pgx/v5 v5.9.2
github.com/jackc/pgx/v5 v5.10.0
github.com/joho/godotenv v1.5.1
github.com/lestrrat-go/httprc/v3 v3.0.5
github.com/lestrrat-go/jwx/v3 v3.1.0
google.golang.org/grpc v1.80.0
github.com/lestrrat-go/httprc/v3 v3.0.6
github.com/lestrrat-go/jwx/v3 v3.1.1
google.golang.org/grpc v1.81.1
)
require (
@@ -27,6 +27,6 @@ require (
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
+12
View File
@@ -37,6 +37,8 @@ github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -57,12 +59,16 @@ github.com/lestrrat-go/httprc/v3 v3.0.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRT
github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM=
github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/httprc/v3 v3.0.6 h1:4FpLQ18KK/ypPbVU3NLWJNRvH3kcYiqKqWfKGqNWxxI=
github.com/lestrrat-go/httprc/v3 v3.0.6/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
github.com/lestrrat-go/jwx/v3 v3.1.0 h1:AyyLtxc0QM75F75JroWgt1phwC7X+wOb3XKhH7XBZWw=
github.com/lestrrat-go/jwx/v3 v3.1.0/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU=
github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzHJzw=
github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -123,6 +129,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA=
@@ -135,6 +143,10 @@ google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+97
View File
@@ -0,0 +1,97 @@
package bootstrap
import (
"context"
"errors"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"git.fossy.my.id/bagas/tunnel-please-controller/internal/config"
"git.fossy.my.id/bagas/tunnel-please-controller/internal/db/sqlc/repository"
"git.fossy.my.id/bagas/tunnel-please-controller/internal/server"
"github.com/jackc/pgx/v5"
"github.com/lestrrat-go/httprc/v3"
"github.com/lestrrat-go/jwx/v3/jwk"
)
type bootstrap struct {
config config.Config
errChan chan error
signalChan chan os.Signal
}
type Bootstrap interface {
Run() error
}
func New(config config.Config) Bootstrap {
errChan := make(chan error, 5)
signalChan := make(chan os.Signal, 1)
return &bootstrap{
config: config,
errChan: errChan,
signalChan: signalChan,
}
}
func (b *bootstrap) Run() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
signal.Notify(b.signalChan, os.Interrupt, syscall.SIGTERM)
dbURL := b.config.DatabaseURL()
repo, err := startDatabase(ctx, dbURL)
if err != nil {
return err
}
client := httprc.NewClient()
jwkCache, err := jwk.NewCache(ctx, client)
if err != nil {
return err
}
s := server.New(repo, jwkCache, b.config)
b.errChan = make(chan error, 2)
go func() {
if err = s.StartAPI(ctx, b.config.ApiAddr()); err != nil && !errors.Is(err, context.Canceled) {
b.errChan <- err
}
}()
go func() {
if err = s.StartController(ctx, b.config.ControllerAddr()); err != nil && !errors.Is(err, context.Canceled) {
b.errChan <- err
}
}()
select {
case err = <-b.errChan:
return fmt.Errorf("service error: %w", err)
case sig := <-b.signalChan:
log.Printf("Received signal %s, initiating graceful shutdown", sig)
cancel()
return nil
}
}
func startDatabase(ctx context.Context, dbURL string) (*repository.Queries, error) {
connect, err := pgx.Connect(ctx, dbURL)
if err != nil {
return nil, err
}
defer func(connect *pgx.Conn, ctx context.Context) {
err = connect.Close(ctx)
if err != nil {
panic(err)
}
}(connect, ctx)
return repository.New(connect), nil
}
+83 -2
View File
@@ -1,8 +1,81 @@
package config
import "os"
import (
"crypto/rand"
"encoding/base64"
"errors"
"log"
"os"
func Getenv(key, defaultValue string) string {
"github.com/joho/godotenv"
)
type config struct {
databaseUrl string
controllerAddr string
apiAddr string
authToken string
jwksUrl string
}
type Config interface {
DatabaseURL() string
ControllerAddr() string
ApiAddr() string
AuthToken() string
JwksURL() string
}
func (c *config) DatabaseURL() string { return c.databaseUrl }
func (c *config) ControllerAddr() string { return c.controllerAddr }
func (c *config) ApiAddr() string { return c.apiAddr }
func (c *config) AuthToken() string { return c.authToken }
func (c *config) JwksURL() string { return c.jwksUrl }
func MustLoad() (Config, error) {
if err := loadEnvFile(); err != nil {
return nil, err
}
cfg, err := parse()
if err != nil {
return nil, err
}
return cfg, nil
}
func loadEnvFile() error {
if _, err := os.Stat(".env"); err == nil {
return godotenv.Load(".env")
}
return nil
}
func parse() (*config, error) {
databaseUrl := getenv("DATABASE_URL", "")
if databaseUrl == "" {
return nil, errors.New("DATABASE_URL environment variable not set")
}
controllerAddr := getenv("CONTROLLER_ADDR", ":8080")
apiAddr := getenv("API_ADDR", ":8081")
authToken := getenv("AUTH_TOKEN", "")
if authToken == "" {
authToken = generateAuthToken()
log.Printf("No AUTH_TOKEN provided. Generated token: %s", authToken)
}
jwksUrl := getenv("JWKS_URL", "")
return &config{
databaseUrl: databaseUrl,
controllerAddr: controllerAddr,
apiAddr: apiAddr,
authToken: authToken,
jwksUrl: jwksUrl,
}, nil
}
func getenv(key, defaultValue string) string {
val := os.Getenv(key)
if val == "" {
val = defaultValue
@@ -10,3 +83,11 @@ func Getenv(key, defaultValue string) string {
return val
}
func generateAuthToken() string {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
panic(err)
}
return base64.StdEncoding.EncodeToString(buf)
}
+10 -8
View File
@@ -12,8 +12,8 @@ import (
"sync"
"time"
"git.fossy.my.id/bagas/tunnel-please-controller/db/sqlc/repository"
"git.fossy.my.id/bagas/tunnel-please-controller/internal/config"
"git.fossy.my.id/bagas/tunnel-please-controller/internal/db/sqlc/repository"
proto "git.fossy.my.id/bagas/tunnel-please-grpc/gen"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
@@ -42,19 +42,19 @@ type Server struct {
Database *repository.Queries
Subscribers map[string]*Subscriber
mu *sync.RWMutex
authToken string
jwkCache *jwk.Cache
config config.Config
proto.UnimplementedEventServiceServer
proto.UnimplementedUserServiceServer
}
func New(database *repository.Queries, authToken string, jwkCache *jwk.Cache) *Server {
func New(database *repository.Queries, jwkCache *jwk.Cache, config config.Config) *Server {
return &Server{
Database: database,
Subscribers: make(map[string]*Subscriber),
mu: new(sync.RWMutex),
authToken: authToken,
jwkCache: jwkCache,
config: config,
}
}
@@ -94,7 +94,7 @@ func (s *Server) Subscribe(event grpc.BidiStreamingServer[proto.Node, proto.Even
return status.Error(codes.InvalidArgument, "missing identity")
}
token := payload.AuthEvent.AuthToken
if token != s.authToken {
if token != s.config.AuthToken() {
return status.Error(codes.Unauthenticated, "invalid auth token")
}
@@ -332,7 +332,7 @@ func (s *Server) StartAPI(ctx context.Context, Addr string) error {
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
jwkURL := config.Getenv("JWKS_URL", "")
jwkURL := s.config.JwksURL()
if jwkURL != "" {
registerCtx, cancel := context.WithTimeout(ctx, jwkRegisterTimeout)
defer cancel()
@@ -629,6 +629,7 @@ func (s *Server) StartAPI(ctx context.Context, Addr string) error {
errCh := make(chan error, 1)
go func() {
log.Printf("Listening api on %s", httpServer.Addr)
errCh <- httpServer.ListenAndServe()
}()
@@ -697,18 +698,19 @@ func (s *Server) StartController(ctx context.Context, Addr string) error {
serveErr := make(chan error, 1)
go func() {
log.Printf("Listening controller on %s", s.config.ControllerAddr())
serveErr <- grpcServer.Serve(listener)
}()
select {
case <-ctx.Done():
grpcServer.GracefulStop()
err := <-serveErr
err = <-serveErr
if err != nil {
return err
}
return ctx.Err()
case err := <-serveErr:
case err = <-serveErr:
return err
}
}
+7 -86
View File
@@ -1,104 +1,25 @@
package main
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"log"
"os"
"os/signal"
"syscall"
"git.fossy.my.id/bagas/tunnel-please-controller/db/sqlc/repository"
"git.fossy.my.id/bagas/tunnel-please-controller/server"
"github.com/jackc/pgx/v5"
"github.com/joho/godotenv"
"github.com/lestrrat-go/httprc/v3"
"github.com/lestrrat-go/jwx/v3/jwk"
"git.fossy.my.id/bagas/tunnel-please-controller/internal/bootstrap"
"git.fossy.my.id/bagas/tunnel-please-controller/internal/config"
)
func main() {
if _, err := os.Stat(".env"); err == nil {
if err = godotenv.Load(".env"); err != nil {
log.Printf("Warning: Failed to load .env file: %s", err)
}
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
log.SetOutput(os.Stdout)
log.SetFlags(log.LstdFlags | log.Lshortfile)
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
log.Fatal("DATABASE_URL is required")
}
controllerAddr := getenv("CONTROLLER_ADDR", ":8080")
apiAddr := getenv("API_ADDR", ":8081")
authToken := getenv("AUTH_TOKEN", "")
if authToken == "" {
authToken = generateAuthToken()
log.Printf("No AUTH_TOKEN provided. Generated token: %s", authToken)
}
connect, err := pgx.Connect(ctx, dbURL)
conf, err := config.MustLoad()
if err != nil {
panic(err)
log.Fatalf("Config load error: %v", err)
}
defer func(connect *pgx.Conn, ctx context.Context) {
err = connect.Close(ctx)
if err != nil {
panic(err)
}
}(connect, ctx)
repo := repository.New(connect)
client := httprc.NewClient()
jwkCache, err := jwk.NewCache(ctx, client)
if err != nil {
log.Printf("failed to initialize jwk cache : %s", err)
}
s := server.New(repo, authToken, jwkCache)
boot := bootstrap.New(conf)
log.Printf("Listening controller on %s", controllerAddr)
log.Printf("Listening api on %s", apiAddr)
errCh := make(chan error, 2)
go func() {
if err = s.StartAPI(ctx, apiAddr); err != nil && !errors.Is(err, context.Canceled) {
errCh <- err
}
}()
go func() {
if err = s.StartController(ctx, controllerAddr); err != nil && !errors.Is(err, context.Canceled) {
errCh <- err
}
}()
select {
case <-ctx.Done():
log.Printf("shutting down: %v", ctx.Err())
case err = <-errCh:
log.Fatalf("server error: %v", err)
if err = boot.Run(); err != nil {
log.Fatalf("Application error: %v", err)
}
}
func getenv(key, def string) string {
if val := os.Getenv(key); val != "" {
return val
}
return def
}
func generateAuthToken() string {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
panic(err)
}
return base64.StdEncoding.EncodeToString(buf)
}