Skip to content
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

Add label filter to list data sources #19

Merged
merged 1 commit into from
Feb 16, 2024
Merged

Conversation

adriansuarez
Copy link
Collaborator

  • Add label filter to databases and projects (plural) data sources, which causes the labelFilter query parameter to be injected with a comma-separated list of the label constraints when invoking GET.
  • Convert the filter block to a nested attribute. Exposing configuration as blocks seems to be a legacy feature and is discouraged in documentation for the Terraform plugin framework.
  • Add schema builder that eliminates boilerplate when creating list data sources.
  • Make some changes based on PR feedback for Use code generation for Terraform schemas and model structs #16.
  • Adding some test coverage of the attributes that were introduced by Use code generation for Terraform schemas and model structs #16 and of the label filtering.

Copy link
Collaborator

@sivanov-nuodb sivanov-nuodb left a comment

Choose a reason for hiding this comment

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

Looks good.

@@ -0,0 +1,141 @@
package framework
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Missing copyright text.

Comment on lines +45 to +48
sb := framework.NewSchemaBuilder().WithDescription("Data source for listing NuoDB databases created using the DBaaS Control Plane")
sb.WithProjectScopeFilters("databases")
sb.WithProjectScopeList("database", "databases").WithNameAttribute("database")
return sb.Build()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Why not making this "one-liner"?

return framework.NewSchemaBuilder().
  WithDescription("Data source for listing NuoDB databases created using the DBaaS Control Plane").
  WithProjectScopeFilters("databases").
  WithProjectScopeList("database", "databases").
  WithNameAttribute("database").
  Build()

Copy link
Collaborator Author

@adriansuarez adriansuarez Feb 16, 2024

Choose a reason for hiding this comment

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

It doesn't work. WithProjectScopeFilters() returns the *AttributeBuilder and not *SchemaBuilder, since it generates a nested attribute that we can attach attributes to. We would have return the parent to enable that, but then we would not be able to use WithOrganizationScopeFilters() inside of WithProjectScopeFilter() to minimize code duplication. When we have backups or other database scope resource, we may want to invoke WithProjectScopeFilter() inside of WithDatabaseScopeFilter(). Maybe it makes more sense to just duplicate the code in the builder to make it easier for the caller to use.

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

resource.TestCheckResourceAttr("nuodbaas_project.proj", "name", "proj"),
resource.TestCheckResourceAttr("nuodbaas_project.proj", "sla", "dev"),
resource.TestCheckResourceAttr("nuodbaas_project.proj", "tier", "n0.nano"),
// Labels should be removed due to explicit setting of labels={}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does omitting the labels field have the same effect?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Omitting them does not have that effect, and whatever is in the state is retained.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is the correct behavior: if a user deletes a label from their config, we should delete the label off of the resource. The fact that the user deleted the last label by explicitly defining the labels attribute as {}or omitting it should not change it. If you think that this is desirable behavior, it should be obvious in the documentation.

As a user, if I change my config, apply it, revert my commit, and apply the old state, I should return the system to its previous state. That is a core feature of infrastructure-as-code. Obviously, a user can expect that some changes are irreversible but we call those out in the docs and error out if the user tries it.

}
projects, err := helper.GetProjects(ctx, client, organization, true)
projects, err := helper.GetProjects(ctx, client, organization, labelFilter, true)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we defer validation of the label filter to the REST API?

Copy link
Collaborator Author

@adriansuarez adriansuarez Feb 16, 2024

Choose a reason for hiding this comment

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

Yes, I think that is the most straightforward thing to do for now.

It is possible for us to add validators to the Terraform schema from the OpenAPI spec, based on pattern for example, which we can explore. But I think the error messages generated by the REST API are pretty clear.

For example, this is how an invalid product version is reported:

