-
Notifications
You must be signed in to change notification settings - Fork 133
chore: add support for an AGENTS.md #3453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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. |
|
|
||
| 3. **Cassette Organization** | ||
| - Store cassettes in `internal/services/{service}/testdata/` | ||
| - Use descriptive names (e.g., `{resource}-basic.cassette.yaml`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 1. **Run linting on changed files only** | ||
| ```bash | ||
| # Run on specific files/directories | ||
| golangci-lint run internal/services/account/... |
There was a problem hiding this comment.
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: | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ### 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 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| - 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ### 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 |
There was a problem hiding this comment.
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.
| ## 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 |
No description provided.