diff --git a/config/app_configuration.go b/config/app_configuration.go index 2369270..7a74e8b 100644 --- a/config/app_configuration.go +++ b/config/app_configuration.go @@ -110,10 +110,10 @@ func (a *AppConfiguration) Normalize(ctx context.Context, pc core.ObjectPathCont return errs } -func (a *AppConfiguration) ToBlock(orgName string, stackId int64) types.Block { - block := a.BlockConfiguration.ToBlock(orgName, stackId) - block.Framework = a.Framework - return block +func (a *AppConfiguration) toBlockDefinition(orgName string, stackId int64) BlockDefinition { + def := a.BlockConfiguration.toBlockDefinition(orgName, stackId) + def.Block.Framework = a.Framework + return def } func (a *AppConfiguration) ApplyChangesTo(ic core.IacContext, updater core.WorkspaceConfigUpdater) error { diff --git a/config/block_configuration.go b/config/block_configuration.go index 11703fe..01539f2 100644 --- a/config/block_configuration.go +++ b/config/block_configuration.go @@ -36,7 +36,6 @@ type BlockConfiguration struct { ModuleConstraint string `json:"moduleConstraint"` Variables VariableConfigurations `json:"vars"` Connections ConnectionConfigurations `json:"connections"` - IsShared bool `json:"isShared"` Metadata MetadataConfiguration `json:"metadata"` // These fields are populated via Resolve() @@ -97,7 +96,6 @@ func blockConfigFromYaml(name string, value yaml.BlockConfiguration, blockType B ModuleConstraint: moduleConstraint, Variables: convertVariables(value.Variables), Connections: convertConnections(value.Connections), - IsShared: value.IsShared, Metadata: convertMetadata(value.Metadata), } } @@ -184,19 +182,22 @@ func (b *BlockConfiguration) Normalize(ctx context.Context, pc core.ObjectPathCo return b.Connections.Normalize(ctx, pc, resolver) } -func (b *BlockConfiguration) ToBlock(orgName string, stackId int64) types.Block { - block := types.Block{ - Type: string(b.Type), - OrgName: orgName, - StackId: stackId, - Name: b.Name, - IsShared: b.IsShared, - DnsName: "", - ModuleSource: b.ModuleSource, - ModuleSourceVersion: b.ModuleConstraint, - Connections: b.Connections.DesiredTargets(), - } - return block +// toBlockDefinition flattens the configuration into an API block. +// Fields IaC does not govern (IsShared, Repo, Framework for non-apps, ...) stay zero so a +// consumer syncing blocks can tell "not declared" from a declared value. +func (b *BlockConfiguration) toBlockDefinition(orgName string, stackId int64) BlockDefinition { + return BlockDefinition{ + Block: types.Block{ + Type: string(b.Type), + OrgName: orgName, + StackId: stackId, + Name: b.Name, + DnsName: "", + ModuleSource: b.ModuleSource, + ModuleSourceVersion: b.ModuleConstraint, + Connections: b.Connections.DesiredTargets(), + }, + } } func (b *BlockConfiguration) ApplyChangesTo(ic core.IacContext, updater core.WorkspaceConfigUpdater) error { diff --git a/config/block_configuration_test.go b/config/block_configuration_test.go index 2c50e1b..c0f758f 100644 --- a/config/block_configuration_test.go +++ b/config/block_configuration_test.go @@ -88,3 +88,58 @@ func TestBlockConfiguration_ApplyChangesTo_dataClassification(t *testing.T) { assert.Equal(t, types.ClassificationLevel(""), wc.Metadata.DataClassification) }) } + +// Sharing is not governed by IaC, so a definition never carries it; a block sync must leave +// the stored flag alone. +func TestBlockConfiguration_toBlockDefinitionOmitsUngovernedFields(t *testing.T) { + bc := blockConfigFromYaml("db", yaml.BlockConfiguration{ModuleSource: "nullstone/aws-rds-postgres"}, BlockTypeDatastore, types.CategoryDatastore) + def := bc.toBlockDefinition("acme", 100) + assert.False(t, def.Block.IsShared) + assert.Empty(t, def.Block.DnsName) + assert.Empty(t, def.Block.Framework) + assert.Empty(t, def.Block.Repo) + assert.Equal(t, "latest", def.Block.ModuleSourceVersion, "omitted module_version resolves to latest") +} + +func TestEnvConfiguration_ToBlockDefinitions(t *testing.T) { + ec := &EnvConfiguration{ + Applications: map[string]*AppConfiguration{ + "api": { + BlockConfiguration: BlockConfiguration{Type: BlockTypeApplication, Name: "api", ModuleSource: "nullstone/aws-fargate-service", ModuleConstraint: "latest"}, + Framework: "go", + }, + }, + Datastores: map[string]*DatastoreConfiguration{ + "db": {BlockConfiguration: BlockConfiguration{Type: BlockTypeDatastore, Name: "db", ModuleSource: "nullstone/aws-rds-postgres"}}, + }, + Subdomains: map[string]*SubdomainConfiguration{ + "web": { + BlockConfiguration: BlockConfiguration{Type: BlockTypeSubdomain, Name: "web", ModuleSource: "nullstone/aws-subdomain"}, + SubdomainNameTemplate: ptr("web.{{ NULLSTONE_ENV }}"), + }, + }, + Blocks: map[string]*BlockConfiguration{ + "queue": {Type: BlockTypeBlock, Name: "queue", ModuleSource: "nullstone/aws-sqs-queue"}, + }, + } + + defs := ec.ToBlockDefinitions("acme", 100) + byName := map[string]BlockDefinition{} + for _, def := range defs { + byName[def.Block.Name] = def + } + require.Len(t, byName, 4) + + assert.Equal(t, string(BlockTypeApplication), byName["api"].Block.Type) + assert.Equal(t, "go", byName["api"].Block.Framework) + + assert.Equal(t, string(BlockTypeDatastore), byName["db"].Block.Type) + assert.Empty(t, byName["db"].Block.Framework) + + assert.Equal(t, "web.{{ NULLSTONE_ENV }}", byName["web"].Block.DnsName) + + assert.Equal(t, string(BlockTypeBlock), byName["queue"].Block.Type) + assert.Empty(t, byName["queue"].Block.DnsName) + + assert.Empty(t, (*EnvConfiguration)(nil).ToBlockDefinitions("acme", 100)) +} diff --git a/config/domain_configuration.go b/config/domain_configuration.go index bc8ed00..da00a46 100644 --- a/config/domain_configuration.go +++ b/config/domain_configuration.go @@ -12,12 +12,12 @@ type DomainConfiguration struct { DomainNameTemplate *string `json:"domainNameTemplate"` } -func (d *DomainConfiguration) ToBlock(orgName string, stackId int64) types.Block { - block := d.BlockConfiguration.ToBlock(orgName, stackId) +func (d *DomainConfiguration) toBlockDefinition(orgName string, stackId int64) BlockDefinition { + def := d.BlockConfiguration.toBlockDefinition(orgName, stackId) if d.DomainNameTemplate != nil { - block.DnsName = *d.DomainNameTemplate + def.Block.DnsName = *d.DomainNameTemplate } - return block + return def } func (d *DomainConfiguration) ApplyChangesTo(ic core.IacContext, updater core.WorkspaceConfigUpdater) error { diff --git a/config/env_configuration.go b/config/env_configuration.go index aeac245..be0859d 100644 --- a/config/env_configuration.go +++ b/config/env_configuration.go @@ -246,41 +246,50 @@ func (e *EnvConfiguration) Normalize(ctx context.Context, resolver core.Normaliz return nil } -func (e *EnvConfiguration) ToBlocks(orgName string, stackId int64) types.Blocks { - blocks := make([]types.Block, 0) +// BlockDefinition is a block as declared in config.yml. Block carries only the fields IaC +// governs; anything IaC does not declare is left at its zero value. It is a struct so that +// declared-only information which the flat types.Block cannot express can be added later. +type BlockDefinition struct { + Block types.Block +} + +// ToBlockDefinitions flattens every block in the file into sync definitions. +// Callers that only need the API shape read Block off each definition. +func (e *EnvConfiguration) ToBlockDefinitions(orgName string, stackId int64) []BlockDefinition { + defs := make([]BlockDefinition, 0) if e == nil { - return blocks + return defs } for _, app := range e.Applications { - blocks = append(blocks, app.ToBlock(orgName, stackId)) + defs = append(defs, app.toBlockDefinition(orgName, stackId)) } for _, ds := range e.Datastores { - blocks = append(blocks, ds.ToBlock(orgName, stackId)) + defs = append(defs, ds.toBlockDefinition(orgName, stackId)) } for _, sub := range e.Subdomains { - blocks = append(blocks, sub.ToBlock(orgName, stackId)) + defs = append(defs, sub.toBlockDefinition(orgName, stackId)) } for _, d := range e.Domains { - blocks = append(blocks, d.ToBlock(orgName, stackId)) + defs = append(defs, d.toBlockDefinition(orgName, stackId)) } for _, i := range e.Ingresses { - blocks = append(blocks, i.ToBlock(orgName, stackId)) + defs = append(defs, i.toBlockDefinition(orgName, stackId)) } for _, cn := range e.ClusterNamespaces { - blocks = append(blocks, cn.ToBlock(orgName, stackId)) + defs = append(defs, cn.toBlockDefinition(orgName, stackId)) } for _, c := range e.Clusters { - blocks = append(blocks, c.ToBlock(orgName, stackId)) + defs = append(defs, c.toBlockDefinition(orgName, stackId)) } for _, n := range e.Networks { - blocks = append(blocks, n.ToBlock(orgName, stackId)) + defs = append(defs, n.toBlockDefinition(orgName, stackId)) } for _, b := range e.Blocks { - blocks = append(blocks, b.ToBlock(orgName, stackId)) + defs = append(defs, b.toBlockDefinition(orgName, stackId)) } - return blocks + return defs } func (e *EnvConfiguration) ApplyChangesTo(block types.Block, updater core.WorkspaceConfigUpdater) error { diff --git a/config/env_configuration_test.go b/config/env_configuration_test.go index cd59a88..b960aa2 100644 --- a/config/env_configuration_test.go +++ b/config/env_configuration_test.go @@ -343,10 +343,10 @@ func TestConvertConfiguration(t *testing.T) { syncBlocks := func(base *EnvConfiguration, resolver *find.ResourceResolver) { backfill := make([]types.Block, 0) - for _, block := range base.ToBlocks(defaults.OrgName, resolver.CurStackId) { - if _, err := resolver.FindBlock(context.TODO(), types.ConnectionTarget{BlockName: block.Name}); err != nil { + for _, def := range base.ToBlockDefinitions(defaults.OrgName, resolver.CurStackId) { + if _, err := resolver.FindBlock(context.TODO(), types.ConnectionTarget{BlockName: def.Block.Name}); err != nil { if find.IsMissingResource(err) { - backfill = append(backfill, block) + backfill = append(backfill, def.Block) } } } diff --git a/config/subdomain_configuration.go b/config/subdomain_configuration.go index 359926f..637f0ee 100644 --- a/config/subdomain_configuration.go +++ b/config/subdomain_configuration.go @@ -24,12 +24,12 @@ type SubdomainConfiguration struct { Reservation *types.SubdomainReservation `json:"reservation,omitempty"` } -func (s *SubdomainConfiguration) ToBlock(orgName string, stackId int64) types.Block { - block := s.BlockConfiguration.ToBlock(orgName, stackId) +func (s *SubdomainConfiguration) toBlockDefinition(orgName string, stackId int64) BlockDefinition { + def := s.BlockConfiguration.toBlockDefinition(orgName, stackId) if s.SubdomainNameTemplate != nil { - block.DnsName = *s.SubdomainNameTemplate + def.Block.DnsName = *s.SubdomainNameTemplate } - return block + return def } func (s *SubdomainConfiguration) Resolve(ctx context.Context, resolver core.ResolveResolver, finder core.IacFinder, ic core.IacContext, pc core.ObjectPathContext) core.ResolveErrors { diff --git a/yaml/block_configuration.go b/yaml/block_configuration.go index f602d32..fe73928 100644 --- a/yaml/block_configuration.go +++ b/yaml/block_configuration.go @@ -9,7 +9,6 @@ type BlockConfiguration struct { ModuleConstraint *string `yaml:"module_version,omitempty" json:"moduleVersion"` Variables map[string]any `yaml:"vars,omitempty" json:"vars"` Connections ConnectionConstraints `yaml:"connections,omitempty" json:"connections"` - IsShared bool `yaml:"is_shared,omitempty" json:"isShared"` // Metadata holds governance/descriptive metadata (e.g. data classification). Metadata *MetadataConfiguration `yaml:"metadata,omitempty" json:"metadata,omitempty"` } diff --git a/yaml/block_configuration_test.go b/yaml/block_configuration_test.go index 76da8ab..23b047c 100644 --- a/yaml/block_configuration_test.go +++ b/yaml/block_configuration_test.go @@ -40,3 +40,11 @@ func TestMetadataFromWorkspaceConfig(t *testing.T) { assert.Equal(t, "restricted", *got.DataClassification) }) } + +// is_shared is not an IaC setting (sharing is chosen in the UI); a stray key parses without error +// and has no effect, matching the lenient decoder used for every other unknown key. +func TestBlockConfiguration_IgnoresIsShared(t *testing.T) { + var bc BlockConfiguration + require.NoError(t, goyaml.Unmarshal([]byte("module: nullstone/aws-rds-postgres\nis_shared: true\n"), &bc)) + assert.Equal(t, "nullstone/aws-rds-postgres", bc.ModuleSource) +}