diff --git a/TODO.md b/TODO.md index aa4567c..108865c 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,6 @@ - Why a factory? - First steps after provisioning -- Use `environment.Dial()` and `environment.Listen()` - Move `provision_entrypoint.sh` into go - Enhance Snapshots - Export the Docker Images diff --git a/cmd/server.go b/cmd/server.go index 23d3949..0ccac29 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -32,13 +32,22 @@ var errServerListen = exit.Error{ } func (s server) Run(context wisski_distillery.Context) error { - handler, err := context.Environment.Dis().Server(context.IOStream) + dis := context.Environment + handler, err := dis.Control().Server(context.IOStream) if err != nil { return err } context.Printf("Listening on %s\n", s.Bind) - err = http.ListenAndServe(s.Bind, http.StripPrefix(s.Prefix, handler)) + + // make a new listener + listener, err := dis.Core.Environment.Listen("tcp", s.Bind) + if err != nil { + return errServerListen.Wrap(err) + } + + // and serve that listener + err = http.Serve(listener, http.StripPrefix(s.Prefix, handler)) if err == nil { return nil } diff --git a/cmd/update_prefix_config.go b/cmd/update_prefix_config.go index 3f60d44..454d774 100644 --- a/cmd/update_prefix_config.go +++ b/cmd/update_prefix_config.go @@ -38,7 +38,7 @@ func (upc updateprefixconfig) Run(context wisski_distillery.Context) error { return errPrefixUpdateFailed.WithMessageF(err) } - ddis := dis.Dis() + ddis := dis.Control() target := ddis.ResolverConfigPath() // print the configuration diff --git a/go.mod b/go.mod index 262f097..c54af81 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/Showmax/go-fqdn v1.0.0 github.com/alessio/shellescape v1.4.1 github.com/feiin/sqlstring v0.3.0 + github.com/go-sql-driver/mysql v1.6.0 github.com/pkg/errors v0.9.1 github.com/tkw1536/goprogram v0.0.12 golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 @@ -16,7 +17,6 @@ require ( ) require ( - github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/jessevdk/go-flags v1.5.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect diff --git a/internal/component/control/control.go b/internal/component/control/control.go index b39f5cd..71fb295 100644 --- a/internal/component/control/control.go +++ b/internal/component/control/control.go @@ -9,7 +9,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/pkg/environment" ) -// Control represents the control server +// Control represents the running control server. type Control struct { component.ComponentBase diff --git a/internal/component/sql/database.go b/internal/component/sql/database.go index 986450e..46e56d0 100644 --- a/internal/component/sql/database.go +++ b/internal/component/sql/database.go @@ -1,9 +1,14 @@ package sql import ( + "context" "errors" "fmt" "io" + "net" + "sync/atomic" + + mysqldriver "github.com/go-sql-driver/mysql" "github.com/FAU-CDI/wisski-distillery/internal/bookkeeping" "github.com/FAU-CDI/wisski-distillery/pkg/logging" @@ -15,13 +20,29 @@ import ( "gorm.io/gorm/logger" ) +var proxyNameCounter uint64 + +// registerDialingProxy registers a new custom network protocol with the underlying sql driver. +// The new protocol will call dialer with the provided network argument. +// The name of the new protocol is returned. +func registerDialingProxy(network string, dialer func(context context.Context, network, address string) (net.Conn, error)) (name string) { + name = fmt.Sprintf("sql-proxy-%d", atomic.AddUint64(&proxyNameCounter, 1)) + mysqldriver.RegisterDialContext(name, func(ctx context.Context, addr string) (net.Conn, error) { + return dialer(ctx, network, addr) + }) + return +} + // sqlOpen opens a new sql connection to the provided database using the administrative credentials -func (sql SQL) openDatabase(database string, config *gorm.Config) (*gorm.DB, error) { +func (sql *SQL) openDatabase(database string, config *gorm.Config) (*gorm.DB, error) { + network := sql.sqlNetwork.Get(func() string { + return registerDialingProxy("tcp", sql.Core.Environment.DialContext) + }) cfg := mysql.Config{ - DSN: fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", sql.Config.MysqlAdminUser, sql.Config.MysqlAdminPassword, sql.ServerURL, database), + DriverName: "mysql", + DSN: fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8&parseTime=True&loc=Local", sql.Config.MysqlAdminUser, sql.Config.MysqlAdminPassword, network, sql.ServerURL, database), DefaultStringSize: 256, } - // TODO: Use sql.Core.Environment.Dial db, err := gorm.Open(mysql.New(cfg), config) if err != nil { @@ -38,7 +59,7 @@ func (sql SQL) openDatabase(database string, config *gorm.Config) (*gorm.DB, err } // OpenBookkeeping opens a connection to the bookkeeping database -func (sql SQL) OpenBookkeeping(silent bool) (*gorm.DB, error) { +func (sql *SQL) OpenBookkeeping(silent bool) (*gorm.DB, error) { config := &gorm.Config{} if silent { @@ -75,12 +96,12 @@ func (sql SQL) Snapshot(io stream.IOStream, dest io.Writer, database string) err } // OpenShell executes a mysql shell command -func (sql SQL) OpenShell(io stream.IOStream, argv ...string) (int, error) { +func (sql *SQL) OpenShell(io stream.IOStream, argv ...string) (int, error) { return sql.Stack(sql.Environment).Exec(io, "sql", "mysql", argv...) } // WaitShell waits for the sql database to be reachable via a docker-compose shell -func (sql SQL) WaitShell() error { +func (sql *SQL) WaitShell() error { n := stream.FromNil() return wait.Wait(func() bool { code, err := sql.OpenShell(n, "-e", "show databases;") @@ -89,7 +110,7 @@ func (sql SQL) WaitShell() error { } // Wait waits for a connection to the bookkeeping table to suceed -func (sql SQL) Wait() error { +func (sql *SQL) Wait() error { return wait.Wait(func() bool { _, err := sql.OpenBookkeeping(true) return err == nil @@ -98,14 +119,14 @@ func (sql SQL) Wait() error { var errInvalidDatabaseName = errors.New("SQLProvision: Invalid database name") -func (sql SQL) Query(query string, args ...interface{}) bool { +func (sql *SQL) Query(query string, args ...interface{}) bool { raw := sqle.Format(query, args...) code, err := sql.OpenShell(stream.FromNil(), "-e", raw) return err == nil && code == 0 } // SQLProvision provisions a new sql database and user -func (sql SQL) Provision(name, user, password string) error { +func (sql *SQL) Provision(name, user, password string) error { // wait for the database if err := sql.WaitShell(); err != nil { return err @@ -128,7 +149,7 @@ func (sql SQL) Provision(name, user, password string) error { var errSQLPurgeUser = errors.New("unable to delete user") // SQLPurgeUser deletes the specified user from the database -func (sql SQL) PurgeUser(user string) error { +func (sql *SQL) PurgeUser(user string) error { if !sql.Query("DROP USER IF EXISTS ?@`%`; FLUSH PRIVILEGES; ", user) { return errSQLPurgeUser } @@ -139,7 +160,7 @@ func (sql SQL) PurgeUser(user string) error { var errSQLPurgeDB = errors.New("unable to drop database") // SQLPurgeDatabase deletes the specified db from the database -func (sql SQL) PurgeDatabase(db string) error { +func (sql *SQL) PurgeDatabase(db string) error { if !sqle.IsSafeDatabaseName(db) { return errSQLPurgeDB } @@ -154,7 +175,7 @@ var errSQLUnsafeDatabaseName = errors.New("bookkeeping database has an unsafe na var errSQLUnableToCreate = errors.New("unable to create bookkeeping database") // Bootstrap bootstraps the SQL database, and makes sure that the bookkeeping table is up-to-date -func (sql SQL) Bootstrap(io stream.IOStream) error { +func (sql *SQL) Bootstrap(io stream.IOStream) error { if err := sql.WaitShell(); err != nil { return err } diff --git a/internal/component/sql/sql.go b/internal/component/sql/sql.go index 038f26f..6369cdc 100644 --- a/internal/component/sql/sql.go +++ b/internal/component/sql/sql.go @@ -7,6 +7,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/component" "github.com/FAU-CDI/wisski-distillery/pkg/environment" + "github.com/FAU-CDI/wisski-distillery/pkg/lazy" ) type SQL struct { @@ -16,6 +17,8 @@ type SQL struct { PollContext context.Context // context to abort polling with PollInterval time.Duration // duration to wait for during wait + + sqlNetwork lazy.Lazy[string] } func (SQL) Name() string { @@ -25,7 +28,7 @@ func (SQL) Name() string { //go:embed all:sql var resources embed.FS -func (ssh SQL) Stack(env environment.Environment) component.StackWithResources { +func (ssh *SQL) Stack(env environment.Environment) component.StackWithResources { return ssh.ComponentBase.MakeStack(env, component.StackWithResources{ Resources: resources, ContextPath: "sql", diff --git a/internal/component/triplestore/database.go b/internal/component/triplestore/database.go index 3e2f132..f765bad 100644 --- a/internal/component/triplestore/database.go +++ b/internal/component/triplestore/database.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "mime/multipart" - "net" "net/http" "github.com/FAU-CDI/wisski-distillery/pkg/logging" @@ -64,10 +63,8 @@ func (ts Triplestore) OpenRaw(method, url string, body interface{}, bodyName str // create the request object client := &http.Client{ Transport: &http.Transport{ - Dial: ts.Environment.Dial, - DialTLS: func(network, addr string) (net.Conn, error) { - return nil, errors.New("not implemented") - }, + DialContext: ts.Environment.DialContext, + DisableKeepAlives: true, }, } req, err := http.NewRequest(method, ts.BaseURL+url, reader) diff --git a/internal/wisski/component.go b/internal/wisski/component.go index e05eb81..af780f3 100644 --- a/internal/wisski/component.go +++ b/internal/wisski/component.go @@ -70,7 +70,7 @@ func makeComponent[C component.Component](dis *Distillery, field *lazy.Lazy[C], func (dis *Distillery) ComponentsX() []component.Component { return []component.Component{ dis.Web(), - dis.Dis(), + dis.Control(), dis.SSH(), dis.Triplestore(), dis.SQL(), @@ -107,7 +107,7 @@ func (dis *Distillery) Web() *web.Web { return makeComponent(dis, &dis.components.web, nil) } -func (d *Distillery) Dis() *control.Control { +func (d *Distillery) Control() *control.Control { return makeComponent(d, &d.components.control, func(ddis *control.Control) { ddis.ResolverFile = core.PrefixConfig ddis.Instances = d.Instances() diff --git a/pkg/environment/environment.go b/pkg/environment/environment.go index 1725de9..6ed8a25 100644 --- a/pkg/environment/environment.go +++ b/pkg/environment/environment.go @@ -1,6 +1,7 @@ package environment import ( + "context" "io" "io/fs" "net" @@ -41,7 +42,7 @@ type Environment interface { Abs(path string) (string, error) Listen(network, address string) (net.Listener, error) - Dial(network, address string) (net.Conn, error) + DialContext(context context.Context, network, address string) (net.Conn, error) Executable() (string, error) Exec(io stream.IOStream, workdir string, exe string, argv ...string) int diff --git a/pkg/environment/native_net.go b/pkg/environment/native_net.go index 2d47e1e..0e6dddf 100644 --- a/pkg/environment/native_net.go +++ b/pkg/environment/native_net.go @@ -1,11 +1,15 @@ package environment -import "net" +import ( + "context" + "net" +) func (Native) Listen(network, address string) (net.Listener, error) { return net.Listen(network, address) } -func (Native) Dial(network, address string) (net.Conn, error) { - return net.Dial(network, address) +func (Native) DialContext(context context.Context, network, address string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(context, network, address) }