Skip to content

Commit d573104

Browse files
PSS: Add stateStoreInitFromConfig method to Meta (#37723)
* Add SetStateStoreChunkSize to the mock provider for tests * Implement configurable state chunk size * Add chunk size negotiation to `savedStateStore`, update happy path test to assert it's set * Update `savedStateStore` to return diagnostic if a nil factory is passed in, add unhappy path tests * Add test coverage for stateStoreConfig, including use of config overrides with state stores * Add godoc comment for backendInitFromConfig * Avoid nil error in stateStoreConfig when there's no provider factory * Add stateStoreInitFromConfig method and test coverage * Refactor stateStoreInitFromConfig to accept factory directly, instead of accepting all BackendOpts This is to avoid confusion, e.g. unnecessary duplication of config being passed in. * Implement configurable state chunk size * Remove TODO which has since been addressed * Update happy path test for `stateStoreInitFromConfig` to check that chunk size is negotiated * Update chunk size negotiation code in `stateStoreInitFromConfig` to error if the provider doesn't return a chunk size > 0 * Fix test error message --------- Co-authored-by: Radek Simko <[email protected]>
1 parent 17445f6 commit d573104

File tree

2 files changed

+456
-0
lines changed

2 files changed

+456
-0
lines changed

internal/command/meta_backend.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,14 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in
567567
}
568568

569569
// Check - is the state store type in the config supported by the provider?
570+
if opts.ProviderFactory == nil {
571+
diags = diags.Append(&hcl.Diagnostic{
572+
Severity: hcl.DiagError,
573+
Summary: "Missing provider details when configuring state store",
574+
Detail: "Terraform attempted to configure a state store and no provider factory was available to launch it. This is a bug in Terraform and should be reported.",
575+
})
576+
return nil, 0, 0, diags
577+
}
570578
provider, err := opts.ProviderFactory()
571579
if err != nil {
572580
diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err))
@@ -1768,6 +1776,12 @@ func (m *Meta) backendConfigNeedsMigration(c *configs.Backend, s *workdir.Backen
17681776
return true
17691777
}
17701778

1779+
// backendInitFromConfig returns an initialized and configured backend, using the backend.Backend interface.
1780+
// During this process:
1781+
// > Users are prompted for input if required attributes are missing.
1782+
// > The backend config is validated
1783+
// > The backend is configured
1784+
// > Service discovery is handled for operations backends (only relevant to `cloud` and `remote`)
17711785
func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.Value, tfdiags.Diagnostics) {
17721786
var diags tfdiags.Diagnostics
17731787

@@ -1833,6 +1847,155 @@ func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.V
18331847
return b, configVal, diags
18341848
}
18351849