...
nuodbaas_database.db: Modifying... [name=db]
╷
│ Error: Error updating database
│ 
│   with nuodbaas_database.db,
│   on example.tf line 19, in resource "nuodbaas_database" "db":
│   19: resource "nuodbaas_database" "db" {
│ 
│ Error response from Control Plane service: status='HTTP 400 Bad Request', code=HTTP_ERROR, detail=[createDatabase.dbModel.properties.productVersion: does not match pattern
│ '([1-9][0-9]*|[1-9][0-9]*\.[0-9]+|[1-9][0-9]*\.[0-9]+\.[0-9]+)([._-][a-z0-9._-]+)?': x.y]
╵

@adriansuarez
Copy link
Collaborator Author

@sivanov-nuodb I'm a little bit troubled by the e2e test failures. Maybe labels does not actually work with the Operator because labels are overridden somewhere?

@sivanov-nuodb
Copy link
Collaborator

sivanov-nuodb commented Feb 16, 2024

@sivanov-nuodb I'm a little bit troubled by the e2e test failures. Maybe labels does not actually work with the Operator because labels are overridden somewhere?

The user-defined labels have special prefix injected by the REST service so I'm not sure how the operator can override them.
The labels present on the CR resource are just inherited by the underlying workloads and the operator doesn't do any special handling of them.

The operator just enforces that the workload labels are immutable after creation. The user can update the labels on the CR but the update won't be propagated to the workloads.

@adriansuarez
Copy link
Collaborator Author

adriansuarez commented Feb 16, 2024

The user-defined labels have special prefix injected by the REST service so I'm not sure how the operator can override them. The labels present on the CR resource are just inherited by the underlying workloads and the operator doesn't do any special handling of them.

The operator just enforces that the workload labels are immutable after creation. The user can update the labels on the CR but the update won't be propagated to the workloads.

Following up, the issue turned out to be in the REST service, which has a path rewrite that does not preserve query parameters. I am skipping the data sources tests in the e2e suite until that bug is fixed.

Copy link
Contributor

@kontaras kontaras left a comment

Choose a reason for hiding this comment

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

I like the list filter logic. I have some cosmetic suggestions and additional test scenarios.

I don't like the list setting logic (not introduced in this change). We should at least document it so users are not caught off guard by it.

resource.TestCheckResourceAttr("nuodbaas_project.proj", "labels.%", "1"),
resource.TestCheckResourceAttr("nuodbaas_project.proj", "labels.key", "value"),
// product_version should be computed by the REST service
resource.TestCheckResourceAttrSet("nuodbaas_project.proj", "properties.product_version"),
Copy link
Contributor

Choose a reason for hiding this comment

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

This test is redundant because we immediately check that the version is 9.9.9.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch.

resource.TestCheckResourceAttr("nuodbaas_project.proj", "name", "proj"),
resource.TestCheckResourceAttr("nuodbaas_project.proj", "sla", "dev"),
resource.TestCheckResourceAttr("nuodbaas_project.proj", "tier", "n0.nano"),
// Labels should be removed due to explicit setting of labels={}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is the correct behavior: if a user deletes a label from their config, we should delete the label off of the resource. The fact that the user deleted the last label by explicitly defining the labels attribute as {}or omitting it should not change it. If you think that this is desirable behavior, it should be obvious in the documentation.

As a user, if I change my config, apply it, revert my commit, and apply the old state, I should return the system to its previous state. That is a core feature of infrastructure-as-code. Obviously, a user can expect that some changes are irreversible but we call those out in the docs and error out if the user tries it.


type NestedAttributeBuilder struct {
AttributeBuilder
parent *AttributeBuilder
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: parent is initialized and never used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I can probably get rid of NestedAttributeBuilder since it adds no value. The idea with creating this was to allow the parent to be returned by chaining With...() calls, but if we call any method that is implemented by AttributeBuilder then we lose access to the parent. Golang does not have covariant return types that would enable chaining without losing the reference to original type.

return ab.WithComputedStringAttribute("name", "The name of the "+typeName)
}

func (ab *AttributeBuilder) WithStringListAttribute(name, description string) *AttributeBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Since you are hard-coding the attribute as optional, consider making it more obvious by calling the method WithStringListArgument.

parent *AttributeBuilder
}

func (ab *AttributeBuilder) WithNewNestedAttribute(name, description string) *NestedAttributeBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Again, make it obvious that this attribute is going to accept an argument structure.

}
}

func (ab *AttributeBuilder) WithNewListNestedAttribute(name, description string) *NestedAttributeBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: And this method can be more explicit about the fact that it is a read-only (output) attribute.

Comment on lines +91 to +95
// TODO(asz6): This is flaky because the mock controller that simulates the
// DBaaS operator sets the database as ready asynchronously with generating the
// CA certificate. This makes it is possible for the database to be marked as
// ready before the CA PEM is available, which is unrealistic. We can uncomment
// this once the mock controller is updated to have more realistic behavior.
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for adding details

Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(resourcePath, "databases.#", "1"),
resource.TestCheckResourceAttr(resourcePath, "databases.0.name", db1Name),
resource.TestCheckResourceAttr(resourcePath, "databases.0.organization", db1Org),
resource.TestCheckResourceAttr(resourcePath, "databases.0.project", db1Proj),
),
},
// Filter by project org1/proj1
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: We are not actually testing filtering by project, just that a filter can have a project field. This test would pass just as well if project was not sent to the CP.

// Skip if running end-to-end test because path rewrite used with Nginx is broken and does not preserve query parameters
// TODO: Remove once path rewrite is fixed
SkipFunc: func() (bool, error) { return IsE2eTest(), nil },
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Test sending multiple filters.

- Add label filter to databases and projects (plural) data sources,
  which causes the labelFilter query parameter to be injected with a
  comma-separated list of the label constraints when invoking GET.
- Convert the filter block to a nested attribute. Exposing configuration
  as blocks seems to be a legacy feature and is discouraged in
  documentation for the Terraform plugin framework.
- Add schema builder that eliminates boilerplate when creating list data
  sources.
- Make some changes based on PR feedback for #16.
- Adding some test coverage of the attributes that were introduced by
  #16 and of the label filtering.

NOTE: End-to-end testing exposed a bug in the REST service that affects
2.3.1 where the path rewrite done by the REST service was not preserving
query parameters, which prevents label filtering from working.
@adriansuarez adriansuarez merged commit dc40693 into main Feb 16, 2024
1 of 2 checks passed
@adriansuarez adriansuarez deleted the asuarez/label-filter branch February 16, 2024 23:19
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.

3 participants