diff --git a/cmd/cleanup/sql.go b/cmd/cleanup/sql.go index 8a7680d3bcb4..1c6f7bbdfd7b 100644 --- a/cmd/cleanup/sql.go +++ b/cmd/cleanup/sql.go @@ -27,7 +27,9 @@ It is recommended to run this command close to the SQL instance (e.g. same subne This decreases risk of failure and decreases time required. You can read in the database URL using the -e flag, for example: export DSN=... + export NETWORK_ID=... kratos cleanup sql -e +NETWORK_ID (config key network.id) must name an existing row in table networks. ### WARNING ### Before running this command on an existing database, create a back up! `, diff --git a/driver/config/config.go b/driver/config/config.go index d1e28d166b98..d0c2e1ce9200 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -55,6 +55,8 @@ const ( DefaultCipherAlgorithm = "noop" UnknownVersion = "unknown version" ViperKeyDSN = "dsn" + ViperKeyNetworkID = "network.id" + ViperKeyForbiddenNetworkID = "network.forbidden_id" ViperKeyCourierSMTPURL = "courier.smtp.connection_uri" ViperKeyCourierSMTPClientCertPath = "courier.smtp.client_cert_path" ViperKeyCourierSMTPClientKeyPath = "courier.smtp.client_key_path" @@ -658,6 +660,14 @@ func (p *Config) DSN(ctx context.Context) string { return "" } +func (p *Config) NetworkID(ctx context.Context) string { + return p.GetProvider(ctx).String(ViperKeyNetworkID) +} + +func (p *Config) ForbiddenNetworkID(ctx context.Context) string { + return p.GetProvider(ctx).String(ViperKeyForbiddenNetworkID) +} + func (p *Config) DisableAPIFlowEnforcement(ctx context.Context) bool { if p.IsInsecureDevMode(ctx) && os.Getenv("DEV_DISABLE_API_FLOW_ENFORCEMENT") == "true" { p.l.Warn("Because \"DEV_DISABLE_API_FLOW_ENFORCEMENT=true\" and the \"--dev\" flag are set, self-service API flows will no longer check if the interaction is actually a browser flow. This is very dangerous as it allows bypassing of anti-CSRF measures, leaving the deployment highly vulnerable. This option should only be used for automated testing and never come close to real user data anywhere.") diff --git a/driver/factory_test.go b/driver/factory_test.go index d5c646550a87..19f0731fc834 100644 --- a/driver/factory_test.go +++ b/driver/factory_test.go @@ -13,6 +13,8 @@ import ( "github.com/gofrs/uuid" "github.com/ory/x/configx" + "github.com/ory/x/dbal" + "github.com/ory/x/networkx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -23,21 +25,31 @@ import ( func TestDriverNew(t *testing.T) { ctx := context.Background() + dsn := dbal.NewSQLiteTestDatabase(t) + nid := uuid.Must(uuid.NewV4()) + + migrator, err := driver.New(ctx, os.Stderr, servicelocatorx.NewOptions(), []driver.RegistryOption{driver.SkipNetworkInit}, + []configx.OptionModifier{configx.WithValue(config.ViperKeyDSN, dsn), configx.SkipValidation()}) + require.NoError(t, err) + require.NoError(t, migrator.Persister().MigrateUp(ctx)) + require.NoError(t, migrator.Persister().GetConnection(ctx).Create(&networkx.Network{ID: nid})) + r, err := driver.New( context.Background(), os.Stderr, servicelocatorx.NewOptions(), nil, []configx.OptionModifier{ - configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN), + configx.WithValue(config.ViperKeyDSN, dsn), + configx.WithValue(config.ViperKeyNetworkID, nid.String()), configx.SkipValidation(), }) require.NoError(t, err) - assert.EqualValues(t, config.DefaultSQLiteMemoryDSN, r.Config().DSN(ctx)) + assert.EqualValues(t, dsn, r.Config().DSN(ctx)) require.NoError(t, r.Persister().Ping()) - assert.NotEqual(t, uuid.Nil.String(), r.Persister().NetworkID(context.Background()).String()) + assert.Equal(t, nid, r.Persister().NetworkID(context.Background())) n, err := r.Persister().DetermineNetwork(context.Background()) require.NoError(t, err) diff --git a/driver/registry_default.go b/driver/registry_default.go index 9f88c7375dd0..2fe3912dddcf 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -17,6 +17,7 @@ import ( "github.com/cenkalti/backoff" "github.com/dgraph-io/ristretto" "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" "github.com/gorilla/sessions" "github.com/hashicorp/go-retryablehttp" "github.com/luna-duclos/instrumentedsql" @@ -61,6 +62,7 @@ import ( "github.com/ory/x/jsonnetsecure" "github.com/ory/x/jwksx" "github.com/ory/x/logrusx" + "github.com/ory/x/networkx" "github.com/ory/x/otelx" otelsql "github.com/ory/x/otelx/sql" "github.com/ory/x/popx" @@ -643,6 +645,14 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize m.identitySchemaProvider = o.replaceIdentitySchemaProvider(m) } + var nid uuid.UUID + if !o.skipNetworkInit { + var err error + if nid, err = m.configuredNetworkID(ctx); err != nil { + return err + } + } + bc := backoff.NewExponentialBackOff() bc.MaxElapsedTime = time.Minute * 5 bc.Reset() @@ -684,7 +694,8 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize } // if dsn is memory we have to run the migrations on every start - if dbal.IsMemorySQLite(m.Config().DSN(ctx)) || m.Config().DSN(ctx) == "memory" { + isMemory := dbal.IsMemorySQLite(m.Config().DSN(ctx)) || m.Config().DSN(ctx) == "memory" + if isMemory { m.Logger().Infoln("Ory Kratos is running migrations on every startup as DSN is memory. This means your data is lost when Kratos terminates.") if err := p.MigrateUp(ctx); err != nil { m.Logger().WithError(err).Warnf("Unable to run migrations, retrying.") @@ -697,12 +708,22 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize return nil } - net, err := p.DetermineNetwork(ctx) - if err != nil { - m.Logger().WithError(err).Warnf("Unable to determine network, retrying.") + net, err := p.NetworkByID(ctx, nid) + if errors.Is(err, sqlcon.ErrNoRows) && isMemory { + // Only this process can reach a memory database, so nothing else can seed its network. + m.Logger().WithField("network_id", nid.String()).Warn("Inserting the network from config, because the DSN is a memory database. This happens only for a memory DSN.") + net = &networkx.Network{ID: nid} + err = p.GetConnection(ctx).Create(net) + } + if errors.Is(err, sqlcon.ErrNoRows) { + // backoff v2 detects a permanent error only by type assertion, so it must not be wrapped. + return backoff.Permanent(errors.Errorf("config value %q names network %s, which is not in table networks", config.ViperKeyNetworkID, nid)) + } else if err != nil { + m.Logger().WithError(err).Warnf("Unable to load network, retrying.") return err } + m.Logger().WithField("network_id", net.ID.String()).Info("Using the network from config.") m.persister = p.WithNetworkID(net.ID) return nil }, bc) @@ -718,6 +739,40 @@ func (m *RegistryDefault) Init(ctx context.Context, ctxer contextx.Contextualize return nil } +func (m *RegistryDefault) configuredNetworkID(ctx context.Context) (uuid.UUID, error) { + nid, err := parseNetworkID(config.ViperKeyNetworkID, m.Config().NetworkID(ctx)) + if err != nil { + return uuid.Nil, err + } + + raw := m.Config().ForbiddenNetworkID(ctx) + if raw == "" { + return nid, nil + } + forbidden, err := parseNetworkID(config.ViperKeyForbiddenNetworkID, raw) + if err != nil { + return uuid.Nil, err + } + if nid == forbidden { + return uuid.Nil, errors.Errorf("config value %q equals config value %q: this release must not use network %s", config.ViperKeyNetworkID, config.ViperKeyForbiddenNetworkID, nid) + } + return nid, nil +} + +func parseNetworkID(key, raw string) (uuid.UUID, error) { + if raw == "" { + return uuid.Nil, errors.Errorf("required config value %q was not set", key) + } + id, err := uuid.FromString(raw) + if err != nil { + return uuid.Nil, errors.Errorf("config value %q is not a UUID: %q", key, raw) + } + if id == uuid.Nil { + return uuid.Nil, errors.Errorf("config value %q must not be the nil UUID", key) + } + return id, nil +} + func (m *RegistryDefault) SetPersister(p persistence.Persister) { m.persister = p } diff --git a/driver/registry_default_network_test.go b/driver/registry_default_network_test.go new file mode 100644 index 000000000000..cbab49031c56 --- /dev/null +++ b/driver/registry_default_network_test.go @@ -0,0 +1,182 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// ABOUTME: Tests that registry Init takes the network ID from config and refuses to start on any doubt. + +package driver_test + +import ( + "context" + "encoding/base64" + "os" + "path/filepath" + "testing" + "time" + + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/identity" + "github.com/ory/x/configx" + "github.com/ory/x/dbal" + "github.com/ory/x/networkx" + "github.com/ory/x/servicelocatorx" + "github.com/ory/x/sqlcon/dockertest" +) + +func TestInitNetworkFromConfig(t *testing.T) { + t.Run("config errors refuse before the database retry", testConfigErrorsRefuseBeforeRetry) + + t.Run("db=sqlite", func(t *testing.T) { + testInitNetworkFromConfig(t, "sqlite://"+filepath.Join(t.TempDir(), "db.sqlite")+"?_fk=true&mode=rwc") + }) + + t.Run("db=cockroach", func(t *testing.T) { + if testing.Short() { + t.Skip("skipping testing in short mode") + } + testInitNetworkFromConfig(t, dockertest.RunTestCockroachDBWithVersion(t, "latest-v23.1")) + }) + + t.Run("db=memory", func(t *testing.T) { + ctx := context.Background() + nid := uuid.Must(uuid.NewV4()) + + reg, err := newRegistry(ctx, map[string]interface{}{ + config.ViperKeyDSN: dbal.NewSQLiteTestDatabase(t), + config.ViperKeyNetworkID: nid.String(), + }) + require.NoError(t, err, "a memory database lives only in this process, so nothing else can seed its network") + assert.Equal(t, nid, reg.Persister().NetworkID(ctx)) + + n, err := reg.Persister().(interface { + NetworkByID(context.Context, uuid.UUID) (*networkx.Network, error) + }).NetworkByID(ctx, nid) + require.NoError(t, err) + assert.Equal(t, nid, n.ID) + }) +} + +func newRegistry(ctx context.Context, values map[string]interface{}, dOpts ...driver.RegistryOption) (driver.Registry, error) { + return driver.New(ctx, os.Stderr, servicelocatorx.NewOptions(), dOpts, + []configx.OptionModifier{configx.WithValues(values), configx.SkipValidation()}) +} + +func countNetworks(t *testing.T, c *pop.Connection) int { + n, err := c.Count(&networkx.Network{}) + require.NoError(t, err) + return n +} + +func testConfigErrorsRefuseBeforeRetry(t *testing.T) { + ctx := context.Background() + // Nothing listens on port 1, so any database call would retry for minutes. + unreachable := "postgres://kratos:secret@127.0.0.1:1/kratos?sslmode=disable" + valid := uuid.Must(uuid.NewV4()).String() + + for _, tc := range []struct { + name string + values map[string]interface{} + error string + }{ + { + name: "key missing", + values: map[string]interface{}{}, + error: `required config value "network.id" was not set`, + }, + { + name: "key not a UUID", + values: map[string]interface{}{config.ViperKeyNetworkID: "not-a-uuid"}, + error: `config value "network.id" is not a UUID: "not-a-uuid"`, + }, + { + name: "key is the nil UUID", + values: map[string]interface{}{config.ViperKeyNetworkID: uuid.Nil.String()}, + error: `config value "network.id" must not be the nil UUID`, + }, + { + name: "key equals the forbidden key", + values: map[string]interface{}{config.ViperKeyNetworkID: valid, config.ViperKeyForbiddenNetworkID: valid}, + error: `config value "network.id" equals config value "network.forbidden_id": this release must not use network ` + valid, + }, + { + name: "forbidden key not a UUID", + values: map[string]interface{}{config.ViperKeyNetworkID: valid, config.ViperKeyForbiddenNetworkID: "classic"}, + error: `config value "network.forbidden_id" is not a UUID: "classic"`, + }, + { + name: "forbidden key is the nil UUID", + values: map[string]interface{}{config.ViperKeyNetworkID: valid, config.ViperKeyForbiddenNetworkID: uuid.Nil.String()}, + error: `config value "network.forbidden_id" must not be the nil UUID`, + }, + } { + t.Run("case="+tc.name, func(t *testing.T) { + tc.values[config.ViperKeyDSN] = unreachable + + start := time.Now() + _, err := newRegistry(ctx, tc.values) + require.Error(t, err) + assert.Equal(t, tc.error, err.Error()) + assert.Less(t, time.Since(start), 10*time.Second, "a config error must not wait for the database retry") + }) + } +} + +func testInitNetworkFromConfig(t *testing.T, dsn string) { + ctx := context.Background() + + migrator, err := newRegistry(ctx, map[string]interface{}{config.ViperKeyDSN: dsn}, driver.SkipNetworkInit) + require.NoError(t, err, "network init that is skipped, as migrate does, needs no network key") + require.NoError(t, migrator.Persister().MigrateUp(ctx)) + conn := migrator.Persister().GetConnection(ctx) + + older := networkx.Network{ID: uuid.Must(uuid.NewV4()), CreatedAt: time.Now().Add(-time.Hour)} + newer := networkx.Network{ID: uuid.Must(uuid.NewV4()), CreatedAt: time.Now()} + require.NoError(t, conn.Create(&older)) + require.NoError(t, conn.Create(&newer)) + + t.Run("case=key names no row", func(t *testing.T) { + before := countNetworks(t, conn) + unknown := uuid.Must(uuid.NewV4()) + + start := time.Now() + _, err := newRegistry(ctx, map[string]interface{}{config.ViperKeyDSN: dsn, config.ViperKeyNetworkID: unknown.String()}) + require.Error(t, err) + assert.Contains(t, err.Error(), `config value "network.id" names network `+unknown.String()+", which is not in table networks") + assert.Less(t, time.Since(start), 30*time.Second, "a missing row must not wait for the database retry") + assert.Equal(t, before, countNetworks(t, conn), "a refused start must not create a network") + }) + + for _, tc := range []struct { + name string + values map[string]interface{} + }{ + {name: "valid key", values: map[string]interface{}{config.ViperKeyNetworkID: newer.ID.String()}}, + {name: "valid key and other forbidden key", values: map[string]interface{}{ + config.ViperKeyNetworkID: newer.ID.String(), + config.ViperKeyForbiddenNetworkID: older.ID.String(), + }}, + } { + t.Run("case="+tc.name, func(t *testing.T) { + tc.values[config.ViperKeyDSN] = dsn + tc.values[config.ViperKeyIdentitySchemas] = config.Schemas{{ + ID: config.DefaultIdentityTraitsSchemaID, + URL: "base64://" + base64.StdEncoding.EncodeToString([]byte(`{"type":"object","properties":{"traits":{"type":"object"}}}`)), + }} + + reg, err := newRegistry(ctx, tc.values) + require.NoError(t, err) + assert.Equal(t, newer.ID, reg.Persister().NetworkID(ctx), "config must win over the oldest network row") + + i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID) + require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i)) + var stored identity.Identity + require.NoError(t, conn.Where("id = ?", i.ID).First(&stored)) + assert.Equal(t, newer.ID, stored.NID) + }) + } +} diff --git a/embedx/config.schema.json b/embedx/config.schema.json index a45c423076dc..cff95817190d 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2023,6 +2023,26 @@ "sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc" ] }, + "network": { + "type": "object", + "title": "Network", + "description": "Selects the row in table `networks` that scopes all data of this process. Every command except `migrate` refuses to start when `id` is not set or names no row. For a memory DSN only, the process inserts the row itself, because no other process can reach that database.", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Network ID", + "description": "The ID of an existing row in table `networks`." + }, + "forbidden_id": { + "type": "string", + "format": "uuid", + "title": "Forbidden Network ID", + "description": "The process refuses to start when `id` equals this value." + } + }, + "additionalProperties": false + }, "courier": { "type": "object", "title": "Courier configuration", diff --git a/internal/testhelpers/e2e_server.go b/internal/testhelpers/e2e_server.go index b4841d4d080c..05b5664806b5 100644 --- a/internal/testhelpers/e2e_server.go +++ b/internal/testhelpers/e2e_server.go @@ -23,9 +23,14 @@ import ( "testing" "time" + "github.com/gofrs/uuid" + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" "github.com/ory/x/dbal" "github.com/ory/x/jsonnetsecure" + "github.com/ory/x/networkx" + "github.com/ory/x/servicelocatorx" "golang.org/x/sync/errgroup" @@ -72,10 +77,12 @@ func startE2EServerOnly(t *testing.T, configFile string, isTLS bool, configOptio dbt, err := os.MkdirTemp(os.TempDir(), "ory-kratos-e2e-examples-*") require.NoError(t, err) dsn := "sqlite://" + filepath.Join(dbt, "db.sqlite") + "?_fk=true&mode=rwc" + nid := uuid.Must(uuid.NewV4()) ctx := configx.ContextWithConfigOptions( context.Background(), configx.WithValue("dsn", dsn), + configx.WithValue(config.ViperKeyNetworkID, nid.String()), configx.WithValue("dev", true), configx.WithValue("log.level", "error"), configx.WithValue("log.leak_sensitive_values", true), @@ -104,6 +111,11 @@ func startE2EServerOnly(t *testing.T, configFile string, isTLS bool, configOptio _ = executor.ExecNoErr(t, "migrate", "sql", dsn, "--yes") t.Logf("Migration done") + migrated, err := driver.New(ctx, os.Stderr, servicelocatorx.NewOptions(), []driver.RegistryOption{driver.SkipNetworkInit}, + []configx.OptionModifier{configx.WithValue("dsn", dsn), configx.SkipValidation()}) + require.NoError(t, err) + require.NoError(t, migrated.Persister().GetConnection(ctx).Create(&networkx.Network{ID: nid})) + t.Log("Starting server...") stdOut, stdErr := &bytes.Buffer{}, &bytes.Buffer{} eg := executor.ExecBackground(nil, stdErr, stdOut, "serve", "--config", configFile, "--watch-courier") diff --git a/persistence/sql/migratest/migration_test.go b/persistence/sql/migratest/migration_test.go index bf727683248b..8f3bd1fee4f1 100644 --- a/persistence/sql/migratest/migration_test.go +++ b/persistence/sql/migratest/migration_test.go @@ -150,6 +150,7 @@ func testDatabase(t *testing.T, db string, c *pop.Connection) { []configx.OptionModifier{ configx.WithValues(map[string]interface{}{ config.ViperKeyDSN: url, + config.ViperKeyNetworkID: "884f556e-eb3a-4b9f-bee3-11345642c6c0", config.ViperKeyPublicBaseURL: "https://www.ory.sh/", config.ViperKeyIdentitySchemas: config.Schemas{{ID: "default", URL: "file://stub/default.schema.json"}}, config.ViperKeySecretsDefault: []string{"secret"}, diff --git a/persistence/sql/persister.go b/persistence/sql/persister.go index 85bcdf7466c8..46591be9a52f 100644 --- a/persistence/sql/persister.go +++ b/persistence/sql/persister.go @@ -27,6 +27,7 @@ import ( "github.com/ory/x/networkx" "github.com/ory/x/otelx" "github.com/ory/x/popx" + "github.com/ory/x/sqlcon" ) var _ persistence.Persister = new(Persister) @@ -133,6 +134,14 @@ func (p *Persister) DetermineNetwork(ctx context.Context) (*networkx.Network, er return p.p.Determine(ctx) } +func (p *Persister) NetworkByID(ctx context.Context, id uuid.UUID) (*networkx.Network, error) { + var n networkx.Network + if err := sqlcon.HandleError(p.Connection(ctx).Find(&n, id)); err != nil { + return nil, err + } + return &n, nil +} + func (p *Persister) Connection(ctx context.Context) *pop.Connection { return p.c.WithContext(ctx) } diff --git a/test/e2e/modd.conf b/test/e2e/modd.conf index c0241db6a80f..63f64ec6a998 100644 --- a/test/e2e/modd.conf +++ b/test/e2e/modd.conf @@ -7,5 +7,6 @@ test/e2e/mock/webhook/*.go { **/*.go !**/*_test.go { prep: go build -tags sqlite,json1 -o test/e2e/.bin/kratos . prep: test/e2e/.bin/kratos migrate sql -e --yes + prep: go run -tags sqlite,json1 ./test/e2e/seed-network daemon +sigterm: test/e2e/.bin/kratos serve --watch-courier --dev -c test/e2e/kratos.generated.yml } diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts index a3f81c73060d..e818ec613336 100644 --- a/test/e2e/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -47,12 +47,14 @@ export default defineConfig({ command: [ "cp test/e2e/playwright/kratos.base-config.json test/e2e/playwright/kratos.config.json", "go run -tags sqlite,json1 . migrate sql -e --yes", + "go run -tags sqlite,json1 ./test/e2e/seed-network", "go run -tags sqlite,json1 . serve --watch-courier --dev -c test/e2e/playwright/kratos.config.json", ].join(" && "), cwd: "../..", url: "http://localhost:4433/health/ready", env: { DSN: dbToDsn(), + NETWORK_ID: "3df92ae5-e1ac-4087-b8c2-3652ecda4c4f", COURIER_SMTP_CONNECTION_URI: "smtp://localhost:8026/?disable_starttls=true", }, diff --git a/test/e2e/run.sh b/test/e2e/run.sh index a0a6d449fc6c..2662dc83a4ef 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -22,6 +22,7 @@ export DEV_DISABLE_API_FLOW_ENFORCEMENT=true export COOKIE_SECRET=kweifawskf23weas export CSRF_COOKIE_NAME=node_csrf_token export CSRF_COOKIE_SECRET=lkaw9oe8isedrhq2 +export NETWORK_ID=3df92ae5-e1ac-4087-b8c2-3652ecda4c4f base=$(pwd) diff --git a/test/e2e/seed-network/main.go b/test/e2e/seed-network/main.go new file mode 100644 index 000000000000..644205742808 --- /dev/null +++ b/test/e2e/seed-network/main.go @@ -0,0 +1,55 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// ABOUTME: Inserts the networks row that NETWORK_ID names into the database that DSN names, so e2e Kratos can start. + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/gobuffalo/pop/v6" + "github.com/gofrs/uuid" + + "github.com/ory/kratos/driver" + "github.com/ory/x/configx" + "github.com/ory/x/dbal" + "github.com/ory/x/networkx" + "github.com/ory/x/servicelocatorx" +) + +func init() { + dbal.RegisterDriver(func() dbal.Driver { + return driver.NewRegistryDefault() + }) +} + +func seedNetwork(c *pop.Connection, nid uuid.UUID) error { + exists, err := c.Where("id = ?", nid).Exists(&networkx.Network{}) + if err != nil || exists { + return err + } + return c.Create(&networkx.Network{ID: nid}) +} + +func main() { + ctx := context.Background() + nid, err := uuid.FromString(os.Getenv("NETWORK_ID")) + if err != nil || nid == uuid.Nil { + fmt.Fprintf(os.Stderr, "NETWORK_ID must be a UUID that is not nil, got %q\n", os.Getenv("NETWORK_ID")) + os.Exit(1) + } + + reg, err := driver.New(ctx, os.Stderr, servicelocatorx.NewOptions(), []driver.RegistryOption{driver.SkipNetworkInit}, + []configx.OptionModifier{configx.SkipValidation()}) + if err != nil { + os.Exit(1) + } + if err := seedNetwork(reg.Persister().GetConnection(ctx), nid); err != nil { + fmt.Fprintf(os.Stderr, "Unable to seed network %s: %v\n", nid, err) + os.Exit(1) + } + fmt.Printf("Network %s is in table networks.\n", nid) +} diff --git a/test/e2e/seed-network/main_test.go b/test/e2e/seed-network/main_test.go new file mode 100644 index 000000000000..d6000f2dadc5 --- /dev/null +++ b/test/e2e/seed-network/main_test.go @@ -0,0 +1,42 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// ABOUTME: Tests that the e2e seed inserts the configured network once and keeps the row on a rerun. + +package main + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver" + "github.com/ory/kratos/driver/config" + "github.com/ory/x/configx" + "github.com/ory/x/networkx" + "github.com/ory/x/servicelocatorx" +) + +func TestSeedNetwork(t *testing.T) { + ctx := context.Background() + dsn := "sqlite://" + filepath.Join(t.TempDir(), "db.sqlite") + "?_fk=true&mode=rwc" + reg, err := driver.New(ctx, os.Stderr, servicelocatorx.NewOptions(), []driver.RegistryOption{driver.SkipNetworkInit}, + []configx.OptionModifier{configx.WithValue(config.ViperKeyDSN, dsn), configx.SkipValidation()}) + require.NoError(t, err) + require.NoError(t, reg.Persister().MigrateUp(ctx)) + conn := reg.Persister().GetConnection(ctx) + nid := uuid.Must(uuid.NewV4()) + + require.NoError(t, seedNetwork(conn, nid)) + require.NoError(t, seedNetwork(conn, nid), "a rerun of the e2e setup must not fail on the existing row") + + var rows []networkx.Network + require.NoError(t, conn.All(&rows)) + require.Len(t, rows, 1) + assert.Equal(t, nid, rows[0].ID) +}