Skip to content

Commit

Permalink
More bootstrapping work
Browse files Browse the repository at this point in the history
Add "init" command for initializing a new database with running
migrations as well as creating a default "admin" role and user with a
randomly generated password. This also needed changes to
insertUserWithArgon2Tx() to allow a NULL organization ID. This has not
been needed when running tests because the database has been seeded by
hand via migration files.

Move out config management to a separate module to make it easier to not
introduce cyclical imports when multiple packages uses the config.

Add a basic test for running "up" migrations.

Some other small tweaks like making it possible to log goose migrations
via zerolog.
  • Loading branch information
eest committed Dec 19, 2024
1 parent ad1b304 commit 365a7e8
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 55 deletions.
51 changes: 51 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cmd

import (
"fmt"

"github.com/SUNET/sunet-cdn-manager/pkg/config"
"github.com/SUNET/sunet-cdn-manager/pkg/server"
"github.com/spf13/cobra"
)

// initCmd represents the init command
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize the database",
Long: `Initialize the database for use by the system, making sure the database structure
is present as well as creating an initial admin user and role for managing the contents.`,
RunE: func(_ *cobra.Command, _ []string) error {
conf, err := config.GetConfig()
if err != nil {
return err
}

pgConfig, err := conf.PGConfig()
if err != nil {
return err
}

u, err := server.Init(cdnLogger, pgConfig)
if err != nil {
return err
}

fmt.Printf("database is initialized\nuser: %s\npassword: %s\n", u.Name, u.Password)

return nil
},
}

func init() {
rootCmd.AddCommand(initCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// initCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// initCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
13 changes: 12 additions & 1 deletion cmd/up.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"github.com/SUNET/sunet-cdn-manager/pkg/config"
"github.com/SUNET/sunet-cdn-manager/pkg/migrations"
"github.com/spf13/cobra"
)
Expand All @@ -11,7 +12,17 @@ var upCmd = &cobra.Command{
Short: "Migrate to latest version",
Long: `Migrate SQL DB to latest version`,
RunE: func(_ *cobra.Command, _ []string) error {
err := migrations.Up()
conf, err := config.GetConfig()
if err != nil {
return err
}

pgConfig, err := conf.PGConfig()
if err != nil {
return err
}

err = migrations.Up(cdnLogger, pgConfig)
if err != nil {
return err
}
Expand Down
55 changes: 55 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package config

import (
"fmt"

"github.com/jackc/pgx/v5/pgxpool"
"github.com/spf13/viper"
)

type Config struct {
Server serverSettings
DB dbSettings
}

type serverSettings struct {
Addr string
}

type dbSettings struct {
User string
Password string
DBName string
Host string
Port int
SSLMode string
}

func GetConfig() (Config, error) {
var conf Config
err := viper.Unmarshal(&conf)
if err != nil {
return Config{}, fmt.Errorf("viper unable to decode into struct: %w", err)
}

return conf, nil
}

func (conf Config) PGConfig() (*pgxpool.Config, error) {
pgConfigString := fmt.Sprintf(
"user=%s password=%s host=%s port=%d dbname=%s sslmode=%s",
conf.DB.User,
conf.DB.Password,
conf.DB.Host,
conf.DB.Port,
conf.DB.DBName,
conf.DB.SSLMode,
)

pgConfig, err := pgxpool.ParseConfig(pgConfigString)
if err != nil {
return nil, fmt.Errorf("unable to parse PostgreSQL config string: %w", err)
}

return pgConfig, nil
}
31 changes: 19 additions & 12 deletions pkg/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,35 @@ import (
"embed"
"fmt"

"github.com/SUNET/sunet-cdn-manager/pkg/server"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
"github.com/rs/zerolog"
)

// Make zerolog usable as a goose logger, currently the "message" will include
// a trailing '\n' but rather than doing extra work here hopefully those can be
// cleaned up upstream: https://github.com/pressly/goose/pull/878
type gooseLogger struct {
logger zerolog.Logger
}

func (gl gooseLogger) Fatalf(format string, v ...interface{}) {
gl.logger.Fatal().Msgf(format, v...)
}

func (gl gooseLogger) Printf(format string, v ...interface{}) {
gl.logger.Info().Msgf(format, v...)
}

//go:embed files/*.sql
var embedMigrations embed.FS

func Up() error {
func Up(logger zerolog.Logger, pgConfig *pgxpool.Config) error {
gl := gooseLogger{logger: logger}
goose.SetLogger(gl)
goose.SetBaseFS(embedMigrations)

config, err := server.GetConfig()
if err != nil {
return fmt.Errorf("unable to get server config: %w", err)
}

pgConfig, err := config.PGConfig()
if err != nil {
return fmt.Errorf("unable to create pg config: %w", err)
}

dbPool, err := pgxpool.NewWithConfig(context.Background(), pgConfig)
if err != nil {
return fmt.Errorf("unable to create database pool: %w", err)
Expand Down
77 changes: 77 additions & 0 deletions pkg/migrations/migrations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package migrations

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"testing"

"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog"
"github.com/stapelberg/postgrestest"
)

var (
pgt *postgrestest.Server
logger zerolog.Logger
)

func TestMain(m *testing.M) {
var err error
pgt, err = postgrestest.Start(context.Background(), postgrestest.WithSQLDriver("pgx"))
if err != nil {
panic(err)
}
defer pgt.Cleanup()

zerolog.CallerMarshalFunc = func(_ uintptr, file string, line int) string {
return filepath.Base(file) + ":" + strconv.Itoa(line)
}
logger = zerolog.New(os.Stderr).With().Timestamp().Caller().Logger()

m.Run()
}

func prepareDatabase() (*pgxpool.Config, error) {
pgurl, err := pgt.CreateDatabase(context.Background())
if err != nil {
return nil, err
}

pgConfig, err := pgxpool.ParseConfig(pgurl)
if err != nil {
return nil, errors.New("unable to parse PostgreSQL config string")
}

fmt.Println(pgConfig.ConnString())

return pgConfig, nil
}

func TestUpMigrations(t *testing.T) {
pgConfig, err := prepareDatabase()
if err != nil {
t.Fatal(err)
}

tests := []struct {
description string
}{
{
description: "run migration",
},
}

for _, test := range tests {
err := Up(logger, pgConfig)
if err != nil {
t.Fatal(err)
}
if err != nil {
t.Fatalf("%s: up call failed: %s", test.description, err)
}
}
}
Loading

0 comments on commit 365a7e8

Please sign in to comment.