Skip to content

Commit 60b5a50

Browse files
Copilotsylvainsf
andauthored
fix: compute recipe context before terraform get to satisfy required argument validation
Terraform 1.14.9 validates required module arguments during `terraform get`. The upstream kubernetes-redis module requires a `context` argument, but it was only being added AFTER the download step. This moves the recipe context computation to BEFORE the download, ensuring required arguments are present when terraform validates during module download. After download and inspection, if the module does not declare a context variable, it is removed from the config to avoid 'unexpected argument' errors during init/apply. Agent-Logs-Url: https://github.com/radius-project/radius/sessions/da1fcb4d-e5e9-4735-822f-cc86911c2544 Co-authored-by: sylvainsf <540991+sylvainsf@users.noreply.github.com>
1 parent ed7ed31 commit 60b5a50

4 files changed

Lines changed: 95 additions & 30 deletions

File tree

‎pkg/recipes/terraform/config/config.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,14 @@ func (cfg *TerraformConfig) AddRecipeContext(ctx context.Context, moduleName str
199199
return nil
200200
}
201201

202+
// RemoveRecipeContext removes the recipe context parameter from the module configuration.
203+
// This is used when the downloaded module does not declare a context variable.
204+
func (cfg *TerraformConfig) RemoveRecipeContext(moduleName string) {
205+
if mod, ok := cfg.Module[moduleName]; ok {
206+
delete(mod, recipecontext.RecipeContextParamKey)
207+
}
208+
}
209+
202210
// newModuleConfig creates a new TFModuleConfig object with the given module source and version
203211
// and also populates RecipeParams in TF module config. If same parameter key exists across params
204212
// then the last map specified gets precedence.

‎pkg/recipes/terraform/config/config_test.go‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,44 @@ func Test_AddRecipeContext(t *testing.T) {
302302
}
303303
}
304304

