Validation Practice & Exercises
Practice Overview
Apply your validation and error handling knowledge through hands-on exercises.
Exercise 1: Custom Email Validator
Task
Create a custom email validator with domain whitelist support.
Requirements:
- Validate email format
- Optional domain whitelist
- Clear error messages
Starter Code
type emailValidator struct {
allowedDomains []string
}
// TODO: Implement ValidateString
// TODO: Add Description methods
Solution
Click to reveal solution
```go type emailValidator struct { allowedDomains []string } func (v emailValidator) Description(ctx context.Context) string { if len(v.allowedDomains) > 0 { return fmt.Sprintf("value must be a valid email from domains: %v", v.allowedDomains) } return "value must be a valid email address" } func (v emailValidator) MarkdownDescription(ctx context.Context) string { return v.Description(ctx) } func (v emailValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() { return } value := req.ConfigValue.ValueString() // Basic email validation emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) if !emailRegex.MatchString(value) { resp.Diagnostics.AddAttributeError( req.Path, "Invalid Email Format", fmt.Sprintf("'%s' is not a valid email address. Format: user@example.com", value), ) return } // Check domain whitelist if provided if len(v.allowedDomains) > 0 { parts := strings.Split(value, "@") if len(parts) != 2 { return } domain := parts[1] validDomain := false for _, allowed := range v.allowedDomains { if domain == allowed { validDomain = true break } } if !validDomain { resp.Diagnostics.AddAttributeError( req.Path, "Invalid Email Domain", fmt.Sprintf( "Email domain '%s' is not allowed. Allowed domains: %v", domain, v.allowedDomains, ), ) } } } func Email(allowedDomains ...string) validator.String { return emailValidator{allowedDomains: allowedDomains} } // Usage "admin_email": schema.StringAttribute{ Required: true, Validators: []validator.String{ Email("example.com", "company.com"), }, }, ```Exercise 2: Cross-Attribute Validation
Task
Implement validation that ensures TLS is enabled when public access is enabled.
Requirements:
- Check if public_access is true
- Require tls_enabled to be true when public_access is true
- Provide clear error message
Starter Code
func (r *CacheResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var data CacheModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
// TODO: Implement validation logic
}
Solution
Click to reveal solution
```go func (r *CacheResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var data CacheModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } // Check if public access is enabled without TLS hasPublicAccess := !data.PublicAccess.IsNull() && data.PublicAccess.ValueBool() hasTLS := !data.TLSEnabled.IsNull() && data.TLSEnabled.ValueBool() if hasPublicAccess && !hasTLS { resp.Diagnostics.AddError( "Insecure Configuration", "TLS must be enabled when public access is enabled. "+ "Set 'tls_enabled = true' to encrypt data in transit. "+ "Learn more: https://docs.example.com/security/tls", ) } // Additional check: warn if TLS is disabled for any deployment if !data.TLSEnabled.IsNull() && !data.TLSEnabled.ValueBool() { resp.Diagnostics.AddWarning( "Security Recommendation", "TLS is not enabled. Consider enabling TLS to protect data in transit. "+ "Set 'tls_enabled = true'.", ) } } ```Exercise 3: API Error Translation
Task
Create a helper function that translates API errors into user-friendly diagnostic messages.
Requirements:
- Handle different error types (not found, rate limit, validation)
- Return appropriate diagnostics
- Include helpful context
Starter Code
func translateAPIError(err error, operation string) diag.Diagnostics {
var diags diag.Diagnostics
// TODO: Implement error translation
return diags
}
Solution
Click to reveal solution
```go func translateAPIError(err error, operation, resourceID string) diag.Diagnostics { var diags diag.Diagnostics // Not found error var notFoundErr *NotFoundError if errors.As(err, ¬FoundErr) { diags.AddError( fmt.Sprintf("%s Failed: Resource Not Found", operation), fmt.Sprintf( "The cache '%s' no longer exists. It may have been deleted outside of Terraform. "+ "Remove this resource from your configuration or run 'terraform apply' to recreate it.", resourceID, ), ) return diags } // Rate limit error var rateLimitErr *RateLimitError if errors.As(err, &rateLimitErr) { resetIn := time.Until(rateLimitErr.ResetAt).Round(time.Second) diags.AddError( "Rate Limit Exceeded", fmt.Sprintf( "API rate limit exceeded during %s operation. "+ "Current usage: %d/%d requests. "+ "Rate limit resets in %s (at %s). "+ "Terraform will automatically retry. If this persists, contact support.", operation, rateLimitErr.Limit-rateLimitErr.Remaining, rateLimitErr.Limit, resetIn, rateLimitErr.ResetAt.Format("15:04:05 MST"), ), ) return diags } // Validation error var validationErr *ValidationError if errors.As(err, &validationErr) { diags.AddAttributeError( path.Root(validationErr.Field), "Configuration Rejected by API", fmt.Sprintf( "The API rejected the value for '%s': %s. "+ "Update the configuration and try again.", validationErr.Field, validationErr.Reason, ), ) return diags } // Timeout error if errors.Is(err, context.DeadlineExceeded) { diags.AddError( fmt.Sprintf("%s Operation Timed Out", operation), fmt.Sprintf( "The %s operation for cache '%s' timed out. "+ "This may be due to network issues or API slowness. "+ "The operation may still complete in the background. "+ "Wait a few minutes and check the cache status, or try again.", operation, resourceID, ), ) return diags } // Generic API error var apiErr *APIError if errors.As(err, &apiErr) { diags.AddError( fmt.Sprintf("%s Failed", operation), fmt.Sprintf( "API request failed with status %d: %s. "+ "Request ID: %s. "+ "If this error persists, contact support with the request ID.", apiErr.StatusCode, apiErr.Message, apiErr.RequestID, ), ) return diags } // Unknown error diags.AddError( fmt.Sprintf("%s Failed", operation), fmt.Sprintf( "An unexpected error occurred during %s: %s. "+ "Check your network connectivity and API credentials. "+ "If the problem persists, contact support.", operation, err.Error(), ), ) return diags } // Usage in resource methods func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data CacheModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) cache, err := r.client.CreateCache(ctx, input) if err != nil { resp.Diagnostics.Append(translateAPIError(err, "Creation", data.Name.ValueString())...) return } // Success } ```Exercise 4: Retry Logic
Task
Implement retry logic with exponential backoff for API calls.
Requirements:
- Retry only retryable errors
- Use exponential backoff
- Add jitter
- Respect context timeout
Solution
Click to reveal solution
```go type RetryConfig struct { MaxRetries int InitialBackoff time.Duration MaxBackoff time.Duration BackoffFactor float64 } func retryWithBackoff(ctx context.Context, config RetryConfig, fn func() error) error { backoff := config.InitialBackoff for attempt := 0; attempt <= config.MaxRetries; attempt++ { err := fn() // Success if err == nil { return nil } // Non-retryable error if !IsRetryableError(err) { return err } // Last attempt if attempt == config.MaxRetries { return fmt.Errorf("max retries (%d) exceeded: %w", config.MaxRetries, err) } // Calculate backoff with jitter (±30%) jitter := time.Duration(rand.Float64() * float64(backoff) * 0.6) - time.Duration(float64(backoff)*0.3) sleepDuration := backoff + jitter // Ensure non-negative sleep if sleepDuration < 0 { sleepDuration = backoff } // Sleep with context awareness select { case <-time.After(sleepDuration): // Continue to next attempt case <-ctx.Done(): return ctx.Err() } // Increase backoff for next attempt backoff = time.Duration(float64(backoff) * config.BackoffFactor) if backoff > config.MaxBackoff { backoff = config.MaxBackoff } } return fmt.Errorf("unexpected retry loop exit") } // Usage func (c *Client) CreateCache(ctx context.Context, input CreateCacheInput) (*Cache, error) { var cache *Cache var lastErr error config := RetryConfig{ MaxRetries: 3, InitialBackoff: 1 * time.Second, MaxBackoff: 30 * time.Second, BackoffFactor: 2.0, } err := retryWithBackoff(ctx, config, func() error { var err error cache, err = c.createCacheInternal(ctx, input) lastErr = err return err }) if err != nil { return nil, err } return cache, nil } ```Challenge Problems
Challenge 1: Complex Validation
Create validation for a cache configuration that requires:
- Backup must be enabled for production environments
- Multi-AZ requires at least 2 availability zones
- Encryption at rest required for compliance mode
- TLS required for public access
Challenge 2: Error Recovery
Implement error recovery for a scenario where:
- Resource creation succeeds but becomes unavailable
- Need to save partial state
- Provide clear guidance to user
- Handle cleanup if needed
Challenge 3: Rate Limit Strategy
Implement smart rate limiting that:
- Tracks API rate limit headers
- Proactively throttles requests
- Queues requests when approaching limits
- Provides feedback to users
Quiz Questions
Question 1
When should you use AddError vs AddWarning?
Answer
**Use AddError when:** - Configuration is invalid and cannot proceed - Required attributes are missing - API returns unrecoverable errors - Operation must be blocked **Use AddWarning when:** - Configuration is valid but suboptimal - Features are deprecated - Best practices are not followed - Non-blocking recommendations Example: ```go // Error: Blocks operation resp.Diagnostics.AddError( "Missing Required Attribute", "The 'engine' attribute is required", ) // Warning: Allows operation resp.Diagnostics.AddWarning( "Deprecated Attribute", "The 'legacy_mode' attribute is deprecated", ) ```Question 2
What’s the difference between schema validation and plan-time validation?
Answer
**Schema Validation:** - Validates individual attributes - Runs during plan phase - Cannot access other attributes - Simple rules (format, range, enum) - Examples: string length, number range, regex **Plan-Time Validation (ValidateConfig):** - Validates across multiple attributes - Runs during plan phase - Can access all configuration - Complex rules (cross-attribute, business logic) - Examples: conflicting settings, dependent attributes ```go // Schema validation: Single attribute "port": schema.Int64Attribute{ Validators: []validator.Int64{ int64validator.Between(1024, 65535), }, } // Plan-time validation: Multiple attributes func (r *Resource) ValidateConfig(...) { if data.PublicAccess.ValueBool() && !data.TLS.ValueBool() { // Error: TLS required for public access } } ```Question 3
How should you handle retryable vs non-retryable errors?
Answer
**Retryable Errors:** - Network timeouts - Rate limiting (429) - Server errors (5xx) - Temporary failures **Non-Retryable Errors:** - Client errors (4xx except 429) - Authentication failures (401, 403) - Not found (404) - Validation errors (400) ```go func IsRetryableError(err error) bool { // Network errors var netErr net.Error if errors.As(err, &netErr) && netErr.Temporary() { return true } // Rate limits if IsRateLimitError(err) { return true } // Server errors var apiErr *APIError if errors.As(err, &apiErr) { return apiErr.StatusCode >= 500 } return false } // Implementation if IsRetryableError(err) { return retryWithBackoff(ctx, fn) } else { return err // Don't retry } ```Best Practices Checklist
Ensure your validation and error handling follows these practices:
- ✅ Validate at the earliest possible stage
- ✅ Use schema validators for simple rules
- ✅ Use ValidateConfig for complex rules
- ✅ Provide clear, actionable error messages
- ✅ Include examples in validation messages
- ✅ Point errors to specific attributes
- ✅ Use warnings for non-blocking issues
- ✅ Classify errors (retryable, permanent, transient)
- ✅ Implement exponential backoff for retries
- ✅ Respect rate limits and Retry-After headers
- ✅ Include request IDs in error messages
- ✅ Handle timeouts gracefully
- ✅ Translate API errors to user-friendly messages
- ✅ Test validation with invalid inputs
- ✅ Test error handling paths
Additional Resources
Documentation
Example Providers
Study validation patterns in:
Key Takeaways
- ✅ Practice implementing custom validators
- ✅ Master cross-attribute validation
- ✅ Write user-friendly error messages
- ✅ Handle API errors gracefully
- ✅ Implement proper retry logic
- ✅ Classify errors appropriately
- ✅ Test validation thoroughly
- ✅ Use warnings for recommendations
- ✅ Provide actionable guidance
- ✅ Follow validation best practices
What’s Next?
Continue to Module 12 to learn about documentation generation for your provider.
Next Module: Documentation Generation →
Previous: ← API Error Handling
Last updated: June 30, 2026