Configuration Validation

8 minute read

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

Updated: