Skip to content

Conversation

@remyleone
Copy link
Member

No description provided.

@codecov-commenter
Copy link

codecov-commenter commented Nov 6, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 1.76%. Comparing base (33203bf) to head (d6a557e).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@          Coverage Diff           @@
##           master   #3453   +/-   ##
======================================
  Coverage    1.76%   1.76%           
======================================
  Files         400     400           
  Lines       43836   43836           
======================================
  Hits          774     774           
  Misses      42975   42975           
  Partials       87      87           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.


3. **Cassette Organization**
- Store cassettes in `internal/services/{service}/testdata/`
- Use descriptive names (e.g., `{resource}-basic.cassette.yaml`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a remark and doesn't require any change, but I believe that, in every case, cassettes' names and paths are automatically generated from the name of the test (cf here). I have kept this naming logic in the new vcr (cf here)

1. **Run linting on changed files only**
```bash
# Run on specific files/directories
golangci-lint run internal/services/account/...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be add

Lint and auto-fix a specific package or directory

golangci-lint run --fix ./internal/services/account/...

## Adding New Resources

When adding a new resource to the provider, follow this checklist to ensure consistency and proper implementation:

Copy link
Contributor

@jremy42 jremy42 Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Critical Pre-Implementation Analysis
**IMPORTANT**: Before writing any code for a new resource, you MUST analyze and determine the following aspects. If the information is not provided by the user, ask clarifying questions or search the codebase for similar resources.
#### 1. Determine Resource Locality
**Action**: Identify if this is a regional, zonal, or global resource.
**How to determine**:
- Check the API SDK: `go doc github.com/scaleway/scaleway-sdk-go/api/{service}/v1`
- Look for `Region` or `Zone` fields in the API requests
- Search for similar resources in `internal/services/` to identify patterns
**Impact on implementation**:
- **Regional**: Use `regional.ParseID()`, `regional.NewIDString()`, `meta.ExtractRegion()`
- **Zonal**: Use `zonal.ParseID()`, `zonal.NewIDString()`, `meta.ExtractZone()`
- **Global**: No locality handling needed, simple ID format
#### 2. Identify Parent-Child Relationships
**Action**: Determine if this resource has a parent resource or child resources.
**Indicators of child resources**:
- API endpoints like `/deployments/{deployment_id}/databases`
- Schema fields referencing other resource IDs (e.g., `deployment_id`, `instance_id`)
- Resources that cannot exist independently
**If this IS a child resource, you MUST**:
- Create custom ID constructor: `Resource{Name}ID(region, parentID, childID) string`
- Create custom ID parser: `Resource{Name}ParseID(id) (region, parentID, childID, error)`
- Add `DiffSuppressFunc: dsf.Locality` to the parent ID field in schema
- Use `locality.ExpandID()` when extracting parent ID in Create operation
- Consider adding `CustomizeDiff: cdf.LocalityCheck("parent_id")`
#### 3. Plan Required Helper Functions
**Action**: Based on locality type, determine which helpers must be created in `helpers.go`.
**For regional/zonal resources, you MUST create**:
1. `NewAPI(m any) *{service}api.API` - Basic client
2. `{service}APIWithRegion(d, m) (*{service}api.API, scw.Region, error)` - For CREATE operations
3. `NewAPIWithRegionAndID(m, id) (*{service}api.API, scw.Region, string, error)` - For READ/UPDATE/DELETE
**For global resources**:
- Only `NewAPI(m any)` is needed
#### 4. Define ID Format Strategy
**Action**: Determine the exact ID format this resource will use.
**Formats**:
- **Simple regional**: `region/resource-id` (e.g., `fr-par/abc-123`)
- **Simple zonal**: `zone/resource-id` (e.g., `fr-par-1/abc-123`)
- **Child resource**: `region/parent-id/child-identifier` (e.g., `fr-par/deployment-id/database-name`)
- **Global**: `resource-id` (no locality prefix)
**Implementation impact**:
- Determines which parsing functions to use
- Affects test helper implementation
- Impacts import statements
#### 5. Verify SDK Type Signatures
**Action**: Before implementing CRUD operations, run this command to see exact API types:
````bash
go doc github.com/scaleway/scaleway-sdk-go/api/{service}/v1.CreateResourceRequest
go doc github.com/scaleway/scaleway-sdk-go/api/{service}/v1.UpdateResourceRequest

When adding a new resource to the provider, follow this checklist to ensure consistency and proper implementation:

### TODO List for Adding a New Resource

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. **Create helper functions in `helpers.go`**
- Implement the three required helpers (see Helper Functions section below)
- Add any service-specific helper functions
- Define timeout constants

- Execute `make lint` to ensure the code passes all linter checks
- Execute `make testacc` to verify acceptance tests pass
- Execute `make format` to ensure proper code formatting

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Compress cassettes with `go run cmd/vcr-compressor/main.go path/to/cassette`


When adding a new resource to the provider, follow this checklist to ensure consistency and proper implementation:

### TODO List for Adding a New Resource
Copy link
Contributor

@jremy42 jremy42 Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### TODO List for Adding a New Resource
**Check for**:
- `string` vs `*string` fields → determines if you use `.()string` or `types.ExpandStringPtr()`
- `[]string` fields → use `types.ExpandStrings()`
- `map[string]string` fields → use `types.ExpandMapPtrStringString()`
- Optional vs required fields → use `GetOk()` for optional
**Never guess types** - always verify with `go doc` to avoid compilation errors.
#### 6. Identify Sensitive Fields
**Action**: Scan the API schema for fields containing sensitive data.
**Sensitive field indicators**:
- Field names: `password`, `secret`, `token`, `key`, `certificate`, `private_key`, `api_key`
- Fields marked as sensitive in API documentation
**For ANY sensitive field, you MUST**:
- Add `Sensitive: true` to the schema field definition
- Document it as sensitive in the description
- Use dummy values in tests (never real credentials)
- Ensure error messages don't leak sensitive values
#### 7. Check for Parent References
**Action**: If implementing a child resource, identify the parent reference field.
**When a parent_id field exists, you MUST**:
1. Add to schema:
"parent_id": {
DiffSuppressFunc: dsf.Locality, // CRITICAL!
}
2. In Create function:
parentID := locality.ExpandID(d.Get("parent_id").(string)) // CRITICAL!
3. Add import: `"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality"`
**Why**: Parent IDs can be specified as `"id"` or `"region/id"` in Terraform configs. Without these, you get 404 errors with duplicated regions in URLs.
---
**Decision Point**: Once you have analyzed all these aspects, you can proceed with implementation. If any aspect is unclear, ask the user or search for similar resources in the codebase before proceeding.
### TODO List for Adding a New Resource

- Execute `make testacc` to verify acceptance tests pass
- Execute `make format` to ensure proper code formatting

## Testing Guidelines
Copy link
Contributor

@jremy42 jremy42 Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have left some doc on the helpers and child resource , I’ve often had issues with that part.

Suggested change
## Testing Guidelines
## Helper Functions - Critical Patterns
Every service MUST implement helper functions in a `helpers.go` file. These helpers are essential for proper region/zone handling and ID parsing.
### The Three Required Helpers (Regional/Zonal Resources)
For regional or zonal resources, you MUST implement these three helper functions:
#### 1. NewAPI(m any) - Basic API Client
Used in tests and when the region/zone is already known.
func NewAPI(m any) *myserviceapi.API {
return myserviceapi.NewAPI(meta.ExtractScwClient(m))
}
#### 2. xxxAPIWithRegion(d, m) - API Client + Region Extraction
Used in **CREATE** operations. Handles region fallback: resource config → provider config → error.
**⚠️ CRITICAL**: Always use `meta.ExtractRegion(d, m)`, NEVER `scw.Region(d.Get("region").(string))` which will panic if region is nil.
// For regional resources
func myserviceAPIWithRegion(d *schema.ResourceData, m any) (*myserviceapi.API, scw.Region, error) {
api := myserviceapi.NewAPI(meta.ExtractScwClient(m))
region, err := meta.ExtractRegion(d, m) // ✅ Handles fallback
if err != nil {
return nil, "", err
}
return api, region, nil
}
// For zonal resources
func myserviceAPIWithZone(d *schema.ResourceData, m any) (*myserviceapi.API, scw.Zone, error) {
api := myserviceapi.NewAPI(meta.ExtractScwClient(m))
zone, err := meta.ExtractZone(d, m) // ✅ Handles fallback
if err != nil {
return nil, "", err
}
return api, zone, nil
}
#### 3. NewAPIWithRegionAndID(m, id) - Parse Composite ID
Used in **READ, UPDATE, DELETE** operations. Parses the composite ID from state.
// For regional resources
func NewAPIWithRegionAndID(m any, id string) (*myserviceapi.API, scw.Region, string, error) {
api := myserviceapi.NewAPI(meta.ExtractScwClient(m))
region, id, err := regional.ParseID(id) // Parses "fr-par/abc-123"
if err != nil {
return nil, "", "", err
}
return api, region, id, nil
}
// For zonal resources
func NewAPIWithZoneAndID(m any, id string) (*myserviceapi.API, scw.Zone, string, error) {
api := myserviceapi.NewAPI(meta.ExtractScwClient(m))
zone, id, err := zonal.ParseID(id) // Parses "fr-par-1/abc-123"
if err != nil {
return nil, "", "", err
}
return api, zone, id, nil
}
### When to Use Which Helper
| Operation | Helper Function | Reason |
|-----------|----------------|--------|
| **CREATE** | `xxxAPIWithRegion(d, m)` | Extracts region from resource config or provider default |
| **READ** | `NewAPIWithRegionAndID(m, d.Id())` | Parses composite ID from state |
| **UPDATE** | `NewAPIWithRegionAndID(m, d.Id())` | Parses composite ID from state |
| **DELETE** | `NewAPIWithRegionAndID(m, d.Id())` | Parses composite ID from state |
| **TESTS** | `NewAPI(m)` | Simpler when region is already known |
### Example Usage in CRUD Operations
// CREATE - No ID yet, need to extract region
func resourceMyResourceCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
api, region, err := myserviceAPIWithRegion(d, m) // ✅ Helper 1
if err != nil {
return diag.FromErr(err)
}
resource, err := api.CreateResource(&myserviceapi.CreateResourceRequest{
Region: region,
Name: d.Get("name").(string),
}, scw.WithContext(ctx))
// Set ID with locality
d.SetId(regional.NewIDString(region, resource.ID)) // "fr-par/abc-123"
return resourceMyResourceRead(ctx, d, m)
}
// READ - Have composite ID in state
func resourceMyResourceRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
api, region, id, err := NewAPIWithRegionAndID(m, d.Id()) // ✅ Helper 2
if err != nil {
return diag.FromErr(err)
}
resource, err := api.GetResource(&myserviceapi.GetResourceRequest{
Region: region, // "fr-par"
ResourceID: id, // "abc-123"
}, scw.WithContext(ctx))
if err != nil {
if httperrors.Is404(err) {
d.SetId("")
return nil
}
return diag.FromErr(err)
}
// ⚠️ CRITICAL: Set region from API response, not local variable
_ = d.Set("region", string(resource.Region)) // ✅ API response
_ = d.Set("name", resource.Name)
return nil
}
// UPDATE and DELETE - Same pattern as READ
func resourceMyResourceUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
api, region, id, err := NewAPIWithRegionAndID(m, d.Id()) // ✅ Helper 2
// ...
}
func resourceMyResourceDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
api, region, id, err := NewAPIWithRegionAndID(m, d.Id()) // ✅ Helper 2
// ...
}
## Child Resources Pattern
When implementing resources that have parent-child relationships (e.g., Deployment → Database, Instance → User), follow this pattern.
### Example: Database as Child of Deployment
#### 1. Custom ID Constructor and Parser
Child resources need a composite ID format: `region/parent-id/child-identifier`
// ID Constructor
func ResourceDatabaseID(region scw.Region, deploymentID, name string) string {
return fmt.Sprintf("%s/%s/%s", region, deploymentID, name)
}
// ID Parser
func ResourceDatabaseParseID(id string) (scw.Region, string, string, error) {
parts := strings.Split(id, "/")
if len(parts) != 3 {
return "", "", "", fmt.Errorf("unexpected format of ID (%s), expected region/deployment_id/name", id)
}
return scw.Region(parts[0]), parts[1], parts[2], nil
}
**⚠️ IMPORTANT**: These functions MUST be exported (capital first letter) as they're used in tests.
#### 2. Schema Field with DiffSuppressFunc
The parent ID field MUST have `DiffSuppressFunc: dsf.Locality` to handle both bare IDs and composite IDs.
"deployment_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "ID of the parent deployment",
DiffSuppressFunc: dsf.Locality, // ⚠️ CRITICAL!
},
**Why?** Parent IDs can be specified as `"abc-123"` or `"fr-par/abc-123"` in Terraform configs. This suppresses unnecessary diffs.
#### 3. Use locality.ExpandID() in Create
When extracting the parent ID, use `locality.ExpandID()` to strip any region prefix.
func resourceDatabaseCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
api, region, err := myserviceAPIWithRegion(d, m)
if err != nil {
return diag.FromErr(err)
}
// Strip region prefix if present
deploymentID := locality.ExpandID(d.Get("deployment_id").(string))
// Input: "fr-par/abc-123" OR "abc-123"
// Output: "abc-123"
req := &myserviceapi.CreateDatabaseRequest{
Region: region,
DeploymentID: deploymentID, // ✅ Bare ID
Name: d.Get("name").(string),
}
db, err := api.CreateDatabase(req, scw.WithContext(ctx))
// Set composite ID
d.SetId(ResourceDatabaseID(region, deploymentID, db.Name))
return resourceDatabaseRead(ctx, d, m)
}
**Why locality.ExpandID()?** Without it:
- ❌ Bad API URL: `/regions/fr-par/deployments/fr-par/abc-123/databases` → 404
- ✅ Good API URL: `/regions/fr-par/deployments/abc-123/databases`
#### 4. CustomizeDiff for Locality Check (Optional)
Optionally add `CustomizeDiff: cdf.LocalityCheck("deployment_id")` to enforce that child and parent are in the same region.
CustomizeDiff: cdf.LocalityCheck("deployment_id"),
### Complete Example
// Schema
Schema: map[string]*schema.Schema{
"deployment_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
DiffSuppressFunc: dsf.Locality,
},
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"region": regional.Schema(),
},
CustomizeDiff: cdf.LocalityCheck("deployment_id"),
// Create
func resourceDatabaseCreate(...) {
api, region, err := myserviceAPIWithRegion(d, m)
deploymentID := locality.ExpandID(d.Get("deployment_id").(string)) // ✅
// ... create database ...
d.SetId(ResourceDatabaseID(region, deploymentID, db.Name))
}
// Read
func resourceDatabaseRead(...) {
region, deploymentID, name, err := ResourceDatabaseParseID(d.Id())
api := myservice.NewAPI(meta.ExtractScwClient(m))
// ... read database ...
}
## Testing Guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants