Skip to content

refactor: add GCP parent resolver abstraction for config provider#3974

Open
amirbenun wants to merge 4 commits intoelastic:mainfrom
amirbenun:gcp-parent-resolver
Open

refactor: add GCP parent resolver abstraction for config provider#3974
amirbenun wants to merge 4 commits intoelastic:mainfrom
amirbenun:gcp-parent-resolver

Conversation

@amirbenun
Copy link
Contributor

Adds a pluggable parent resolution layer to the GCP auth config provider so that the GCP parent (e.g. projects/pid or organizations/oid) is determined by a dedicated ParentResolver interface. This enables clearer separation of concerns and testability when resolving project ID from config, application default credentials, or organization ID. The implementation introduces ProjectParentResolver and OrganizationParentResolver, a DefaultParentResolver that composes them, a helper to extract project number from Workload Identity Federation audience URLs, and updates the asset inventory and benchmark strategies to use the new NewConfigProvider() factory.

Copilot AI review requested due to automatic review settings February 17, 2026 10:51
@amirbenun amirbenun requested a review from a team as a code owner February 17, 2026 10:51
@mergify
Copy link

mergify bot commented Feb 17, 2026

This pull request does not have a backport label. Could you fix it @amirbenun? 🙏
To fixup this pull request, you need to add the backport labels for the needed
branches, such as:

  • backport-v./d./d./d is the label to automatically backport to the 8./d branch. /d is the digit
  • backport-active-all is the label that automatically backports to all active branches.
  • backport-active-8 is the label that automatically backports to all active minor branches for the 8 major.
  • backport-active-9 is the label that automatically backports to all active minor branches for the 9 major.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the GCP authentication configuration provider by introducing a pluggable parent resolution abstraction. The refactoring extracts the logic for determining GCP parent resources (projects or organizations) from configuration, Workload Identity Federation audience URLs, or Application Default Credentials into dedicated resolver components.

