mirror of
https://github.com/hanzoai/migrate.git
synced 2026-09-30 22:55:10 +00:00
* MSI Auth for SQL Server Add MSI Auth option to SQL Server connection. Default if no password is provided in connection string. * Parse resource endpoint from server for msi update host name parsing to get just the resource endpoint for msi * Update go-mssqldb update go-mssqldb to resolve panic issue referenced here: https://github.com/denisenkom/go-mssqldb/pull/642 * Update sqlserver.go switch from deprecated methods. NewServicePrincipalTokenFromManagedIdentity calls the two methods that are deprecated for us * Update sqlserver.go add useMsi param instead of looking for nil password * Update sqlserver readme Update sqlserver readme for msi auth. make useMsi a bit safer * Update sqlserver.go remove comment * Update database/sqlserver/README.md Co-authored-by: Keegan Campbell <[email protected]> * Update sqlserver.go refactor resource uri logic into its own method * Update sqlserver_test.go add tests for msi connection. can only test whether it fails with useMsi= true, or succeeds with useMsi=false * Update sqlserver.go check msi.EnsureFresh return value * Return error for multiple auth and move query filter move migrate.FilterCustomQuery(purl).String() into one line out of if/else. return error if both useMsi=true and password are passed * Update README.md update readme with warning for useMsi * Update sqlserver_test.go Update TestMsiFalse test case as now it should fail when useMsi=false and no password is provided Co-authored-by: Keegan Campbell <[email protected]>
292 lines
6.8 KiB
Go
292 lines
6.8 KiB
Go
package sqlserver
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
sqldriver "database/sql/driver"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/dhui/dktest"
|
|
"github.com/golang-migrate/migrate/v4"
|
|
|
|
dt "github.com/golang-migrate/migrate/v4/database/testing"
|
|
"github.com/golang-migrate/migrate/v4/dktesting"
|
|
|
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
)
|
|
|
|
const defaultPort = 1433
|
|
const saPassword = "Root1234"
|
|
|
|
var (
|
|
opts = dktest.Options{
|
|
Env: map[string]string{"ACCEPT_EULA": "Y", "SA_PASSWORD": saPassword, "MSSQL_PID": "Express"},
|
|
PortRequired: true, ReadyFunc: isReady, PullTimeout: 2 * time.Minute,
|
|
}
|
|
// Container versions: https://mcr.microsoft.com/v2/mssql/server/tags/list
|
|
specs = []dktesting.ContainerSpec{
|
|
{ImageName: "mcr.microsoft.com/mssql/server:2017-latest", Options: opts},
|
|
{ImageName: "mcr.microsoft.com/mssql/server:2019-latest", Options: opts},
|
|
}
|
|
)
|
|
|
|
func msConnectionString(host, port string) string {
|
|
return fmt.Sprintf("sqlserver://sa:%v@%v:%v?database=master", saPassword, host, port)
|
|
}
|
|
|
|
func msConnectionStringMsiWithPassword(host, port string, useMsi bool) string {
|
|
return fmt.Sprintf("sqlserver://sa:%v@%v:%v?database=master&useMsi=%t", saPassword, host, port, useMsi)
|
|
}
|
|
|
|
func msConnectionStringMsi(host, port string, useMsi bool) string {
|
|
return fmt.Sprintf("sqlserver://sa@%v:%v?database=master&useMsi=%t", host, port, useMsi)
|
|
}
|
|
|
|
func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
|
|
ip, port, err := c.Port(defaultPort)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
uri := msConnectionString(ip, port)
|
|
db, err := sql.Open("sqlserver", uri)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer func() {
|
|
if err := db.Close(); err != nil {
|
|
log.Println("close error:", err)
|
|
}
|
|
}()
|
|
if err = db.PingContext(ctx); err != nil {
|
|
switch err {
|
|
case sqldriver.ErrBadConn:
|
|
return false
|
|
default:
|
|
fmt.Println(err)
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func Test(t *testing.T) {
|
|
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
|
ip, port, err := c.Port(defaultPort)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
addr := msConnectionString(ip, port)
|
|
p := &SQLServer{}
|
|
d, err := p.Open(addr)
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
|
|
defer func() {
|
|
if err := d.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
|
|
dt.Test(t, d, []byte("SELECT 1"))
|
|
})
|
|
}
|
|
|
|
func TestMigrate(t *testing.T) {
|
|
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
|
ip, port, err := c.Port(defaultPort)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
addr := msConnectionString(ip, port)
|
|
p := &SQLServer{}
|
|
d, err := p.Open(addr)
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
|
|
defer func() {
|
|
if err := d.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
|
|
m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "master", d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dt.TestMigrate(t, m)
|
|
})
|
|
}
|
|
|
|
func TestMultiStatement(t *testing.T) {
|
|
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
|
ip, port, err := c.FirstPort()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
addr := msConnectionString(ip, port)
|
|
ms := &SQLServer{}
|
|
d, err := ms.Open(addr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := d.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLE bar (bar text);")); err != nil {
|
|
t.Fatalf("expected err to be nil, got %v", err)
|
|
}
|
|
|
|
// make sure second table exists
|
|
var exists int
|
|
if err := d.(*SQLServer).conn.QueryRowContext(context.Background(), "SELECT COUNT(1) FROM information_schema.tables WHERE table_name = 'bar' AND table_schema = (SELECT schema_name()) AND table_catalog = (SELECT db_name())").Scan(&exists); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if exists != 1 {
|
|
t.Fatalf("expected table bar to exist")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestErrorParsing(t *testing.T) {
|
|
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
|
ip, port, err := c.FirstPort()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
addr := msConnectionString(ip, port)
|
|
p := &SQLServer{}
|
|
d, err := p.Open(addr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := d.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
|
|
wantErr := `migration failed: Unknown object type 'TABLEE' used in a CREATE, DROP, or ALTER statement. in line 1:` +
|
|
` CREATE TABLE foo (foo text); CREATE TABLEE bar (bar text); (details: mssql: Unknown object type ` +
|
|
`'TABLEE' used in a CREATE, DROP, or ALTER statement.)`
|
|
if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLEE bar (bar text);")); err == nil {
|
|
t.Fatal("expected err but got nil")
|
|
} else if err.Error() != wantErr {
|
|
t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLockWorks(t *testing.T) {
|
|
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
|
ip, port, err := c.Port(defaultPort)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
addr := fmt.Sprintf("sqlserver://sa:%v@%v:%v?master", saPassword, ip, port)
|
|
p := &SQLServer{}
|
|
d, err := p.Open(addr)
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
dt.Test(t, d, []byte("SELECT 1"))
|
|
|
|
ms := d.(*SQLServer)
|
|
|
|
err = ms.Lock()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = ms.Unlock()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// make sure the 2nd lock works (RELEASE_LOCK is very finicky)
|
|
err = ms.Lock()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = ms.Unlock()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMsiTrue(t *testing.T) {
|
|
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
|
ip, port, err := c.Port(defaultPort)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
addr := msConnectionStringMsi(ip, port, true)
|
|
p := &SQLServer{}
|
|
_, err = p.Open(addr)
|
|
if err == nil {
|
|
t.Fatal("MSI should fail when not running in an Azure context.")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOpenWithPasswordAndMSI(t *testing.T) {
|
|
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
|
ip, port, err := c.Port(defaultPort)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
addr := msConnectionStringMsiWithPassword(ip, port, true)
|
|
p := &SQLServer{}
|
|
_, err = p.Open(addr)
|
|
if err == nil {
|
|
t.Fatal("Open should fail when both password and useMsi=true are passed.")
|
|
}
|
|
|
|
addr = msConnectionStringMsiWithPassword(ip, port, false)
|
|
p = &SQLServer{}
|
|
d, err := p.Open(addr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() {
|
|
if err := d.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
|
|
dt.Test(t, d, []byte("SELECT 1"))
|
|
})
|
|
}
|
|
|
|
func TestMsiFalse(t *testing.T) {
|
|
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
|
ip, port, err := c.Port(defaultPort)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
addr := msConnectionStringMsi(ip, port, false)
|
|
p := &SQLServer{}
|
|
_, err = p.Open(addr)
|
|
if err == nil {
|
|
t.Fatal("Open should fail since no password was passed and useMsi is false.")
|
|
}
|
|
})
|
|
}
|