diff --git a/server/internal/resource/migrations/1_0_0_test.go b/server/internal/resource/migrations/1_0_0_test.go index 703880a2..fb74a858 100644 --- a/server/internal/resource/migrations/1_0_0_test.go +++ b/server/internal/resource/migrations/1_0_0_test.go @@ -139,7 +139,10 @@ func TestVersion_1_0_0(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - state := resource.NewState() + state := &resource.State{ + Version: resource.StateVersion_1_0_0, + Resources: map[resource.Type]map[string]*resource.ResourceData{}, + } state.Add(tc.in...) migration := &migrations.Version_1_0_0{} diff --git a/server/internal/resource/migrations/1_1_0.go b/server/internal/resource/migrations/1_1_0.go new file mode 100644 index 00000000..b2691e5a --- /dev/null +++ b/server/internal/resource/migrations/1_1_0.go @@ -0,0 +1,39 @@ +package migrations + +import ( + "github.com/pgEdge/control-plane/server/internal/ds" + "github.com/pgEdge/control-plane/server/internal/resource" +) + +var _ resource.StateMigration = (*Version_1_1_0)(nil) + +// Version_1_1_0 removes swarm.service_user_role resources and scrubs +// references to them from all other resources' dependency lists. +// Services now use connect_as to reference database_users directly. +type Version_1_1_0 struct{} + +func (v *Version_1_1_0) Version() *ds.Version { + return resource.StateVersion_1_1_0 +} + +func (v *Version_1_1_0) Run(state *resource.State) error { + const serviceUserRoleType resource.Type = "swarm.service_user_role" + + // 1. Delete all service_user_role resources from state + delete(state.Resources, serviceUserRoleType) + + // 2. Remove service_user_role from all other resources' dependency lists + for _, resources := range state.Resources { + for _, data := range resources { + filtered := data.Dependencies[:0] + for _, dep := range data.Dependencies { + if dep.Type != serviceUserRoleType { + filtered = append(filtered, dep) + } + } + data.Dependencies = filtered + } + } + + return nil +} diff --git a/server/internal/resource/migrations/1_1_0_test.go b/server/internal/resource/migrations/1_1_0_test.go new file mode 100644 index 00000000..3dad2147 --- /dev/null +++ b/server/internal/resource/migrations/1_1_0_test.go @@ -0,0 +1,168 @@ +package migrations_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pgEdge/control-plane/server/internal/resource" + "github.com/pgEdge/control-plane/server/internal/resource/migrations" +) + +func TestVersion_1_1_0(t *testing.T) { + serviceUserRoleType := resource.Type("swarm.service_user_role") + mcpConfigType := resource.Type("swarm.mcp_config") + serviceInstanceSpecType := resource.Type("swarm.service_instance_spec") + networkType := resource.Type("swarm.network") + dirType := resource.Type("swarm.dir") + + svcRoleRO := resource.Identifier{ID: "appmcp-ro", Type: serviceUserRoleType} + svcRoleRW := resource.Identifier{ID: "appmcp-rw", Type: serviceUserRoleType} + networkDep := resource.Identifier{ID: "db-network", Type: networkType} + dirDep := resource.Identifier{ID: "data-dir", Type: dirType} + + t.Run("removes service_user_role resources", func(t *testing.T) { + state := &resource.State{ + Version: resource.StateVersion_1_0_0.Clone(), + Resources: map[resource.Type]map[string]*resource.ResourceData{ + serviceUserRoleType: { + "appmcp-ro": {Identifier: svcRoleRO}, + "appmcp-rw": {Identifier: svcRoleRW}, + }, + mcpConfigType: { + "mcp-cfg": { + Identifier: resource.Identifier{ID: "mcp-cfg", Type: mcpConfigType}, + Dependencies: []resource.Identifier{dirDep, svcRoleRO, svcRoleRW}, + }, + }, + serviceInstanceSpecType: { + "svc-spec": { + Identifier: resource.Identifier{ID: "svc-spec", Type: serviceInstanceSpecType}, + Dependencies: []resource.Identifier{networkDep, svcRoleRO, svcRoleRW}, + }, + }, + }, + } + + migration := &migrations.Version_1_1_0{} + err := migration.Run(state) + require.NoError(t, err) + + // service_user_role resources should be gone + _, exists := state.Resources[serviceUserRoleType] + assert.False(t, exists, "service_user_role resources should be deleted") + + // mcp_config should have service_user_role deps removed + mcpCfg := state.Resources[mcpConfigType]["mcp-cfg"] + require.NotNil(t, mcpCfg) + assert.Equal(t, []resource.Identifier{dirDep}, mcpCfg.Dependencies) + + // service_instance_spec should have service_user_role deps removed + svcSpec := state.Resources[serviceInstanceSpecType]["svc-spec"] + require.NotNil(t, svcSpec) + assert.Equal(t, []resource.Identifier{networkDep}, svcSpec.Dependencies) + }) + + t.Run("removes service_user_role resources across all service types", func(t *testing.T) { + ragConfigType := resource.Type("swarm.rag_config") + postgrestConfigType := resource.Type("swarm.postgrest_config") + + mcpRO := resource.Identifier{ID: "appmcp-ro", Type: serviceUserRoleType} + mcpRW := resource.Identifier{ID: "appmcp-rw", Type: serviceUserRoleType} + ragRO := resource.Identifier{ID: "apprag-ro", Type: serviceUserRoleType} + prstRO := resource.Identifier{ID: "appprst-ro", Type: serviceUserRoleType} + prstRW := resource.Identifier{ID: "appprst-rw", Type: serviceUserRoleType} + + state := &resource.State{ + Version: resource.StateVersion_1_0_0.Clone(), + Resources: map[resource.Type]map[string]*resource.ResourceData{ + serviceUserRoleType: { + "appmcp-ro": {Identifier: mcpRO}, + "appmcp-rw": {Identifier: mcpRW}, + "apprag-ro": {Identifier: ragRO}, + "appprst-ro": {Identifier: prstRO}, + "appprst-rw": {Identifier: prstRW}, + }, + mcpConfigType: { + "mcp-cfg": { + Identifier: resource.Identifier{ID: "mcp-cfg", Type: mcpConfigType}, + Dependencies: []resource.Identifier{dirDep, mcpRO, mcpRW}, + }, + }, + ragConfigType: { + "rag-cfg": { + Identifier: resource.Identifier{ID: "rag-cfg", Type: ragConfigType}, + Dependencies: []resource.Identifier{dirDep, ragRO}, + }, + }, + postgrestConfigType: { + "prst-cfg": { + Identifier: resource.Identifier{ID: "prst-cfg", Type: postgrestConfigType}, + Dependencies: []resource.Identifier{dirDep, prstRO, prstRW}, + }, + }, + serviceInstanceSpecType: { + "svc-spec": { + Identifier: resource.Identifier{ID: "svc-spec", Type: serviceInstanceSpecType}, + Dependencies: []resource.Identifier{networkDep, mcpRO, mcpRW, ragRO, prstRO, prstRW}, + }, + }, + }, + } + + migration := &migrations.Version_1_1_0{} + err := migration.Run(state) + require.NoError(t, err) + + // All service_user_role resources should be gone + _, exists := state.Resources[serviceUserRoleType] + assert.False(t, exists, "service_user_role resources should be deleted") + + // MCP config: only dirDep remains + assert.Equal(t, []resource.Identifier{dirDep}, state.Resources[mcpConfigType]["mcp-cfg"].Dependencies) + + // RAG config: only dirDep remains + assert.Equal(t, []resource.Identifier{dirDep}, state.Resources[ragConfigType]["rag-cfg"].Dependencies) + + // PostgREST config: only dirDep remains + assert.Equal(t, []resource.Identifier{dirDep}, state.Resources[postgrestConfigType]["prst-cfg"].Dependencies) + + // service_instance_spec: only networkDep remains + assert.Equal(t, []resource.Identifier{networkDep}, state.Resources[serviceInstanceSpecType]["svc-spec"].Dependencies) + }) + + t.Run("no-op when no service_user_role resources exist", func(t *testing.T) { + state := &resource.State{ + Version: resource.StateVersion_1_0_0.Clone(), + Resources: map[resource.Type]map[string]*resource.ResourceData{ + mcpConfigType: { + "mcp-cfg": { + Identifier: resource.Identifier{ID: "mcp-cfg", Type: mcpConfigType}, + Dependencies: []resource.Identifier{dirDep}, + }, + }, + }, + } + + migration := &migrations.Version_1_1_0{} + err := migration.Run(state) + require.NoError(t, err) + + // mcp_config should be untouched + mcpCfg := state.Resources[mcpConfigType]["mcp-cfg"] + require.NotNil(t, mcpCfg) + assert.Equal(t, []resource.Identifier{dirDep}, mcpCfg.Dependencies) + }) + + t.Run("empty state", func(t *testing.T) { + state := &resource.State{ + Version: resource.StateVersion_1_0_0.Clone(), + Resources: map[resource.Type]map[string]*resource.ResourceData{}, + } + + migration := &migrations.Version_1_1_0{} + err := migration.Run(state) + require.NoError(t, err) + }) +} diff --git a/server/internal/resource/migrations/provide.go b/server/internal/resource/migrations/provide.go index f532e634..ec9797b0 100644 --- a/server/internal/resource/migrations/provide.go +++ b/server/internal/resource/migrations/provide.go @@ -14,6 +14,7 @@ func provideStateMigrations(i *do.Injector) { do.Provide(i, func(i *do.Injector) (*resource.StateMigrations, error) { return resource.NewStateMigrations([]resource.StateMigration{ &Version_1_0_0{}, + &Version_1_1_0{}, }), nil }) } diff --git a/server/internal/resource/state.go b/server/internal/resource/state.go index 551ef87b..b562ccbd 100644 --- a/server/internal/resource/state.go +++ b/server/internal/resource/state.go @@ -15,8 +15,9 @@ import ( var ( StateVersion_1_0_0 = ds.MustParseVersion("1.0.0") + StateVersion_1_1_0 = ds.MustParseVersion("1.1.0") - CurrentVersion = StateVersion_1_0_0 + CurrentVersion = StateVersion_1_1_0 ) var (