Changes:

  • Introduces ParentResolver, ProjectParentResolver, and OrganizationParentResolver interfaces with default implementations
  • Adds projectNumberFromAudience helper function to extract project numbers from WIF audience URLs
  • Updates ConfigProvider to use ParentResolver and provides NewConfigProvider() factory for production use
  • Updates benchmark and asset inventory strategies to use the new factory function

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/resources/providers/gcplib/auth/parent_resolver.go Defines ParentResolver interface and defaultParentResolver facade that delegates to project/org resolvers
internal/resources/providers/gcplib/auth/project_parent_resolver.go Implements ProjectParentResolver with fallback logic: config → audience → ADC
internal/resources/providers/gcplib/auth/organization_parent_resolver.go Implements OrganizationParentResolver using Resource Manager API to resolve org from ancestry
internal/resources/providers/gcplib/auth/audience.go Helper function to extract project number from WIF audience URLs
internal/resources/providers/gcplib/auth/audience_test.go Comprehensive tests for audience parsing logic
internal/resources/providers/gcplib/auth/parent_resolver_test.go Unit tests for defaultParentResolver delegation logic
internal/resources/providers/gcplib/auth/parent_resolver_mock.go Mock implementations for ProjectParentResolver and OrganizationParentResolver interfaces
internal/resources/providers/gcplib/auth/credentials_mock.go Added mocks for DefaultCredentialsFinder and ParentResolver interfaces
internal/resources/providers/gcplib/auth/credentials.go Refactored to use ParentResolver; adds NewConfigProvider() factory; removes old parent resolution functions
internal/resources/providers/gcplib/auth/credentials_test.go Updated to initialize ParentResolver in test fixtures
internal/flavors/benchmark/strategy.go Updated to use NewConfigProvider() factory
internal/flavors/assetinventory/strategy.go Updated to use NewConfigProvider() factory

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +65 to +83
func resolveProjectIdFromAudience(ctx context.Context, clientOpts []option.ClientOption, audience string) (string, error) {
projectNumber, ok := projectNumberFromAudience(audience)
if !ok {
return "", fmt.Errorf("audience does not contain a valid project number (expected format: //iam.googleapis.com/projects/PROJECT_NUMBER/...): %w", ErrProjectNotFound)
}
opts := append([]option.ClientOption{option.WithScopes(cloudresourcemanager.CloudPlatformReadOnlyScope)}, clientOpts...)
svc, err := cloudresourcemanager.NewService(ctx, opts...)
if err != nil {
return "", fmt.Errorf("failed to create Resource Manager client: %w", err)
}
project, err := svc.Projects.Get("projects/" + projectNumber).Context(ctx).Do()
if err != nil {
return "", fmt.Errorf("failed to get project (number %s): %w", projectNumber, err)
}
if project.ProjectId == "" {
return "", fmt.Errorf("project response missing project_id: %w", ErrProjectNotFound)
}
return project.ProjectId, nil
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The new functions resolveProjectIdFromAudience (line 65) and resolveOrganizationIdFromAudience in organization_parent_resolver.go (line 59) make external API calls to GCP Cloud Resource Manager but lack direct unit test coverage. While the audience.go helper function projectNumberFromAudience has comprehensive tests, the actual API integration logic for resolving project IDs and organization IDs from the audience is not tested.

Consider adding unit tests that mock the Cloud Resource Manager API responses to verify:

  • Successful project ID resolution from project number
  • Successful organization ID resolution via GetAncestry
  • Error handling when the API calls fail
  • Edge cases like missing project_id in API response or no organization in ancestry

The existing test coverage in parent_resolver_test.go only tests the delegation logic, not the actual implementation of these resolver functions.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +88
func resolveOrganizationIdFromAudience(ctx context.Context, clientOpts []option.ClientOption, audience string) (string, error) {
opts := append([]option.ClientOption{option.WithScopes(cloudresourcemanager.CloudPlatformReadOnlyScope)}, clientOpts...)
svcV3, err := cloudresourcemanager.NewService(ctx, opts...)
if err != nil {
return "", fmt.Errorf("failed to create Resource Manager client: %w", err)
}
projectNumber, ok := projectNumberFromAudience(audience)
if !ok {
return "", fmt.Errorf("audience does not contain a valid project number: %w", ErrMissingOrgId)
}
project, err := svcV3.Projects.Get("projects/" + projectNumber).Context(ctx).Do()
if err != nil {
return "", fmt.Errorf("failed to get project (number %s): %w", projectNumber, err)
}
optsV1 := append([]option.ClientOption{option.WithScopes(crmv1.CloudPlatformReadOnlyScope)}, clientOpts...)
svcV1, err := crmv1.NewService(ctx, optsV1...)
if err != nil {
return "", fmt.Errorf("failed to create Resource Manager v1 client: %w", err)
}
resp, err := svcV1.Projects.GetAncestry(project.ProjectId, &crmv1.GetAncestryRequest{}).Context(ctx).Do()
if err != nil {
return "", fmt.Errorf("failed to get project ancestry: %w", err)
}
for _, anc := range resp.Ancestor {
if anc.ResourceId != nil && anc.ResourceId.Type == resourceIdTypeOrganization && anc.ResourceId.Id != "" {
return anc.ResourceId.Id, nil
}
}
return "", fmt.Errorf("no organization found in resource hierarchy: %w", ErrMissingOrgId)
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The function resolveOrganizationIdFromAudience makes external API calls to GCP Cloud Resource Manager v3 and v1 but lacks direct unit test coverage. While the facade pattern is tested in parent_resolver_test.go, the actual implementation logic including the two-step API call sequence (Projects.Get followed by Projects.GetAncestry) is not tested.

Consider adding unit tests that mock the Cloud Resource Manager API responses to verify:

  • Successful organization ID resolution through ancestry lookup
  • Error handling when Projects.Get fails
  • Error handling when Projects.GetAncestry fails
  • Edge case when no organization is found in the resource hierarchy
  • Validation of the resource ID type filtering logic (line 83)

Copilot uses AI. Check for mistakes.

// resolveOrganizationIdFromAudience uses the project number in the audience to get
// the project via v3, then calls v1 getAncestry to resolve the organization in one call.
// clientOpts must authenticate with resourcemanager.projects.get.
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The comment states that clientOpts must authenticate with "resourcemanager.projects.get" permission, but the function also calls Projects.GetAncestry (line 78) which requires the "resourcemanager.projects.getAncestry" permission.

Update the comment to clarify that both permissions are required:
"clientOpts must authenticate as an identity with resourcemanager.projects.get and resourcemanager.projects.getAncestry permissions."

Suggested change
// clientOpts must authenticate with resourcemanager.projects.get.
// clientOpts must authenticate as an identity with resourcemanager.projects.get and resourcemanager.projects.getAncestry permissions.

Copilot uses AI. Check for mistakes.
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.

1 participant