Skip to content

Commit a336c9b

Browse files
Encode tag key as a single path segment in tag-policy and tag-assignment APIs
Governed tag keys may be hierarchical and contain "/" (e.g. "a/b"). The generated clients interpolate the key straight into the request path, so a raw "/" is treated as a path separator and the request resolves to no matching endpoint (404 / ENDPOINT_NOT_FOUND). Get, Delete, and Update are affected. Add hand-written overrides that url.PathEscape the tag-key path parameter before delegating to the generated impl; the server decodes it back to the original key. Covers: - tags.TagPoliciesAPI: Get/Delete/UpdateTagPolicy - tags.WorkspaceEntityTagAssignmentsAPI: Get/Delete/UpdateTagAssignment - catalog.EntityTagAssignmentsAPI: Get/Delete/Update Only the tag-key path parameter is encoded; entity_type/entity_id/entity_name are left untouched. The Create methods are intentionally not overridden -- they send the key in the JSON body, where "/" is valid and must be stored verbatim. Co-authored-by: Isaac Signed-off-by: Lizhen Xiang <lizhen.xiang@databricks.com>
1 parent 9be376d commit a336c9b

5 files changed

Lines changed: 211 additions & 0 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
### Bug Fixes
1010

11+
* 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)).
12+
1113
### Documentation
1214

1315
### Internal Changes
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package catalog
2+
3+
import (
4+
"context"
5+
"net/url"
6+
)
7+
8+
// This file is hand-written (not generated). It extends the generated
9+
// EntityTagAssignmentsAPI to percent-encode the governed-tag key when it travels
10+
// as a URL *path* parameter.
11+
//
12+
// Governed tag keys are hierarchical and may contain "/" (e.g.
13+
// "Field/Shared Technical Services"). The generated impl builds the path as
14+
// /api/2.1/unity-catalog/entity-tag-assignments/{entity_type}/{entity_name}/tags/{tag_key},
15+
// so a raw "/" in the key is treated as a path separator and the request
16+
// resolves to no endpoint (404). Encoding the key ("/" -> %2F) makes it route as
17+
// a single path segment; the server decodes it back to the original key.
18+
//
19+
// These methods shadow the promoted (generated) entityTagAssignmentsImpl methods.
20+
// Only the tag-key path parameter is encoded (entity_type/entity_name are left
21+
// untouched). Create is intentionally NOT overridden: it sends the key in the
22+
// JSON body, where a raw "/" is correct and must be stored verbatim. The TagKey
23+
// field encoded below is `json:"-" url:"-"`, so it is used only in the path —
24+
// never the body.
25+
26+
func (a *EntityTagAssignmentsAPI) Get(ctx context.Context, request GetEntityTagAssignmentRequest) (*EntityTagAssignment, error) {
27+
request.TagKey = url.PathEscape(request.TagKey)
28+
return a.entityTagAssignmentsImpl.Get(ctx, request)
29+
}
30+
31+
func (a *EntityTagAssignmentsAPI) Delete(ctx context.Context, request DeleteEntityTagAssignmentRequest) error {
32+
request.TagKey = url.PathEscape(request.TagKey)
33+
return a.entityTagAssignmentsImpl.Delete(ctx, request)
34+
}
35+
36+
func (a *EntityTagAssignmentsAPI) Update(ctx context.Context, request UpdateEntityTagAssignmentRequest) (*EntityTagAssignment, error) {
37+
request.TagKey = url.PathEscape(request.TagKey)
38+
return a.entityTagAssignmentsImpl.Update(ctx, request)
39+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package catalog
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/databricks/databricks-sdk-go/qa"
8+
)
9+
10+
// Verifies that the governed-tag key is percent-encoded when it is sent as a URL
11+
// *path* parameter, so a hierarchical key containing "/" routes as a single path
12+
// segment. The qa fixture matches on the raw request URI, so a passing match
13+
// proves the "/" was encoded to %2F.
14+
func TestGetEntityTagAssignmentEncodesPathKey(t *testing.T) {
15+
requestMocks := qa.HTTPFixtures{
16+
{
17+
Method: "GET",
18+
Resource: "/api/2.1/unity-catalog/entity-tag-assignments/schemas/main.default/tags/cost%2Fcenter?",
19+
Response: EntityTagAssignment{},
20+
},
21+
}
22+
client, server := requestMocks.Client(t)
23+
defer server.Close()
24+
api := &EntityTagAssignmentsAPI{entityTagAssignmentsImpl: entityTagAssignmentsImpl{client: client}}
25+
26+
_, err := api.Get(context.Background(), GetEntityTagAssignmentRequest{
27+
EntityType: "schemas",
28+
EntityName: "main.default",
29+
TagKey: "cost/center",
30+
})
31+
if err != nil {
32+
t.Fatalf("Get returned error: %v", err)
33+
}
34+
}

