Configuration Validation
Why Validate Provider Configuration?
Provider configuration validation ensures that:
- ✅ Required credentials are provided
- ✅ Configuration values are valid
- ✅ Authentication methods don’t conflict
- ✅ Connections can be established
- ✅ Users get clear error messages early
Validate during terraform plan, not during resource operations!
Validation Types
1. Schema-Level Validation
Built into the schema using validators.
2. Configure-Time Validation
Custom logic in the Configure method.
3. Connection Testing
Verify connectivity to the API.
Schema-Level Validation
Value Constraints
func (p *MyCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"timeout": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Request timeout in seconds (1-300)",
Validators: []validator.Number{
numbervalidator.Between(1, 300),
},
},
"max_retries": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Maximum retry attempts (0-10)",
Validators: []validator.Number{
numbervalidator.Between(0, 10),
},
},
"region": schema.StringAttribute{
Optional: true,
MarkdownDescription: "AWS region",
Validators: []validator.String{
stringvalidator.OneOf(
"us-east-1",
"us-west-2",
"eu-west-1",
"ap-southeast-1",
),
},
},
"api_key": schema.StringAttribute{
Optional: true,
Sensitive: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.RegexMatches(
regexp.MustCompile(`^sk-[a-zA-Z0-9]{32}$`),
"API key must start with 'sk-' followed by 32 alphanumeric characters",
),
},
},
},
}
}
Configure-Time Validation
Required Configuration
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Validate API key
apiKey := config.APIKey.ValueString()
if apiKey == "" {
apiKey = os.Getenv("MYCLOUD_API_KEY")
}
if apiKey == "" {
resp.Diagnostics.AddError(
"Missing API Key",
"API key must be provided via one of:\n"+
" - api_key attribute in provider block\n"+
" - MYCLOUD_API_KEY environment variable\n"+
" - ~/.mycloud/config.json configuration file",
)
return
}
// Continue with client setup...
}
Mutually Exclusive Authentication
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
req.Config.Get(ctx, &config)
// Check which authentication methods are provided
hasAPIKey := !config.APIKey.IsNull() && config.APIKey.ValueString() != ""
hasUsername := !config.Username.IsNull() && config.Username.ValueString() != ""
hasPassword := !config.Password.IsNull() && config.Password.ValueString() != ""
hasToken := !config.Token.IsNull() && config.Token.ValueString() != ""
hasUserPass := hasUsername && hasPassword
// Count authentication methods
authMethodCount := 0
if hasAPIKey {
authMethodCount++
}
if hasUserPass {
authMethodCount++
}
if hasToken {
authMethodCount++
}
// Validate exactly one method
if authMethodCount == 0 {
resp.Diagnostics.AddError(
"Missing Authentication",
"One of the following authentication methods must be provided:\n"+
" - api_key\n"+
" - username and password\n"+
" - token",
)
return
}
if authMethodCount > 1 {
resp.Diagnostics.AddError(
"Multiple Authentication Methods",
"Only one authentication method can be specified at a time.\n"+
"Please provide only one of: api_key, username/password, or token.",
)
return
}
// Validate username/password together
if hasUsername != hasPassword {
resp.Diagnostics.AddError(
"Incomplete Authentication",
"Both username and password must be provided together.",
)
return
}
// Continue with client setup...
}
Dependent Configuration
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
req.Config.Get(ctx, &config)
// If client certificate is provided, key must also be provided
hasClientCert := !config.ClientCert.IsNull() && config.ClientCert.ValueString() != ""
hasClientKey := !config.ClientKey.IsNull() && config.ClientKey.ValueString() != ""
if hasClientCert && !hasClientKey {
resp.Diagnostics.AddError(
"Missing Client Key",
"client_key must be provided when client_cert is specified",
)
return
}
if hasClientKey && !hasClientCert {
resp.Diagnostics.AddError(
"Missing Client Certificate",
"client_cert must be provided when client_key is specified",
)
return
}
// If TLS is disabled, warn user
if !config.InsecureSkipVerify.IsNull() && config.InsecureSkipVerify.ValueBool() {
resp.Diagnostics.AddWarning(
"TLS Verification Disabled",
"TLS certificate verification is disabled. This is not recommended for production use.",
)
}
// Continue with client setup...
}
File Path Validation
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
req.Config.Get(ctx, &config)
// Validate certificate file exists
if !config.ClientCert.IsNull() {
certPath := config.ClientCert.ValueString()
// Expand home directory
if strings.HasPrefix(certPath, "~/") {
homeDir, err := os.UserHomeDir()
if err != nil {
resp.Diagnostics.AddError(
"Failed to Expand Home Directory",
err.Error(),
)
return
}
certPath = filepath.Join(homeDir, certPath[2:])
}
// Check if file exists
if _, err := os.Stat(certPath); os.IsNotExist(err) {
resp.Diagnostics.AddError(
"Certificate File Not Found",
fmt.Sprintf("Client certificate file does not exist: %s", certPath),
)
return
}
// Try to load certificate to validate format
if _, err := tls.LoadX509KeyPair(certPath, config.ClientKey.ValueString()); err != nil {
resp.Diagnostics.AddError(
"Invalid Certificate",
fmt.Sprintf("Could not load client certificate: %s", err.Error()),
)
return
}
}
// Continue with client setup...
}
Connection Testing
Test Connection During Configure
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
req.Config.Get(ctx, &config)
// ... configuration and validation ...
// Create client
client, err := api.NewClient(api.Config{
APIKey: apiKey,
Endpoint: endpoint,
Timeout: timeout,
})
if err != nil {
resp.Diagnostics.AddError(
"Failed to Create Client",
fmt.Sprintf("Could not create API client: %s", err.Error()),
)
return
}
// Test connection
tflog.Debug(ctx, "Testing API connection")
if err := client.TestConnection(ctx); err != nil {
resp.Diagnostics.AddError(
"Connection Test Failed",
fmt.Sprintf(
"Could not connect to API at %s.\n"+
"Error: %s\n\n"+
"Please verify:\n"+
" - API key is valid\n"+
" - Endpoint is correct\n"+
" - Network connectivity",
endpoint,
err.Error(),
),
)
return
}
tflog.Info(ctx, "Successfully connected to API")
// Share client
resp.ResourceData = client
resp.DataSourceData = client
}
API Client Test Method
// In your API client package
func (c *Client) TestConnection(ctx context.Context) error {
// Make a simple API call to verify connectivity and authentication
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/v1/health", nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("connection failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fmt.Errorf("authentication failed (HTTP %d)", resp.StatusCode)
}
if resp.StatusCode >= 400 {
return fmt.Errorf("API returned error (HTTP %d)", resp.StatusCode)
}
return nil
}
Validation Error Messages
Clear and Actionable
// ✅ Good: Specific and actionable
resp.Diagnostics.AddError(
"Missing API Key",
"API key must be provided via one of:\n"+
" - api_key attribute in provider block\n"+
" - MYCLOUD_API_KEY environment variable\n"+
" - ~/.mycloud/config.json configuration file\n\n"+
"Example:\n"+
" provider \"mycloud\" {\n"+
" api_key = \"sk-abc123...\"\n"+
" }",
)
// ❌ Bad: Vague
resp.Diagnostics.AddError("Error", "Missing authentication")
Include Context
// ✅ Good: Includes what was attempted
resp.Diagnostics.AddError(
"Connection Test Failed",
fmt.Sprintf(
"Could not connect to API at %s.\n"+
"Error: %s\n\n"+
"Troubleshooting:\n"+
" 1. Verify API key is valid\n"+
" 2. Check endpoint URL is correct\n"+
" 3. Ensure network connectivity\n"+
" 4. Check firewall rules",
endpoint,
err.Error(),
),
)
Warnings for Insecure Config
// ✅ Good: Warn about security issues
if insecureSkipVerify {
resp.Diagnostics.AddWarning(
"TLS Verification Disabled",
"TLS certificate verification is disabled. This is not recommended for production use.\n"+
"Set insecure_skip_verify = false (default) to enable TLS verification.",
)
}
Validation Patterns
Pattern 1: At Least One Required
// At least one of multiple fields must be provided
if id == "" && name == "" {
resp.Diagnostics.AddError(
"Missing Required Configuration",
"Either 'id' or 'name' must be provided",
)
return
}
Pattern 2: Exactly One Required
// Exactly one of multiple fields must be provided
provided := 0
if apiKey != "" {
provided++
}
if token != "" {
provided++
}
if username != "" && password != "" {
provided++
}
if provided == 0 {
resp.Diagnostics.AddError("Missing Authentication", ...)
}
if provided > 1 {
resp.Diagnostics.AddError("Multiple Authentication Methods", ...)
}
Pattern 3: Conditional Requirements
// If A is provided, B must also be provided
if enableFeature && featureConfig == "" {
resp.Diagnostics.AddError(
"Missing Configuration",
"feature_config must be provided when enable_feature is true",
)
}
Pattern 4: Range Validation
// Validate numeric ranges
if timeout < 1 || timeout > 300 {
resp.Diagnostics.AddError(
"Invalid Timeout",
fmt.Sprintf("Timeout must be between 1 and 300 seconds, got %d", timeout),
)
}
Pattern 5: Format Validation
// Validate string format
if !regexp.MustCompile(`^https?://`).MatchString(endpoint) {
resp.Diagnostics.AddError(
"Invalid Endpoint",
fmt.Sprintf("Endpoint must be a valid HTTP or HTTPS URL, got: %s", endpoint),
)
}
Complete Validation Example
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config MyCloudProviderModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// 1. Resolve configuration with precedence
apiKey := resolveString(config.APIKey, "MYCLOUD_API_KEY", "")
endpoint := resolveString(config.Endpoint, "MYCLOUD_ENDPOINT", "https://api.mycloud.com")
region := resolveString(config.Region, "MYCLOUD_REGION", "us-east-1")
// 2. Validate required fields
if apiKey == "" {
resp.Diagnostics.AddError(
"Missing API Key",
"API key must be provided via:\n"+
" - api_key attribute\n"+
" - MYCLOUD_API_KEY environment variable",
)
return
}
// 3. Validate formats
if !regexp.MustCompile(`^sk-[a-zA-Z0-9]{32}$`).MatchString(apiKey) {
resp.Diagnostics.AddError(
"Invalid API Key Format",
"API key must start with 'sk-' followed by 32 alphanumeric characters",
)
return
}
if !regexp.MustCompile(`^https?://`).MatchString(endpoint) {
resp.Diagnostics.AddError(
"Invalid Endpoint",
fmt.Sprintf("Endpoint must be a valid HTTP or HTTPS URL, got: %s", endpoint),
)
return
}
// 4. Validate regional availability
validRegions := []string{"us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"}
if !contains(validRegions, region) {
resp.Diagnostics.AddError(
"Invalid Region",
fmt.Sprintf("Region must be one of: %s. Got: %s", strings.Join(validRegions, ", "), region),
)
return
}
// 5. Create and test client
client, err := api.NewClient(api.Config{
APIKey: apiKey,
BaseURL: endpoint,
Region: region,
})
if err != nil {
resp.Diagnostics.AddError(
"Failed to Create Client",
fmt.Sprintf("Could not create API client: %s", err.Error()),
)
return
}
// 6. Test connection
if err := client.TestConnection(ctx); err != nil {
resp.Diagnostics.AddError(
"Connection Test Failed",
fmt.Sprintf(
"Could not connect to API.\n"+
"Endpoint: %s\n"+
"Region: %s\n"+
"Error: %s",
endpoint, region, err.Error(),
),
)
return
}
tflog.Info(ctx, "Provider configured successfully", map[string]any{
"endpoint": endpoint,
"region": region,
})
// 7. Share client
resp.ResourceData = client
resp.DataSourceData = client
}
Best Practices
1. Validate Early
// ✅ Validate in Configure, not in resources
func (p *MyCloudProvider) Configure(...) {
if apiKey == "" {
resp.Diagnostics.AddError("Missing API Key", ...)
return
}
}
2. Provide Multiple Solutions
// ✅ Show all ways to provide config
"API key must be provided via:\n"+
" - api_key attribute\n"+
" - MYCLOUD_API_KEY environment variable\n"+
" - ~/.mycloud/config.json file"
3. Include Examples
// ✅ Include usage example
"Example:\n"+
" provider \"mycloud\" {\n"+
" api_key = \"sk-abc123...\"\n"+
" }"
4. Test Connections
// ✅ Test connectivity during configure
if err := client.TestConnection(ctx); err != nil {
resp.Diagnostics.AddError("Connection Failed", ...)
}
5. Log Validation Steps
// ✅ Log for debugging
tflog.Debug(ctx, "Validating provider configuration")
tflog.Debug(ctx, "Testing API connection", map[string]any{
"endpoint": endpoint,
})
Key Takeaways
- ✅ Validate configuration in provider’s Configure method
- ✅ Use schema validators for simple constraints
- ✅ Check for required configuration
- ✅ Validate mutually exclusive options
- ✅ Test API connectivity during configuration
- ✅ Provide clear, actionable error messages
- ✅ Include multiple solutions in error messages
- ✅ Warn about insecure configurations
- ✅ Log validation steps for debugging
- ✅ Validate formats and ranges
What’s Next?
Now let’s apply everything in a hands-on lab!
Next: Provider Configuration Lab →
Previous: ← Shared Client Pattern
Last updated: June 30, 2026