Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Bug Fixes

* Encode the governed-tag key as a single path segment when it is sent as a URL path parameter, so hierarchical keys containing `/` route correctly instead of being split into extra path segments and resolving to no endpoint (`404` / `ENDPOINT_NOT_FOUND`). Affects `GetTagPolicy`/`DeleteTagPolicy`/`UpdateTagPolicy` ([tags.TagPoliciesAPI](https://pkg.go.dev/github.com/databricks/databricks-sdk-go/service/tags#TagPoliciesAPI)), `GetTagAssignment`/`DeleteTagAssignment`/`UpdateTagAssignment` ([tags.WorkspaceEntityTagAssignmentsAPI](https://pkg.go.dev/github.com/databricks/databricks-sdk-go/service/tags#WorkspaceEntityTagAssignmentsAPI)), and `Get`/`Delete`/`Update` ([catalog.EntityTagAssignmentsAPI](https://pkg.go.dev/github.com/databricks/databricks-sdk-go/service/catalog#EntityTagAssignmentsAPI)).

### Documentation

### Internal Changes
Expand Down
39 changes: 39 additions & 0 deletions service/catalog/ext_entity_tag_assignments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package catalog

import (
"context"
"net/url"
)

// This file is hand-written (not generated). It extends the generated
// EntityTagAssignmentsAPI to percent-encode the governed-tag key when it travels
// as a URL *path* parameter.
//
// Governed tag keys are hierarchical and may contain "/" (e.g.
// "Field/Shared Technical Services"). The generated impl builds the path as
// /api/2.1/unity-catalog/entity-tag-assignments/{entity_type}/{entity_name}/tags/{tag_key},
// so a raw "/" in the key is treated as a path separator and the request
// resolves to no endpoint (404). Encoding the key ("/" -> %2F) makes it route as
// a single path segment; the server decodes it back to the original key.
//
// These methods shadow the promoted (generated) entityTagAssignmentsImpl methods.
// Only the tag-key path parameter is encoded (entity_type/entity_name are left
// untouched). Create is intentionally NOT overridden: it sends the key in the
// JSON body, where a raw "/" is correct and must be stored verbatim. The TagKey
// field encoded below is `json:"-" url:"-"`, so it is used only in the path —
// never the body.

func (a *EntityTagAssignmentsAPI) Get(ctx context.Context, request GetEntityTagAssignmentRequest) (*EntityTagAssignment, error) {
request.TagKey = url.PathEscape(request.TagKey)
return a.entityTagAssignmentsImpl.Get(ctx, request)
}

func (a *EntityTagAssignmentsAPI) Delete(ctx context.Context, request DeleteEntityTagAssignmentRequest) error {
request.TagKey = url.PathEscape(request.TagKey)
return a.entityTagAssignmentsImpl.Delete(ctx, request)
}

func (a *EntityTagAssignmentsAPI) Update(ctx context.Context, request UpdateEntityTagAssignmentRequest) (*EntityTagAssignment, error) {
request.TagKey = url.PathEscape(request.TagKey)
return a.entityTagAssignmentsImpl.Update(ctx, request)
}
34 changes: 34 additions & 0 deletions service/catalog/ext_entity_tag_assignments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package catalog

import (
"context"
"testing"

"github.com/databricks/databricks-sdk-go/qa"
)

// Verifies that the governed-tag key is percent-encoded when it is sent as a URL
// *path* parameter, so a hierarchical key containing "/" routes as a single path
// segment. The qa fixture matches on the raw request URI, so a passing match
// proves the "/" was encoded to %2F.
func TestGetEntityTagAssignmentEncodesPathKey(t *testing.T) {
requestMocks := qa.HTTPFixtures{
{
Method: "GET",
Resource: "/api/2.1/unity-catalog/entity-tag-assignments/schemas/main.default/tags/cost%2Fcenter?",
Response: EntityTagAssignment{},
},
}
client, server := requestMocks.Client(t)
defer server.Close()
api := &EntityTagAssignmentsAPI{entityTagAssignmentsImpl: entityTagAssignmentsImpl{client: client}}

_, err := api.Get(context.Background(), GetEntityTagAssignmentRequest{
EntityType: "schemas",
EntityName: "main.default",
TagKey: "cost/center",
})
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
}
56 changes: 56 additions & 0 deletions service/tags/ext_impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package tags

import (
"context"
"net/url"
)

// This file is hand-written (not generated). It extends the generated tag APIs
// to percent-encode the governed-tag key when it travels as a URL *path*
// parameter.
//
// Governed tag keys are hierarchical and may contain "/" (e.g.
// "Field/Shared Technical Services"). The generated impls interpolate the key
// straight into the request path:
// - TagPolicies: /api/2.1/tag-policies/{tag_key}
// - WorkspaceEntityTagAssignments: /api/2.0/entity-tag-assignments/{entity_type}/{entity_id}/tags/{tag_key}
// so a raw "/" is treated as a path separator and the request resolves to no
// endpoint (404). Encoding the key ("/" -> %2F) makes it route as a single path
// segment; the server decodes it back to the original key.
//
// These methods shadow the promoted (generated) impl methods. Only the tag-key
// path parameter is encoded (entity_type/entity_id are left untouched). The
// Create* methods are intentionally NOT overridden: they send the key in the
// JSON body, where a raw "/" is correct and must be stored verbatim. The TagKey
// fields encoded below are `json:"-" url:"-"`, so they are used only in the
// path — never the body.

func (a *TagPoliciesAPI) GetTagPolicy(ctx context.Context, request GetTagPolicyRequest) (*TagPolicy, error) {
request.TagKey = url.PathEscape(request.TagKey)
return a.tagPoliciesImpl.GetTagPolicy(ctx, request)
}

func (a *TagPoliciesAPI) DeleteTagPolicy(ctx context.Context, request DeleteTagPolicyRequest) error {
request.TagKey = url.PathEscape(request.TagKey)
return a.tagPoliciesImpl.DeleteTagPolicy(ctx, request)
}

func (a *TagPoliciesAPI) UpdateTagPolicy(ctx context.Context, request UpdateTagPolicyRequest) (*TagPolicy, error) {
request.TagKey = url.PathEscape(request.TagKey)
return a.tagPoliciesImpl.UpdateTagPolicy(ctx, request)
}

func (a *WorkspaceEntityTagAssignmentsAPI) GetTagAssignment(ctx context.Context, request GetTagAssignmentRequest) (*TagAssignment, error) {
request.TagKey = url.PathEscape(request.TagKey)
return a.workspaceEntityTagAssignmentsImpl.GetTagAssignment(ctx, request)
}

func (a *WorkspaceEntityTagAssignmentsAPI) DeleteTagAssignment(ctx context.Context, request DeleteTagAssignmentRequest) error {
request.TagKey = url.PathEscape(request.TagKey)
return a.workspaceEntityTagAssignmentsImpl.DeleteTagAssignment(ctx, request)
}

func (a *WorkspaceEntityTagAssignmentsAPI) UpdateTagAssignment(ctx context.Context, request UpdateTagAssignmentRequest) (*TagAssignment, error) {
request.TagKey = url.PathEscape(request.TagKey)
return a.workspaceEntityTagAssignmentsImpl.UpdateTagAssignment(ctx, request)
}
80 changes: 80 additions & 0 deletions service/tags/ext_impl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package tags

import (
"context"
"testing"

"github.com/databricks/databricks-sdk-go/qa"
)

// These tests verify that the governed-tag key is percent-encoded when it is
// sent as a URL *path* parameter, so a hierarchical key containing "/" (e.g.
// "Field/Shared Technical Services") routes as a single path segment rather
// than being split into multiple segments (which resolves to no endpoint).
// The qa fixture matches on the raw request URI, so a passing match proves the
// "/" was encoded to %2F.

func TestGetTagPolicyEncodesPathKey(t *testing.T) {
requestMocks := qa.HTTPFixtures{
{
Method: "GET",
Resource: "/api/2.1/tag-policies/Field%2FShared%20Technical%20Services?",
Response: TagPolicy{TagKey: "Field/Shared Technical Services"},
},
}
client, server := requestMocks.Client(t)
defer server.Close()
api := &TagPoliciesAPI{tagPoliciesImpl: tagPoliciesImpl{client: client}}

resp, err := api.GetTagPolicy(context.Background(), GetTagPolicyRequest{
TagKey: "Field/Shared Technical Services",
})
if err != nil {
t.Fatalf("GetTagPolicy returned error: %v", err)
}
if resp.TagKey != "Field/Shared Technical Services" {
t.Fatalf("unexpected tag_key in response: %q", resp.TagKey)
}
}

func TestDeleteTagPolicyEncodesPathKey(t *testing.T) {
requestMocks := qa.HTTPFixtures{
{
Method: "DELETE",
Resource: "/api/2.1/tag-policies/Field%2FShared%20Technical%20Services?",
Response: map[string]any{},
},
}
client, server := requestMocks.Client(t)
defer server.Close()
api := &TagPoliciesAPI{tagPoliciesImpl: tagPoliciesImpl{client: client}}

err := api.DeleteTagPolicy(context.Background(), DeleteTagPolicyRequest{
TagKey: "Field/Shared Technical Services",
})
if err != nil {
t.Fatalf("DeleteTagPolicy returned error: %v", err)
}
}

func TestGetTagAssignmentEncodesPathKey(t *testing.T) {
requestMocks := qa.HTTPFixtures{
{
Method: "GET",
Resource: "/api/2.0/entity-tag-assignments/dashboards/123/tags/team%2Fdata-eng?",
Response: TagAssignment{},
},
}
client, server := requestMocks.Client(t)
defer server.Close()
api := &WorkspaceEntityTagAssignmentsAPI{workspaceEntityTagAssignmentsImpl: workspaceEntityTagAssignmentsImpl{client: client}}

_, err := api.GetTagAssignment(context.Background(), GetTagAssignmentRequest{
EntityType: "dashboards",
EntityId: "123",
TagKey: "team/data-eng",
})
if err != nil {
t.Fatalf("GetTagAssignment returned error: %v", err)
}
}
Loading