service/tags/ext_impl.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package tags
2+
3+
import (
4+
"context"
5+
"net/url"
6+
)
7+
8+
// This file is hand-written (not generated). It extends the generated tag APIs
9+
// to percent-encode the governed-tag key when it travels as a URL *path*
10+
// parameter.
11+
//
12+
// Governed tag keys are hierarchical and may contain "/" (e.g.
13+
// "Field/Shared Technical Services"). The generated impls interpolate the key
14+
// straight into the request path:
15+
// - TagPolicies: /api/2.1/tag-policies/{tag_key}
16+
// - WorkspaceEntityTagAssignments: /api/2.0/entity-tag-assignments/{entity_type}/{entity_id}/tags/{tag_key}
17+
// so a raw "/" is treated as a path separator and the request resolves to no
18+
// endpoint (404). Encoding the key ("/" -> %2F) makes it route as a single path
19+
// segment; the server decodes it back to the original key.
20+
//
21+
// These methods shadow the promoted (generated) impl methods. Only the tag-key
22+
// path parameter is encoded (entity_type/entity_id are left untouched). The
23+
// Create* methods are intentionally NOT overridden: they send the key in the
24+
// JSON body, where a raw "/" is correct and must be stored verbatim. The TagKey
25+
// fields encoded below are `json:"-" url:"-"`, so they are used only in the
26+
// path — never the body.
27+
28+
func (a *TagPoliciesAPI) GetTagPolicy(ctx context.Context, request GetTagPolicyRequest) (*TagPolicy, error) {
29+
request.TagKey = url.PathEscape(request.TagKey)
30+
return a.tagPoliciesImpl.GetTagPolicy(ctx, request)
31+
}
32+
33+
func (a *TagPoliciesAPI) DeleteTagPolicy(ctx context.Context, request DeleteTagPolicyRequest) error {
34+
request.TagKey = url.PathEscape(request.TagKey)
35+
return a.tagPoliciesImpl.DeleteTagPolicy(ctx, request)
36+
}
37+
38+
func (a *TagPoliciesAPI) UpdateTagPolicy(ctx context.Context, request UpdateTagPolicyRequest) (*TagPolicy, error) {
39+
request.TagKey = url.PathEscape(request.TagKey)
40+
return a.tagPoliciesImpl.UpdateTagPolicy(ctx, request)
41+
}
42+
43+
func (a *WorkspaceEntityTagAssignmentsAPI) GetTagAssignment(ctx context.Context, request GetTagAssignmentRequest) (*TagAssignment, error) {
44+
request.TagKey = url.PathEscape(request.TagKey)
45+
return a.workspaceEntityTagAssignmentsImpl.GetTagAssignment(ctx, request)
46+
}
47+
48+
func (a *WorkspaceEntityTagAssignmentsAPI) DeleteTagAssignment(ctx context.Context, request DeleteTagAssignmentRequest) error {
49+
request.TagKey = url.PathEscape(request.TagKey)
50+
return a.workspaceEntityTagAssignmentsImpl.DeleteTagAssignment(ctx, request)
51+
}
52+
53+
func (a *WorkspaceEntityTagAssignmentsAPI) UpdateTagAssignment(ctx context.Context, request UpdateTagAssignmentRequest) (*TagAssignment, error) {
54+
request.TagKey = url.PathEscape(request.TagKey)
55+
return a.workspaceEntityTagAssignmentsImpl.UpdateTagAssignment(ctx, request)
56+
}