1850+
// stateStoreInitFromConfig returns an initialized and configured state store, using the backend.Backend interface.
1851+
// During this process:
1852+
// > The provider is configured, after validating provider config
1853+
// > The state store is configured, after validating state_store config
1854+
//
1855+
// NOTE: the backend version of this method, `backendInitFromConfig`, prompts users for input if any required fields
1856+
// are missing from the backend config. In `stateStoreInitFromConfig` we don't do this, and instead users will see an error.
1857+
func (m *Meta) stateStoreInitFromConfig(c *configs.StateStore, factory providers.Factory) (backend.Backend, cty.Value, cty.Value, tfdiags.Diagnostics) {
1858+
var diags tfdiags.Diagnostics
1859+
1860+
if factory == nil {
1861+
diags = diags.Append(&hcl.Diagnostic{
1862+
Severity: hcl.DiagError,
1863+
Summary: "Missing provider details when configuring state store",
1864+
Detail: "Terraform attempted to configure a state store and no provider factory was available to launch it. This is a bug in Terraform and should be reported.",
1865+
})
1866+
return nil, cty.NilVal, cty.NilVal, diags
1867+
}
1868+
provider, err := factory()
1869+
if err != nil {
1870+
diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err))
1871+
return nil, cty.NilVal, cty.NilVal, diags
1872+
}
1873+
// We purposefully don't have a deferred call to the provider's Close method here because the calling code needs a
1874+
// running provider instance inside the returned backend.Backend instance.
1875+
// Stopping the provider process is the responsibility of the calling code.
1876+
1877+
resp := provider.GetProviderSchema()
1878+
1879+
if len(resp.StateStores) == 0 {
1880+
diags = diags.Append(&hcl.Diagnostic{
1881+
Severity: hcl.DiagError,
1882+
Summary: "Provider does not support pluggable state storage",
1883+
Detail: fmt.Sprintf("There are no state stores implemented by provider %s (%q)",
1884+
c.Provider.Name,
1885+
c.ProviderAddr),
1886+
Subject: &c.DeclRange,
1887+
})
1888+
return nil, cty.NilVal, cty.NilVal, diags
1889+
}
1890+
1891+
schema, exists := resp.StateStores[c.Type]
1892+
if !exists {
1893+
suggestions := slices.Sorted(maps.Keys(resp.StateStores))
1894+
suggestion := didyoumean.NameSuggestion(c.Type, suggestions)
1895+
if suggestion != "" {
1896+
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
1897+
}
1898+
diags = diags.Append(&hcl.Diagnostic{
1899+
Severity: hcl.DiagError,
1900+
Summary: "State store not implemented by the provider",
1901+
Detail: fmt.Sprintf("State store %q is not implemented by provider %s (%q)%s",
1902+
c.Type, c.Provider.Name,
1903+
c.ProviderAddr, suggestion),
1904+
Subject: &c.DeclRange,
1905+
})
1906+
return nil, cty.NilVal, cty.NilVal, diags
1907+
}
1908+
1909+
// Handle the nested provider block.
1910+
pDecSpec := resp.Provider.Body.DecoderSpec()
1911+
pConfig := c.Provider.Config
1912+
providerConfigVal, pDecDiags := hcldec.Decode(pConfig, pDecSpec, nil)
1913+
diags = diags.Append(pDecDiags)
1914+
1915+
// Handle the schema for the state store itself, excluding the provider block.
1916+
ssdecSpec := schema.Body.DecoderSpec()
1917+
stateStoreConfigVal, ssDecDiags := hcldec.Decode(c.Config, ssdecSpec, nil)
1918+
diags = diags.Append(ssDecDiags)
1919+
if ssDecDiags.HasErrors() {
1920+
return nil, cty.NilVal, cty.NilVal, diags
1921+
}
1922+
1923+
// Validate and configure the provider
1924+
//
1925+
// NOTE: there are no marks we need to remove at this point.
1926+
// We haven't added marks since the provider config from the backend state was used
1927+
// because the state-storage provider's config isn't going to be presented to the user via terminal output or diags.
1928+
validateResp := provider.ValidateProviderConfig(providers.ValidateProviderConfigRequest{
1929+
Config: providerConfigVal,
1930+
})
1931+
diags = diags.Append(validateResp.Diagnostics)
1932+
if validateResp.Diagnostics.HasErrors() {
1933+
return nil, cty.NilVal, cty.NilVal, diags
1934+
}
1935+
1936+
configureResp := provider.ConfigureProvider(providers.ConfigureProviderRequest{
1937+
TerraformVersion: tfversion.String(),
1938+
Config: providerConfigVal,
1939+
})
1940+
diags = diags.Append(configureResp.Diagnostics)
1941+
if configureResp.Diagnostics.HasErrors() {
1942+
return nil, cty.NilVal, cty.NilVal, diags
1943+
}
1944+
1945+
// Validate state store config and configure the state store
1946+
//
1947+
// NOTE: there are no marks we need to remove at this point.
1948+
// We haven't added marks since the provider config from the backend state was used
1949+
// because the state-storage provider's config isn't going to be presented to the user via terminal output or diags.
1950+
validateStoreResp := provider.ValidateStateStoreConfig(providers.ValidateStateStoreConfigRequest{
1951+
TypeName: c.Type,
1952+
Config: stateStoreConfigVal,
1953+
})
1954+
diags = diags.Append(validateStoreResp.Diagnostics)
1955+
if validateStoreResp.Diagnostics.HasErrors() {
1956+
return nil, cty.NilVal, cty.NilVal, diags
1957+
}
1958+
1959+
cfgStoreResp := provider.ConfigureStateStore(providers.ConfigureStateStoreRequest{
1960+
TypeName: c.Type,
1961+
Config: stateStoreConfigVal,
1962+
Capabilities: providers.StateStoreClientCapabilities{
1963+
ChunkSize: defaultStateStoreChunkSize,
1964+
},
1965+
})
1966+
diags = diags.Append(cfgStoreResp.Diagnostics)
1967+
if cfgStoreResp.Diagnostics.HasErrors() {
1968+
return nil, cty.NilVal, cty.NilVal, diags
1969+
}
1970+
1971+
chunkSize := cfgStoreResp.Capabilities.ChunkSize
1972+
if chunkSize == 0 || chunkSize > maxStateStoreChunkSize {
1973+
diags = diags.Append(fmt.Errorf("Failed to negotiate acceptable chunk size. "+
1974+
"Expected size > 0 and <= %d bytes, provider wants %d bytes",
1975+
maxStateStoreChunkSize, chunkSize,
1976+
))
1977+
return nil, cty.NilVal, cty.NilVal, diags
1978+
}
1979+
1980+
p, ok := provider.(providers.StateStoreChunkSizeSetter)
1981+
if !ok {
1982+
msg := fmt.Sprintf("Unable to set chunk size for provider %s; this is a bug in Terraform - please report it", c.Type)
1983+
panic(msg)
1984+
}
1985+
// casting to int here is okay because the number should never exceed int32
1986+
p.SetStateStoreChunkSize(c.Type, int(chunkSize))
1987+
1988+
// Now we have a fully configured state store, ready to be used.
1989+
// To make it usable we need to return it in a backend.Backend interface.
1990+
b, err := backendPluggable.NewPluggable(provider, c.Type)
1991+
if err != nil {
1992+
diags = diags.Append(err)
1993+
return nil, cty.NilVal, cty.NilVal, diags
1994+
}
1995+
1996+
return b, stateStoreConfigVal, providerConfigVal, diags
1997+
}
1998+
18361999
// Helper method to get aliases from the enhanced backend and alias them
18372000
// in the Meta service discovery. It's unfortunate that the Meta backend
18382001
// is modifying the service discovery at this level, but the owner

0 commit comments

Comments
 (0)