Skip to content

chore: use conversion functions, fix Read methods#95

Draft
markgoddard wants to merge 1 commit into
mainfrom
fix-reads
Draft

chore: use conversion functions, fix Read methods#95
markgoddard wants to merge 1 commit into
mainfrom
fix-reads

Conversation

@markgoddard
Copy link
Copy Markdown
Member

@markgoddard markgoddard commented Mar 6, 2026

I put this together with Gemini some time ago, using the conversion function pattern more consistently. I wondered if it might help to address some of the issues that #91 is attempting to resolve.

I don't remember how much I tested this, but thought I'd share it as an alternative/complementary approach.

@markgoddard markgoddard self-assigned this Mar 6, 2026
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the Terraform provider's internal services by introducing dedicated conversion functions and enhancing the Read operations for resources and data sources. The changes aim to improve code organization, reduce duplication, and ensure more robust handling of resource lifecycle events, particularly when resources are no longer found in the backend API.

Highlights

  • Centralized Conversion Logic: Introduced dedicated convert.go files for apbinding, cluster, federation, and trustzone services. These files now house modelToProto and protoToModel functions, centralizing the conversion logic between Terraform models and protobuf messages.
  • Improved Read Method Robustness: Refactored the Read methods across various resources (APBinding, Cluster, Federation, TrustZone) to correctly handle NotFound gRPC errors by removing the resource from the Terraform state, ensuring better state management for deleted resources.
  • Code Cleanliness and Maintainability: Updated Create, Read, and Update methods in resource and data source files to utilize the new conversion functions, significantly reducing boilerplate code and improving readability. Unnecessary imports were also removed.
  • Consistent Receiver Naming: Standardized the receiver names in APBindingDataSource and APBindingResource methods from a to d and r respectively, aligning with Go best practices for clarity.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • internal/services/apbinding/convert.go
    • Added modelToProto function to convert APBindingModel to apbindingpb.APBinding.
    • Added protoToModel function to convert apbindingpb.APBinding to APBindingModel.
  • internal/services/apbinding/data_source.go
    • Removed unused types import.
    • Updated receiver names for methods from a to d.
    • Refactored Read method to use protoToModel for state conversion.
  • internal/services/apbinding/resource.go
    • Removed unused apbindingpb and tftypes imports.
    • Updated receiver names for methods from a to r.
    • Refactored Create method to use modelToProto for request and protoToModel for response.
    • Implemented robust Read method with GetAPBinding call and NotFound error handling.
    • Refactored Update method to use modelToProto for request and protoToModel for response.
  • internal/services/cluster/convert.go
    • Added modelToProto function for Cluster conversions, including OIDC and Helm values.
    • Added protoToModel function for Cluster conversions, handling OIDC and Helm values.
    • Added newTrustProvider function to validate and create TrustProvider protobufs.
    • Added parseExtraHelmValues function to convert string Helm values to structpb.Struct.
    • Added validateHelmValues function to compare plan and response Helm values.
  • internal/services/cluster/data_source.go
    • Removed unused base64 and types imports.
    • Refactored Read method to use validateHelmValues and protoToModel for state conversion.
  • internal/services/cluster/resource.go
    • Removed numerous unused imports (base64, json, clusterpb, trustproviderpb, tftypes, structpb, yaml).
    • Refactored Create method to use modelToProto and protoToModel.
    • Implemented robust Read method with GetCluster call, NotFound error handling, and validateHelmValues.
    • Refactored Update method to use modelToProto and protoToModel.
    • Removed newTrustProvider, parseExtraHelmValues, and validateHelmValues as they are now in convert.go.
  • internal/services/federation/convert.go
    • Added modelToProto function to convert FederationModel to federationpb.Federation.
    • Added protoToModel function to convert federationpb.Federation to FederationModel.
  • internal/services/federation/data_source.go
    • Removed unused types import.
    • Refactored Read method to use protoToModel for state conversion.
  • internal/services/federation/resource.go
    • Removed unused federationpb and tftypes imports.
    • Refactored Create method to use modelToProto for request and protoToModel for response.
    • Implemented robust Read method with GetFederation call and NotFound error handling.
  • internal/services/organization/convert.go
    • Updated protoToModel function to use types.StringValue directly and added a comment.
  • internal/services/trustzone/convert.go
    • Added modelToProto function for TrustZone conversions, handling bundle endpoint profiles and JWT issuer.
    • Added protoToModel function for TrustZone conversions.
    • Added getBundleEndpointProfile function to convert string profiles to enum pointers.
  • internal/services/trustzone/data_source.go
    • Removed unused types import.
    • Refactored Read method to use protoToModel for state conversion.
  • internal/services/trustzone/resource.go
    • Removed unused trustzonepb and tftypes imports.
    • Refactored Create method to use modelToProto for request and protoToModel for response.
    • Implemented robust Read method with GetTrustZone call and NotFound error handling.
    • Refactored Update method to use modelToProto for request and protoToModel for response.
    • Removed getBundleEndpointProfile as it is now in convert.go.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@markgoddard