service/tags/ext_impl_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package tags
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/databricks/databricks-sdk-go/qa"
8+
)
9+
10+
// These tests verify that the governed-tag key is percent-encoded when it is
11+
// sent as a URL *path* parameter, so a hierarchical key containing "/" (e.g.
12+
// "Field/Shared Technical Services") routes as a single path segment rather
13+
// than being split into multiple segments (which resolves to no endpoint).
14+
// The qa fixture matches on the raw request URI, so a passing match proves the
15+
// "/" was encoded to %2F.
16+
17+
func TestGetTagPolicyEncodesPathKey(t *testing.T) {
18+
requestMocks := qa.HTTPFixtures{
19+
{
20+
Method: "GET",
21+
Resource: "/api/2.1/tag-policies/Field%2FShared%20Technical%20Services?",
22+
Response: TagPolicy{TagKey: "Field/Shared Technical Services"},
23+
},
24+
}
25+
client, server := requestMocks.Client(t)
26+
defer server.Close()
27+
api := &TagPoliciesAPI{tagPoliciesImpl: tagPoliciesImpl{client: client}}
28+
29+
resp, err := api.GetTagPolicy(context.Background(), GetTagPolicyRequest{
30+
TagKey: "Field/Shared Technical Services",
31+
})
32+
if err != nil {
33+
t.Fatalf("GetTagPolicy returned error: %v", err)
34+
}
35+
if resp.TagKey != "Field/Shared Technical Services" {
36+
t.Fatalf("unexpected tag_key in response: %q", resp.TagKey)
37+
}
38+
}
39+
40+
func TestDeleteTagPolicyEncodesPathKey(t *testing.T) {
41+
requestMocks := qa.HTTPFixtures{
42+
{
43+
Method: "DELETE",
44+
Resource: "/api/2.1/tag-policies/Field%2FShared%20Technical%20Services?",
45+
Response: map[string]any{},
46+
},
47+
}
48+
client, server := requestMocks.Client(t)
49+
defer server.Close()
50+
api := &TagPoliciesAPI{tagPoliciesImpl: tagPoliciesImpl{client: client}}
51+
52+
err := api.DeleteTagPolicy(context.Background(), DeleteTagPolicyRequest{
53+
TagKey: "Field/Shared Technical Services",
54+
})
55+
if err != nil {
56+
t.Fatalf("DeleteTagPolicy returned error: %v", err)
57+
}
58+
}
59+
60+
func TestGetTagAssignmentEncodesPathKey(t *testing.T) {
61+
requestMocks := qa.HTTPFixtures{
62+
{
63+
Method: "GET",
64+
Resource: "/api/2.0/entity-tag-assignments/dashboards/123/tags/team%2Fdata-eng?",
65+
Response: TagAssignment{},
66+
},
67+
}
68+
client, server := requestMocks.Client(t)
69+
defer server.Close()
70+
api := &WorkspaceEntityTagAssignmentsAPI{workspaceEntityTagAssignmentsImpl: workspaceEntityTagAssignmentsImpl{client: client}}
71+
72+
_, err := api.GetTagAssignment(context.Background(), GetTagAssignmentRequest{
73+
EntityType: "dashboards",
74+
EntityId: "123",
75+
TagKey: "team/data-eng",
76+
})
77+
if err != nil {
78+
t.Fatalf("GetTagAssignment returned error: %v", err)
79+
}
80+
}

0 commit comments

Comments
 (0)