305+
func Test_RemoveRecipeContext(t *testing.T) {
306+
ctx := testcontext.New(t)
307+
308+
envdef := &recipes.EnvironmentDefinition{
309+
Name: testRecipeName,
310+
TemplatePath: testTemplatePath,
311+
TemplateVersion: testTemplateVersion,
312+
Parameters: envParams,
313+
}
314+
metadata := &recipes.ResourceMetadata{
315+
Name: testRecipeName,
316+
Parameters: resourceParams,
317+
}
318+
319+
tfconfig, err := New(context.Background(), testRecipeName, envdef, metadata)
320+
require.NoError(t, err)
321+
322+
// Add recipe context
323+
err = tfconfig.AddRecipeContext(ctx, testRecipeName, getTestRecipeContext())
324+
require.NoError(t, err)
325+
326+
// Verify context is present
327+
mod := tfconfig.Module[testRecipeName]
328+
_, hasContext := mod[recipecontext.RecipeContextParamKey]
329+
require.True(t, hasContext)
330+
331+
// Remove recipe context
332+
tfconfig.RemoveRecipeContext(testRecipeName)
333+
334+
// Verify context is removed
335+
_, hasContext = mod[recipecontext.RecipeContextParamKey]
336+
require.False(t, hasContext)
337+
338+
// Verify other params are still present
339+
_, hasSource := mod[moduleSourceKey]
340+
require.True(t, hasSource)
341+
}
342+
305343
func Test_AddProviders(t *testing.T) {
306344
mProvider, ucpConfiguredProviders, mBackend := setup(t)
307345
envRecipe, resourceRecipe := getTestInputs()

‎pkg/recipes/terraform/execute.go‎

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,28 @@ func (e *executor) GetRecipeMetadata(ctx context.Context, options Options) (map[
179179
return nil, err
180180
}
181181

182-
_, err = getTerraformConfig(ctx, tf.WorkingDir(), options)
182+
tfConfig, err := getTerraformConfig(ctx, tf.WorkingDir(), options)
183183
if err != nil {
184184
return nil, err
185185
}
186186

187+
// Pre-compute the recipe context and include it in the module config before downloading.
188+
// Terraform validates required module arguments during "terraform get".
189+
recipectx, err := recipecontext.New(options.ResourceRecipe, options.EnvConfig)
190+
if err != nil {
191+
return nil, err
192+
}
193+
if options.ResourceRecipe != nil {
194+
recipectx.Resource.Connections = options.ResourceRecipe.ConnectedResourcesProperties
195+
}
196+
if err = tfConfig.AddRecipeContext(ctx, options.EnvRecipe.Name, recipectx); err != nil {
197+
return nil, err
198+
}
199+
200+
if err := tfConfig.Save(ctx, tf.WorkingDir()); err != nil {
201+
return nil, err
202+
}
203+
187204
result, err := downloadAndInspect(ctx, tf, options)
188205
if err != nil {
189206
return nil, err
@@ -260,11 +277,37 @@ func (e *executor) generateConfig(ctx context.Context, tf *tfexec.Terraform, opt
260277
return "", err
261278
}
262279

280+
// Pre-compute the recipe context and include it in the module config before downloading.
281+
// Terraform validates required module arguments during "terraform get", so the context
282+
// must be present in the config before the module is downloaded.
283+
// After download and inspection, the context will be removed if the module does not declare it.
284+
recipectx, err := recipecontext.New(options.ResourceRecipe, options.EnvConfig)
285+
if err != nil {
286+
return "", err
287+
}
288+
if options.ResourceRecipe != nil {
289+
recipectx.Resource.Connections = options.ResourceRecipe.ConnectedResourcesProperties
290+
}
291+
if err = tfConfig.AddRecipeContext(ctx, options.EnvRecipe.Name, recipectx); err != nil {
292+
return "", err
293+
}
294+
295+
// Save the config with recipe context included before downloading the module.
296+
if err := tfConfig.Save(ctx, workingDir); err != nil {
297+
return "", err
298+
}
299+
263300
loadedModule, err := downloadAndInspect(ctx, tf, options)
264301
if err != nil {
265302
return "", err
266303
}
267304

305+
// After inspection, remove the recipe context if the module does not declare the context variable.
306+
// Terraform will reject unexpected arguments during init/apply.
307+
if !loadedModule.ContextVarExists {
308+
tfConfig.RemoveRecipeContext(options.EnvRecipe.Name)
309+
}
310+
268311
// Generate Terraform providers configuration for required providers and add it to the Terraform configuration.
269312
logger.Info(fmt.Sprintf("Adding provider config for required providers %+v", loadedModule.RequiredProviders))
270313
if err := tfConfig.AddProviders(ctx, loadedModule.RequiredProviders, providers.GetUCPConfiguredTerraformProviders(e.ucpConn, e.secretProvider),
@@ -292,26 +335,6 @@ func (e *executor) generateConfig(ctx context.Context, tf *tfexec.Terraform, opt
292335
}
293336
}
294337

295-
// Add recipe context parameter to the generated Terraform config's module parameters.
296-
// This should only be added if the recipe context variable is declared in the downloaded module.
297-
if loadedModule.ContextVarExists {
298-
logger.Info("Adding recipe context module result")
299-
300-
// Create the recipe context object to be passed to the recipe deployment
301-
recipectx, err := recipecontext.New(options.ResourceRecipe, options.EnvConfig)
302-
if err != nil {
303-
return "", err
304-
}
305-
306-
//update the recipe context with connected resources properties
307-
if options.ResourceRecipe != nil {
308-
recipectx.Resource.Connections = options.ResourceRecipe.ConnectedResourcesProperties
309-
}
310-
311-
if err = tfConfig.AddRecipeContext(ctx, options.EnvRecipe.Name, recipectx); err != nil {
312-
return "", err
313-
}
314-
}
315338
if loadedModule.ResultOutputExists {
316339
if err = tfConfig.AddOutputs(options.EnvRecipe.Name); err != nil {
317340
return "", err
@@ -346,12 +369,6 @@ func getTerraformConfig(ctx context.Context, workingDir string, options Options)
346369
return nil, err
347370
}
348371

349-
// Before downloading the module, Teraform configuration needs to be persisted in the working directory.
350-
// Terraform Get command uses this config file to download module from the source specified in the config.
351-
if err := tfConfig.Save(ctx, workingDir); err != nil {
352-
return nil, err
353-
}
354-
355372
return tfConfig, nil
356373
}
357374

‎pkg/recipes/terraform/execute_test.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ func Test_GetTerraformConfig_EmptyRecipeName(t *testing.T) {
115115
}
116116

117117
func Test_GetTerraformConfig_InvalidDirectory(t *testing.T) {
118+
// getTerraformConfig no longer saves the file; it only creates the in-memory config.
119+
// An invalid directory should not cause an error because saving happens separately.
118120
workingDir := "invalid-directory"
119121
options := Options{
120122
EnvRecipe: &recipes.EnvironmentDefinition{
@@ -124,9 +126,9 @@ func Test_GetTerraformConfig_InvalidDirectory(t *testing.T) {
124126
ResourceRecipe: &recipes.ResourceMetadata{},
125127
}
126128

127-
_, err := getTerraformConfig(testcontext.New(t), workingDir, options)
128-
require.Error(t, err)
129-
require.Contains(t, err.Error(), "error creating file: open invalid-directory/main.tf.json: no such file or directory")
129+
cfg, err := getTerraformConfig(testcontext.New(t), workingDir, options)
130+
require.NoError(t, err)
131+
require.NotNil(t, cfg)
130132
}
131133

132134
func TestSetEnvironmentVariables(t *testing.T) {

0 commit comments

Comments
 (0)