Copy link
Copy Markdown
Member Author

I put this together with Gemini some time ago, using the conversion function pattern more consistently. I wondered if it might help to address some of the issues that #91 is attempting to resolve.

I don't remember how much I tested this, but thought I'd share it as an alternative/complementary approach.

@markgoddard markgoddard requested review from alec-w and mattbates March 6, 2026 10:53
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant improvement to the codebase. The introduction of convert.go files for each service to handle model-to-proto and proto-to-model conversions is an excellent refactoring that greatly enhances code clarity and maintainability. Additionally, implementing the Read methods for several resources is a crucial fix that will improve the provider's correctness and reliability. I've found one issue in the Helm values validation logic that could lead to incorrect plan failures, for which I've provided a detailed comment and suggestion.

Comment on lines +118 to +154
func validateHelmValues(planValues types.String, responseValues *structpb.Struct) error {
if planValues.ValueString() == "" && responseValues == nil {
return nil
}

if planValues.ValueString() == "" && responseValues != nil {
return fmt.Errorf("invalid Helm values: plan is empty while the response is not")
}

if planValues.ValueString() != "" && responseValues == nil {
return fmt.Errorf("invalid Helm values: plan is not empty while the response is nil")
}

var planValuesMap map[string]interface{}

if err := yaml.Unmarshal([]byte(planValues.ValueString()), &planValuesMap); err != nil {
return fmt.Errorf("error parsing plan extra_helm_values: %w", err)
}

responseValuesMap := responseValues.AsMap()

planValuesJSON, err := json.Marshal(planValuesMap)
if err != nil {
return fmt.Errorf("error marshaling plan values: %w", err)
}

responseValuesJSON, err := json.Marshal(responseValuesMap)
if err != nil {
return fmt.Errorf("error marshaling response values: %w", err)
}

if string(planValuesJSON) != string(responseValuesJSON) {
return fmt.Errorf("a Helm values mismatch: plan values don't match response values")
}

return nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current implementation of validateHelmValues has a flaw in how it compares empty Helm values. An empty string ("") from the plan is unmarshaled into a nil map, which then becomes "null" when marshaled to JSON. However, an empty struct from the API response becomes an empty map, which marshals to "{}". This leads to a mismatch ("null" != "{}") and an incorrect validation failure.

This can happen, for example, during a Read operation where the state has an empty extra_helm_values (represented as "") and the API returns an empty struct. The current logic would incorrectly report a diff.

I suggest simplifying the function to consistently handle different representations of "empty" values (null, empty string, empty map/struct) by treating them all as an empty map for comparison purposes. This will make the validation more robust.

// validateHelmValues compares the Helm values from the plan and the cluster API response.
func validateHelmValues(planValues types.String, responseValues *structpb.Struct) error {
	var planValuesMap map[string]interface{}
	// An empty string from plan should be treated as an empty map for comparison.
	if !planValues.IsNull() && planValues.ValueString() != "" {
		if err := yaml.Unmarshal([]byte(planValues.ValueString()), &planValuesMap); err != nil {
			return fmt.Errorf("error parsing plan extra_helm_values: %w", err)
		}
	}

	responseValuesMap := responseValues.AsMap()

	// If both are effectively empty, they match.
	if len(planValuesMap) == 0 && len(responseValuesMap) == 0 {
		return nil
	}

	planValuesJSON, err := json.Marshal(planValuesMap)
	if err != nil {
		return fmt.Errorf("error marshaling plan values: %w", err)
	}

	responseValuesJSON, err := json.Marshal(responseValuesMap)
	if err != nil {
		return fmt.Errorf("error marshaling response values: %w", err)
	}

	if string(planValuesJSON) != string(responseValuesJSON) {
		return fmt.Errorf("a Helm values mismatch: plan values don't match response values")
	}

	return nil
}

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