github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-internal/moduletest/provider.go (about)

     1  package moduletest
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"sync"
     7  
     8  	"github.com/zclconf/go-cty/cty"
     9  	"github.com/zclconf/go-cty/cty/gocty"
    10  	ctyjson "github.com/zclconf/go-cty/cty/json"
    11  
    12  	"github.com/hashicorp/hcl/v2/hclsyntax"
    13  	"github.com/muratcelep/terraform/not-internal/configs/configschema"
    14  	"github.com/muratcelep/terraform/not-internal/providers"
    15  	"github.com/muratcelep/terraform/not-internal/repl"
    16  	"github.com/muratcelep/terraform/not-internal/tfdiags"
    17  )
    18  
    19  // Provider is an implementation of providers.Interface which we're
    20  // using as a likely-only-temporary vehicle for research on an opinionated
    21  // module testing workflow in Terraform.
    22  //
    23  // We expose this to configuration as "terraform.io/builtin/test", but
    24  // any attempt to configure it will emit a warning that it is experimental
    25  // and likely to change or be removed entirely in future Terraform CLI
    26  // releases.
    27  //
    28  // The testing provider exists to gather up test results during a Terraform
    29  // apply operation. Its "test_results" managed resource type doesn't have any
    30  // user-visible effect on its own, but when used in conjunction with the
    31  // "terraform test" experimental command it is the intermediary that holds
    32  // the test results while the test runs, so that the test command can then
    33  // report them.
    34  //
    35  // For correct behavior of the assertion tracking, the "terraform test"
    36  // command must be sure to use the same instance of Provider for both the
    37  // plan and apply steps, so that the assertions that were planned can still
    38  // be tracked during apply. For other commands that don't explicitly support
    39  // test assertions, the provider will still succeed but the assertions data
    40  // may not be complete if the apply step fails.
    41  type Provider struct {
    42  	// components tracks all of the "component" names that have been
    43  	// used in test assertions resources so far. Each resource must have
    44  	// a unique component name.
    45  	components map[string]*Component
    46  
    47  	// Must lock mutex in order to interact with the components map, because
    48  	// test assertions can potentially run concurrently.
    49  	mutex sync.RWMutex
    50  }
    51  
    52  var _ providers.Interface = (*Provider)(nil)
    53  
    54  // NewProvider returns a new instance of the test provider.
    55  func NewProvider() *Provider {
    56  	return &Provider{
    57  		components: make(map[string]*Component),
    58  	}
    59  }
    60  
    61  // TestResults returns the current record of test results tracked inside the
    62  // provider.
    63  //
    64  // The result is a direct reference to the not-internal state of the provider,
    65  // so the caller mustn't modify it nor store it across calls to provider
    66  // operations.
    67  func (p *Provider) TestResults() map[string]*Component {
    68  	return p.components
    69  }
    70  
    71  // Reset returns the recieving provider back to its original state, with no
    72  // recorded test results.
    73  //
    74  // It additionally detaches the instance from any data structure previously
    75  // returned by method TestResults, freeing the caller from the constraints
    76  // in its documentation about mutability and storage.
    77  //
    78  // For convenience in the presumed common case of resetting as part of
    79  // capturing the results for storage, this method also returns the result
    80  // that method TestResults would've returned if called prior to the call
    81  // to Reset.
    82  func (p *Provider) Reset() map[string]*Component {
    83  	p.mutex.Lock()
    84  	log.Print("[TRACE] moduletest.Provider: Reset")
    85  	ret := p.components
    86  	p.components = make(map[string]*Component)
    87  	p.mutex.Unlock()
    88  	return ret
    89  }
    90  
    91  // GetProviderSchema returns the complete schema for the provider.
    92  func (p *Provider) GetProviderSchema() providers.GetProviderSchemaResponse {
    93  	return providers.GetProviderSchemaResponse{
    94  		ResourceTypes: map[string]providers.Schema{
    95  			"test_assertions": testAssertionsSchema,
    96  		},
    97  	}
    98  }
    99  
   100  // ValidateProviderConfig validates the provider configuration.
   101  func (p *Provider) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse {
   102  	// This provider has no configurable settings, so nothing to validate.
   103  	var res providers.ValidateProviderConfigResponse
   104  	return res
   105  }
   106  
   107  // ConfigureProvider configures and initializes the provider.
   108  func (p *Provider) ConfigureProvider(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
   109  	// This provider has no configurable settings, but we use the configure
   110  	// request as an opportunity to generate a warning about it being
   111  	// experimental.
   112  	var res providers.ConfigureProviderResponse
   113  	res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   114  		tfdiags.Warning,
   115  		"The test provider is experimental",
   116  		"The Terraform team is using the test provider (terraform.io/builtin/test) as part of ongoing research about declarative testing of Terraform modules.\n\nThe availability and behavior of this provider is expected to change significantly even in patch releases, so we recommend using this provider only in test configurations and constraining your test configurations to an exact Terraform version.",
   117  		nil,
   118  	))
   119  	return res
   120  }
   121  
   122  // ValidateResourceConfig is used to validate configuration values for a resource.
   123  func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
   124  	log.Print("[TRACE] moduletest.Provider: ValidateResourceConfig")
   125  
   126  	var res providers.ValidateResourceConfigResponse
   127  	if req.TypeName != "test_assertions" { // we only have one resource type
   128  		res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
   129  		return res
   130  	}
   131  
   132  	config := req.Config
   133  	if !config.GetAttr("component").IsKnown() {
   134  		res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   135  			tfdiags.Error,
   136  			"Invalid component expression",
   137  			"The component name must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.",
   138  			cty.GetAttrPath("component"),
   139  		))
   140  	}
   141  	if !hclsyntax.ValidIdentifier(config.GetAttr("component").AsString()) {
   142  		res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   143  			tfdiags.Error,
   144  			"Invalid component name",
   145  			"The component name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.",
   146  			cty.GetAttrPath("component"),
   147  		))
   148  	}
   149  	for it := config.GetAttr("equal").ElementIterator(); it.Next(); {
   150  		k, obj := it.Element()
   151  		if !hclsyntax.ValidIdentifier(k.AsString()) {
   152  			res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   153  				tfdiags.Error,
   154  				"Invalid assertion name",
   155  				"An assertion name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.",
   156  				cty.GetAttrPath("equal").Index(k),
   157  			))
   158  		}
   159  		if !obj.GetAttr("description").IsKnown() {
   160  			res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   161  				tfdiags.Error,
   162  				"Invalid description expression",
   163  				"The description must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.",
   164  				cty.GetAttrPath("equal").Index(k).GetAttr("description"),
   165  			))
   166  		}
   167  	}
   168  	for it := config.GetAttr("check").ElementIterator(); it.Next(); {
   169  		k, obj := it.Element()
   170  		if !hclsyntax.ValidIdentifier(k.AsString()) {
   171  			res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   172  				tfdiags.Error,
   173  				"Invalid assertion name",
   174  				"An assertion name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.",
   175  				cty.GetAttrPath("check").Index(k),
   176  			))
   177  		}
   178  		if !obj.GetAttr("description").IsKnown() {
   179  			res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   180  				tfdiags.Error,
   181  				"Invalid description expression",
   182  				"The description must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.",
   183  				cty.GetAttrPath("equal").Index(k).GetAttr("description"),
   184  			))
   185  		}
   186  	}
   187  
   188  	return res
   189  }
   190  
   191  // ReadResource refreshes a resource and returns its current state.
   192  func (p *Provider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse {
   193  	log.Print("[TRACE] moduletest.Provider: ReadResource")
   194  
   195  	var res providers.ReadResourceResponse
   196  	if req.TypeName != "test_assertions" { // we only have one resource type
   197  		res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
   198  		return res
   199  	}
   200  	// Test assertions are not a real remote object, so there isn't actually
   201  	// anything to refresh here.
   202  	res.NewState = req.PriorState
   203  	return res
   204  }
   205  
   206  // UpgradeResourceState is called to allow the provider to adapt the raw value
   207  // stored in the state in case the schema has changed since it was originally
   208  // written.
   209  func (p *Provider) UpgradeResourceState(req providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
   210  	log.Print("[TRACE] moduletest.Provider: UpgradeResourceState")
   211  
   212  	var res providers.UpgradeResourceStateResponse
   213  	if req.TypeName != "test_assertions" { // we only have one resource type
   214  		res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
   215  		return res
   216  	}
   217  
   218  	// We assume here that there can never be a flatmap version of this
   219  	// resource type's data, because this provider was never included in a
   220  	// version of Terraform that used flatmap and this provider's schema
   221  	// contains attributes that are not flatmap-compatible anyway.
   222  	if len(req.RawStateFlatmap) != 0 {
   223  		res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("can't upgrade a flatmap state for %q", req.TypeName))
   224  		return res
   225  	}
   226  	if req.Version != 0 {
   227  		res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("the state for this %s was created by a newer version of the provider", req.TypeName))
   228  		return res
   229  	}
   230  
   231  	v, err := ctyjson.Unmarshal(req.RawStateJSON, testAssertionsSchema.Block.ImpliedType())
   232  	if err != nil {
   233  		res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("failed to decode state for %s: %s", req.TypeName, err))
   234  		return res
   235  	}
   236  
   237  	res.UpgradedState = v
   238  	return res
   239  }
   240  
   241  // PlanResourceChange takes the current state and proposed state of a
   242  // resource, and returns the planned final state.
   243  func (p *Provider) PlanResourceChange(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
   244  	log.Print("[TRACE] moduletest.Provider: PlanResourceChange")
   245  
   246  	var res providers.PlanResourceChangeResponse
   247  	if req.TypeName != "test_assertions" { // we only have one resource type
   248  		res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
   249  		return res
   250  	}
   251  
   252  	// During planning, our job is to gather up all of the planned test
   253  	// assertions marked as pending, which will then allow us to include
   254  	// all of them in test results even if there's a failure during apply
   255  	// that prevents the full completion of the graph walk.
   256  	//
   257  	// In a sense our plan phase is similar to the compile step for a
   258  	// test program written in another language. Planning itself can fail,
   259  	// which means we won't be able to form a complete test plan at all,
   260  	// but if we succeed in planning then subsequent problems can be treated
   261  	// as test failures at "runtime", while still keeping a full manifest
   262  	// of all of the tests that ought to have run if the apply had run to
   263  	// completion.
   264  
   265  	proposed := req.ProposedNewState
   266  	res.PlannedState = proposed
   267  	componentName := proposed.GetAttr("component").AsString() // proven known during validate
   268  	p.mutex.Lock()
   269  	defer p.mutex.Unlock()
   270  	// NOTE: Ideally we'd do something here to verify if two assertions
   271  	// resources in the configuration attempt to declare the same component,
   272  	// but we can't actually do that because Terraform calls PlanResourceChange
   273  	// during both plan and apply, and so the second one would always fail.
   274  	// Since this is just providing a temporary pseudo-syntax for writing tests
   275  	// anyway, we'll live with this for now and aim to solve it with a future
   276  	// iteration of testing that's better integrated into the Terraform
   277  	// language.
   278  	/*
   279  		if _, exists := p.components[componentName]; exists {
   280  			res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   281  				tfdiags.Error,
   282  				"Duplicate test component",
   283  				fmt.Sprintf("Another test_assertions resource already declared assertions for the component name %q.", componentName),
   284  				cty.GetAttrPath("component"),
   285  			))
   286  			return res
   287  		}
   288  	*/
   289  
   290  	component := Component{
   291  		Assertions: make(map[string]*Assertion),
   292  	}
   293  
   294  	for it := proposed.GetAttr("equal").ElementIterator(); it.Next(); {
   295  		k, obj := it.Element()
   296  		name := k.AsString()
   297  		if _, exists := component.Assertions[name]; exists {
   298  			// We can't actually get here in practice because so far we've
   299  			// only been pulling keys from one map, and so any duplicates
   300  			// would've been caught during config decoding, but this is here
   301  			// just to make these two blocks symmetrical to avoid mishaps in
   302  			// future refactoring/reorganization.
   303  			res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   304  				tfdiags.Error,
   305  				"Duplicate test assertion",
   306  				fmt.Sprintf("Another assertion block in this resource already declared an assertion named %q.", name),
   307  				cty.GetAttrPath("equal").Index(k),
   308  			))
   309  			continue
   310  		}
   311  
   312  		var desc string
   313  		descVal := obj.GetAttr("description")
   314  		if descVal.IsNull() {
   315  			descVal = cty.StringVal("")
   316  		}
   317  		err := gocty.FromCtyValue(descVal, &desc)
   318  		if err != nil {
   319  			// We shouldn't get here because we've already validated everything
   320  			// that would make FromCtyValue fail above and during validate.
   321  			res.Diagnostics = res.Diagnostics.Append(err)
   322  		}
   323  
   324  		component.Assertions[name] = &Assertion{
   325  			Outcome:     Pending,
   326  			Description: desc,
   327  		}
   328  	}
   329  
   330  	for it := proposed.GetAttr("check").ElementIterator(); it.Next(); {
   331  		k, obj := it.Element()
   332  		name := k.AsString()
   333  		if _, exists := component.Assertions[name]; exists {
   334  			res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   335  				tfdiags.Error,
   336  				"Duplicate test assertion",
   337  				fmt.Sprintf("Another assertion block in this resource already declared an assertion named %q.", name),
   338  				cty.GetAttrPath("check").Index(k),
   339  			))
   340  			continue
   341  		}
   342  
   343  		var desc string
   344  		descVal := obj.GetAttr("description")
   345  		if descVal.IsNull() {
   346  			descVal = cty.StringVal("")
   347  		}
   348  		err := gocty.FromCtyValue(descVal, &desc)
   349  		if err != nil {
   350  			// We shouldn't get here because we've already validated everything
   351  			// that would make FromCtyValue fail above and during validate.
   352  			res.Diagnostics = res.Diagnostics.Append(err)
   353  		}
   354  
   355  		component.Assertions[name] = &Assertion{
   356  			Outcome:     Pending,
   357  			Description: desc,
   358  		}
   359  	}
   360  
   361  	p.components[componentName] = &component
   362  	return res
   363  }
   364  
   365  // ApplyResourceChange takes the planned state for a resource, which may
   366  // yet contain unknown computed values, and applies the changes returning
   367  // the final state.
   368  func (p *Provider) ApplyResourceChange(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
   369  	log.Print("[TRACE] moduletest.Provider: ApplyResourceChange")
   370  
   371  	var res providers.ApplyResourceChangeResponse
   372  	if req.TypeName != "test_assertions" { // we only have one resource type
   373  		res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
   374  		return res
   375  	}
   376  
   377  	// During apply we actually check the assertions and record the results.
   378  	// An assertion failure isn't reflected as an error from the apply call
   379  	// because if possible we'd like to continue exercising other objects
   380  	// downstream in case that allows us to gather more information to report.
   381  	// (If something downstream returns an error then that could prevent us
   382  	// from completing other assertions, though.)
   383  
   384  	planned := req.PlannedState
   385  	res.NewState = planned
   386  	if res.NewState.IsNull() {
   387  		// If we're destroying then we'll just quickly return success to
   388  		// allow the test process to clean up after itself.
   389  		return res
   390  	}
   391  	componentName := planned.GetAttr("component").AsString() // proven known during validate
   392  
   393  	p.mutex.Lock()
   394  	defer p.mutex.Unlock()
   395  	component := p.components[componentName]
   396  	if component == nil {
   397  		// We might get here when using this provider outside of the
   398  		// "terraform test" command, where there won't be any mechanism to
   399  		// preserve the test provider instance between the plan and apply
   400  		// phases. In that case, we assume that nobody will come looking to
   401  		// collect the results anyway, and so we can just silently skip
   402  		// checking.
   403  		return res
   404  	}
   405  
   406  	for it := planned.GetAttr("equal").ElementIterator(); it.Next(); {
   407  		k, obj := it.Element()
   408  		name := k.AsString()
   409  		var desc string
   410  		if plan, exists := component.Assertions[name]; exists {
   411  			desc = plan.Description
   412  		}
   413  		assert := &Assertion{
   414  			Outcome:     Pending,
   415  			Description: desc,
   416  		}
   417  
   418  		gotVal := obj.GetAttr("got")
   419  		wantVal := obj.GetAttr("want")
   420  		switch {
   421  		case wantVal.RawEquals(gotVal):
   422  			assert.Outcome = Passed
   423  			gotStr := repl.FormatValue(gotVal, 4)
   424  			assert.Message = fmt.Sprintf("correct value\n    got: %s\n", gotStr)
   425  		default:
   426  			assert.Outcome = Failed
   427  			gotStr := repl.FormatValue(gotVal, 4)
   428  			wantStr := repl.FormatValue(wantVal, 4)
   429  			assert.Message = fmt.Sprintf("wrong value\n    got:  %s\n    want: %s\n", gotStr, wantStr)
   430  		}
   431  
   432  		component.Assertions[name] = assert
   433  	}
   434  
   435  	for it := planned.GetAttr("check").ElementIterator(); it.Next(); {
   436  		k, obj := it.Element()
   437  		name := k.AsString()
   438  		var desc string
   439  		if plan, exists := component.Assertions[name]; exists {
   440  			desc = plan.Description
   441  		}
   442  		assert := &Assertion{
   443  			Outcome:     Pending,
   444  			Description: desc,
   445  		}
   446  
   447  		condVal := obj.GetAttr("condition")
   448  		switch {
   449  		case condVal.IsNull():
   450  			res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
   451  				tfdiags.Error,
   452  				"Invalid check condition",
   453  				"The condition value must be a boolean expression, not null.",
   454  				cty.GetAttrPath("check").Index(k).GetAttr("condition"),
   455  			))
   456  			continue
   457  		case condVal.True():
   458  			assert.Outcome = Passed
   459  			assert.Message = "condition passed"
   460  		default:
   461  			assert.Outcome = Failed
   462  			// For "check" we can't really return a decent error message
   463  			// because we've lost all of the context by the time we get here.
   464  			// "equal" will be better for most tests for that reason, and also
   465  			// this is one reason why in the long run it would be better for
   466  			// test assertions to be a first-class language feature rather than
   467  			// just a provider-based concept.
   468  			assert.Message = "condition failed"
   469  		}
   470  
   471  		component.Assertions[name] = assert
   472  	}
   473  
   474  	return res
   475  }
   476  
   477  // ImportResourceState requests that the given resource be imported.
   478  func (p *Provider) ImportResourceState(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
   479  	var res providers.ImportResourceStateResponse
   480  	res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("%s is not importable", req.TypeName))
   481  	return res
   482  }
   483  
   484  // ValidateDataResourceConfig is used to to validate the resource configuration values.
   485  func (p *Provider) ValidateDataResourceConfig(req providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse {
   486  	// This provider has no data resouce types at all.
   487  	var res providers.ValidateDataResourceConfigResponse
   488  	res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported data source %s", req.TypeName))
   489  	return res
   490  }
   491  
   492  // ReadDataSource returns the data source's current state.
   493  func (p *Provider) ReadDataSource(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
   494  	// This provider has no data resouce types at all.
   495  	var res providers.ReadDataSourceResponse
   496  	res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported data source %s", req.TypeName))
   497  	return res
   498  }
   499  
   500  // Stop is called when the provider should halt any in-flight actions.
   501  func (p *Provider) Stop() error {
   502  	// This provider doesn't do anything that can be cancelled.
   503  	return nil
   504  }
   505  
   506  // Close is a noop for this provider, since it's run in-process.
   507  func (p *Provider) Close() error {
   508  	return nil
   509  }
   510  
   511  var testAssertionsSchema = providers.Schema{
   512  	Block: &configschema.Block{
   513  		Attributes: map[string]*configschema.Attribute{
   514  			"component": {
   515  				Type:            cty.String,
   516  				Description:     "The name of the component being tested. This is just for namespacing assertions in a result report.",
   517  				DescriptionKind: configschema.StringPlain,
   518  				Required:        true,
   519  			},
   520  		},
   521  		BlockTypes: map[string]*configschema.NestedBlock{
   522  			"equal": {
   523  				Nesting: configschema.NestingMap,
   524  				Block: configschema.Block{
   525  					Attributes: map[string]*configschema.Attribute{
   526  						"description": {
   527  							Type:            cty.String,
   528  							Description:     "An optional human-readable description of what's being tested by this assertion.",
   529  							DescriptionKind: configschema.StringPlain,
   530  							Required:        true,
   531  						},
   532  						"got": {
   533  							Type:            cty.DynamicPseudoType,
   534  							Description:     "The actual result value generated by the relevant component.",
   535  							DescriptionKind: configschema.StringPlain,
   536  							Required:        true,
   537  						},
   538  						"want": {
   539  							Type:            cty.DynamicPseudoType,
   540  							Description:     "The value that the component is expected to have generated.",
   541  							DescriptionKind: configschema.StringPlain,
   542  							Required:        true,
   543  						},
   544  					},
   545  				},
   546  			},
   547  			"check": {
   548  				Nesting: configschema.NestingMap,
   549  				Block: configschema.Block{
   550  					Attributes: map[string]*configschema.Attribute{
   551  						"description": {
   552  							Type:            cty.String,
   553  							Description:     "An optional (but strongly recommended) human-readable description of what's being tested by this assertion.",
   554  							DescriptionKind: configschema.StringPlain,
   555  							Required:        true,
   556  						},
   557  						"condition": {
   558  							Type:            cty.Bool,
   559  							Description:     "An expression that must be true in order for the test to pass.",
   560  							DescriptionKind: configschema.StringPlain,
   561  							Required:        true,
   562  						},
   563  					},
   564  				},
   565  			},
   566  		},
   567  	},
   568  }