-
Notifications
You must be signed in to change notification settings - Fork 37
fix(permission-grant): validate namespace exists during plan #208
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ import ( | |
| "errors" | ||
| "fmt" | ||
| "net/http" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
||
|
|
@@ -72,6 +73,7 @@ func resourcePulsarPermissionGrant() *schema.Resource { | |
| ReadContext: resourcePulsarPermissionGrantRead, | ||
| UpdateContext: resourcePulsarPermissionGrantUpdate, | ||
| DeleteContext: resourcePulsarPermissionGrantDelete, | ||
| CustomizeDiff: resourcePulsarPermissionGrantCustomizeDiff, | ||
|
|
||
| Description: `Provides a resource for managing permissions on either Pulsar namespaces or topics. | ||
| Permission can be granted to specific roles using this resource. | ||
|
|
@@ -123,6 +125,80 @@ See the ` + "`permission_grant`" + ` attribute of ` + "`pulsar_namespace`" + ` a | |
| } | ||
| } | ||
|
|
||
| // resourcePulsarPermissionGrantCustomizeDiff runs during the plan phase and fails | ||
| // the plan when the grant references a namespace that is known not to exist. Without | ||
| // this check the missing namespace is only discovered at apply time as a | ||
| // "404 Namespace not found" error, after the change has already been merged. | ||
| func resourcePulsarPermissionGrantCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, | ||
| meta interface{}) error { | ||
| // meta is nil when the provider is not configured (e.g. `terraform validate`). | ||
| // There is no client to consult, so skip the check rather than failing. | ||
| if meta == nil { | ||
| return nil | ||
| } | ||
| client := getClientFromMeta(meta) | ||
|
|
||
| namespace, _ := diff.GetOk("namespace") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Note the resulting behavior is spelling-dependent: |
||
| topic, _ := diff.GetOk("topic") | ||
|
|
||
| return verifyGrantNamespaceExists(client, namespace.(string), topic.(string)) | ||
| } | ||
|
|
||
| // verifyGrantNamespaceExists returns a diagnostic-style error when the namespace | ||
| // targeted by the grant (either directly, or the namespace owning the topic) is | ||
| // known not to exist. | ||
| // | ||
| // It intentionally errs on the side of NOT failing the plan: if the namespace | ||
| // cannot be determined (unparseable input, or the tenant listing call fails — | ||
| // which also covers the tenant itself not existing yet, and transient/network | ||
| // errors), the check is skipped. It only returns an error when the tenant's | ||
| // namespace listing succeeds AND the target namespace is provably absent. This | ||
| // avoids false positives during a fresh apply where the tenant/namespace are | ||
| // being created in the same run. | ||
| func verifyGrantNamespaceExists(client admin.Client, namespace, topic string) error { | ||
| var nsName *utils.NameSpaceName | ||
|
|
||
| switch { | ||
| case namespace != "": | ||
| parsed, err := utils.GetNamespaceName(namespace) | ||
| if err != nil { | ||
| // Malformed input is reported by the CRUD path; don't block the plan here. | ||
| return nil | ||
| } | ||
| nsName = parsed | ||
| case topic != "": | ||
| topicName, err := utils.GetTopicName(topic) | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| parsed, err := utils.GetNameSpaceName(topicName.GetTenant(), topicName.GetNamespace()) | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| nsName = parsed | ||
| default: | ||
| // Neither set: the schema's ExactlyOneOf validation handles this. | ||
| return nil | ||
| } | ||
|
|
||
| // NameSpaceName only exposes String() ("tenant/namespace"); the tenant is the | ||
| // first segment and is required to list the tenant's namespaces. | ||
| tenant := strings.SplitN(nsName.String(), "/", 2)[0] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The The comment is accurate that |
||
| namespaces, err := client.Namespaces().GetNamespaces(tenant) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One If this approach survives, memoize per tenant for the life of the diff, or use a targeted lookup (e.g. |
||
| if err != nil { | ||
| // Cannot determine existence (tenant missing, network error, etc.). Skip. | ||
| return nil | ||
| } | ||
|
|
||
| if !contains(namespaces, nsName.String()) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is where the false positive lands. See the repro in the review summary: this fires on config that applies cleanly on |
||
| return fmt.Errorf( | ||
| "namespace %q does not exist; create it before granting permissions "+ | ||
| "(referenced by pulsar_permission_grant)", nsName.String()) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func resourcePulsarPermissionGrantCreate(ctx context.Context, d *schema.ResourceData, | ||
| meta interface{}) diag.Diagnostics { | ||
| client := getClientFromMeta(meta) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -712,3 +712,93 @@ resource "pulsar_permission_grant" "test" { | |
| } | ||
| `, wsURL, role) | ||
| } | ||
|
|
||
| // TestPermissionGrantNamespaceDoesNotExist verifies that the plan fails when a | ||
| // grant references a namespace that does not exist under an existing tenant — | ||
| // exactly the misconfiguration that previously only surfaced as a 404 at apply | ||
| // time. | ||
| // | ||
| // The test is intentionally two steps. The plan-time check deliberately skips | ||
| // when the tenant's namespace listing fails (e.g. the tenant does not exist yet | ||
| // during a fresh apply), so the tenant must be created first. Step 1 provisions | ||
| // only the cluster and tenant; step 2 introduces the grant against a namespace | ||
| // that is never declared, so the listing succeeds and provably lacks it. | ||
| func TestPermissionGrantNamespaceDoesNotExist(t *testing.T) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This covers the case where the namespace is never declared. The gap is the mirror case: tenant already exists and the namespace is declared in the same config. Adding that as a step here would have caught the regression — step 1 cluster + tenant, step 2 the same plus |
||
| cName := acctest.RandString(10) | ||
| tName := acctest.RandString(10) | ||
| nsName := acctest.RandString(10) | ||
| roleName := acctest.RandString(10) | ||
|
|
||
| resource.Test(t, resource.TestCase{ | ||
| PreCheck: func() { testAccPreCheck(t) }, | ||
| ProviderFactories: testAccProviderFactories, | ||
| Steps: []resource.TestStep{ | ||
| { | ||
| // Create the tenant (and cluster) so the namespace listing succeeds later. | ||
| Config: testPulsarPermissionGrantTenantOnly(testWebServiceURL, cName, tName), | ||
| }, | ||
| { | ||
| // Add a grant referencing a namespace that was never created. | ||
| Config: testPulsarPermissionGrantMissingNamespace( | ||
| testWebServiceURL, cName, tName, nsName, roleName), | ||
| ExpectError: regexp.MustCompile(`namespace .* does not exist`), | ||
| }, | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| func testPulsarPermissionGrantTenantOnly(wsURL, cluster, tenant string) string { | ||
| return fmt.Sprintf(` | ||
| provider "pulsar" { | ||
| web_service_url = "%s" | ||
| } | ||
|
|
||
| resource "pulsar_cluster" "test_cluster" { | ||
| cluster = "%s" | ||
|
|
||
| cluster_data { | ||
| web_service_url = "http://localhost:8080" | ||
| broker_service_url = "pulsar://localhost:6050" | ||
| peer_clusters = ["standalone"] | ||
| } | ||
| } | ||
|
|
||
| resource "pulsar_tenant" "test_tenant" { | ||
| tenant = "%s" | ||
| allowed_clusters = [pulsar_cluster.test_cluster.cluster, "standalone"] | ||
| } | ||
| `, wsURL, cluster, tenant) | ||
| } | ||
|
|
||
| // testPulsarPermissionGrantMissingNamespace keeps the cluster and tenant from the | ||
| // previous step and adds a permission grant referencing a namespace under that | ||
| // tenant that is never declared. The tenant's namespace listing succeeds and | ||
| // does not contain the referenced namespace, so the plan-time check must fail. | ||
| func testPulsarPermissionGrantMissingNamespace(wsURL, cluster, tenant, namespace, role string) string { | ||
| return fmt.Sprintf(` | ||
| provider "pulsar" { | ||
| web_service_url = "%s" | ||
| } | ||
|
|
||
| resource "pulsar_cluster" "test_cluster" { | ||
| cluster = "%s" | ||
|
|
||
| cluster_data { | ||
| web_service_url = "http://localhost:8080" | ||
| broker_service_url = "pulsar://localhost:6050" | ||
| peer_clusters = ["standalone"] | ||
| } | ||
| } | ||
|
|
||
| resource "pulsar_tenant" "test_tenant" { | ||
| tenant = "%s" | ||
| allowed_clusters = [pulsar_cluster.test_cluster.cluster, "standalone"] | ||
| } | ||
|
|
||
| resource "pulsar_permission_grant" "test" { | ||
| namespace = "${pulsar_tenant.test_tenant.tenant}/%s" | ||
| role = "%s" | ||
| actions = ["produce", "consume"] | ||
| } | ||
| `, wsURL, cluster, tenant, namespace, role) | ||
| } | ||
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.
Nit:
terraform validatedoesn't reachCustomizeDiff— it callsValidateResourceConfig, notPlanResourceChange. The nil guard is worth keeping as defensive code, sincegetClientFromMetadoes an uncheckedmeta.(PulsarClientBundle), but the stated reason is misleading.