Skip to content

Commit b061884

Browse files
authored
Merge pull request #2331 from redpanda-data/fe/sr-context-support
Schema registry context support
2 parents 5fa83db + e9c29ac commit b061884

36 files changed

Lines changed: 3389 additions & 726 deletions

backend/go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ require (
4545
github.com/redpanda-data/common-go/api v0.0.0-20260130192523-413455981e59
4646
github.com/redpanda-data/common-go/net v0.1.1-0.20240429123545-4da3d2b371f7
4747
github.com/redpanda-data/common-go/rpadmin v0.2.0
48-
github.com/redpanda-data/common-go/rpsr v0.1.2
48+
github.com/redpanda-data/common-go/rpsr v0.1.4
4949
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
5050
github.com/stretchr/testify v1.11.1
5151
github.com/testcontainers/testcontainers-go v0.38.0
@@ -56,7 +56,7 @@ require (
5656
github.com/twmb/franz-go/pkg/kfake v0.0.0-20251115002817-3affad808a82
5757
github.com/twmb/franz-go/pkg/kmsg v1.12.0
5858
github.com/twmb/franz-go/pkg/sasl/kerberos v1.1.0
59-
github.com/twmb/franz-go/pkg/sr v1.6.0
59+
github.com/twmb/franz-go/pkg/sr v1.7.0
6060
github.com/twmb/franz-go/plugin/kslog v1.0.0
6161
github.com/twmb/go-cache v1.2.1
6262
github.com/twmb/tlscfg v1.2.1
@@ -66,7 +66,7 @@ require (
6666
go.vallahaye.net/connect-gateway v0.11.0
6767
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
6868
golang.org/x/net v0.51.0
69-
golang.org/x/sync v0.19.0
69+
golang.org/x/sync v0.20.0
7070
golang.org/x/text v0.34.0
7171
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d
7272
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d

backend/go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,8 @@ github.com/redpanda-data/common-go/net v0.1.1-0.20240429123545-4da3d2b371f7 h1:M
398398
github.com/redpanda-data/common-go/net v0.1.1-0.20240429123545-4da3d2b371f7/go.mod h1:UJIi/yUxGOBYXUrfUsOkxfYxcb/ll7mZrwae/i+U2kc=
399399
github.com/redpanda-data/common-go/rpadmin v0.2.0 h1:s2MyyY+yq7B17mLjjW17RO81wFlzo856K9IuBpsmvv0=
400400
github.com/redpanda-data/common-go/rpadmin v0.2.0/go.mod h1:qmu76v7RRKgEXLS3UXxZ8KDpObtSNq6RinOIejJNWzw=
401-
github.com/redpanda-data/common-go/rpsr v0.1.2 h1:DThUeyfBH8fkL9WoP1sEbRhT2NVV22zmsTpcCtzfusQ=
402-
github.com/redpanda-data/common-go/rpsr v0.1.2/go.mod h1:2j2416onosg5FKaKz52NooRE+q/9EJqQn0kyTcTXWHc=
401+
github.com/redpanda-data/common-go/rpsr v0.1.4 h1:d9lu5q5wyhZWBYR1GnZkq+eZGKU0qoaSwwybRS9Uk2k=
402+
github.com/redpanda-data/common-go/rpsr v0.1.4/go.mod h1:qVa7b0yaCRdZDn5dcZ9CazqVr4jYbgtOJUywI2X3G3I=
403403
github.com/rickb777/period v1.0.15 h1:nWR4rgCtImT0CXw5kAsjHv+ExCEFt/18zAySOi7pWI8=
404404
github.com/rickb777/period v1.0.15/go.mod h1:3lWluyeZEk6n1jfLCPG4dH3C0N3NxjmYL4Dmcxip3es=
405405
github.com/rickb777/plural v1.4.4 h1:OpZU8uRr9P2NkYAbkLMwlKNVJyJ5HvRcRBFyXGJtKGI=
@@ -468,8 +468,8 @@ github.com/twmb/franz-go/pkg/kmsg v1.12.0 h1:CbatD7ers1KzDNgJqPbKOq0Bz/WLBdsTH75
468468
github.com/twmb/franz-go/pkg/kmsg v1.12.0/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY=
469469
github.com/twmb/franz-go/pkg/sasl/kerberos v1.1.0 h1:alKdbddkPw3rDh+AwmUEwh6HNYgTvDSFIe/GWYRR9RM=
470470
github.com/twmb/franz-go/pkg/sasl/kerberos v1.1.0/go.mod h1:k8BoBjyUbFj34f0rRbn+Ky12sZFAPbmShrg0karAIMo=
471-
github.com/twmb/franz-go/pkg/sr v1.6.0 h1:YcnD65hmdEuJljSM4O9Hldr/0oi+vrjPGHaRUuwwusA=
472-
github.com/twmb/franz-go/pkg/sr v1.6.0/go.mod h1:64CsHlsQnyFRq1sYPcCmlRrEG3PlLPb6cDddx2wGr28=
471+
github.com/twmb/franz-go/pkg/sr v1.7.0 h1:wHStlO6aOPWWgZ68ZYcdtQe9tRbkcTc1gRLbgs+8QAA=
472+
github.com/twmb/franz-go/pkg/sr v1.7.0/go.mod h1:64CsHlsQnyFRq1sYPcCmlRrEG3PlLPb6cDddx2wGr28=
473473
github.com/twmb/franz-go/plugin/kslog v1.0.0 h1:I64oEmF+0PDvmyLgwrlOtg4mfpSE9GwlcLxM4af2t60=
474474
github.com/twmb/franz-go/plugin/kslog v1.0.0/go.mod h1:8pMjK3OJJJNNYddBSbnXZkIK5dCKFIk9GcVVCDgvnQc=
475475
github.com/twmb/go-cache v1.2.1 h1:yUkLutow4S2x5NMbqFW24o14OsucoFI5Fzmlb6uBinM=
@@ -572,8 +572,8 @@ golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE
572572
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
573573
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
574574
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
575-
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
576-
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
575+
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
576+
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
577577
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
578578
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
579579
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

backend/pkg/api/handle_schema_registry.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,26 @@ func (api *API) handleDeleteSchemaRegistrySubjectMode() http.HandlerFunc {
423423
}
424424
}
425425

426+
func (api *API) handleGetSchemaRegistryContexts() http.HandlerFunc {
427+
if !api.Cfg.SchemaRegistry.Enabled {
428+
return api.handleSchemaRegistryNotConfigured()
429+
}
430+
431+
return func(w http.ResponseWriter, r *http.Request) {
432+
contexts, err := api.ConsoleSvc.GetSchemaRegistryContexts(r.Context())
433+
if err != nil {
434+
rest.SendRESTError(w, r, api.Logger, &rest.Error{
435+
Err: err,
436+
Status: http.StatusBadGateway,
437+
Message: fmt.Sprintf("Failed to retrieve contexts from the schema registry: %v", err.Error()),
438+
IsSilent: false,
439+
})
440+
return
441+
}
442+
rest.SendResponse(w, r, api.Logger, http.StatusOK, contexts)
443+
}
444+
}
445+
426446
func (api *API) handleGetSchemaSubjects() http.HandlerFunc {
427447
if !api.Cfg.SchemaRegistry.Enabled {
428448
return api.handleSchemaRegistryNotConfigured()

backend/pkg/api/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,7 @@ func (api *API) routes() *chi.Mux {
643643
r.Get("/schema-registry/config/{subject}", api.handleGetSchemaRegistrySubjectConfig())
644644
r.Put("/schema-registry/config/{subject}", api.handlePutSchemaRegistrySubjectConfig())
645645
r.Delete("/schema-registry/config/{subject}", api.handleDeleteSchemaRegistrySubjectConfig())
646+
r.Get("/schema-registry/contexts", api.handleGetSchemaRegistryContexts())
646647
r.Get("/schema-registry/subjects", api.handleGetSchemaSubjects())
647648
r.Get("/schema-registry/schemas/types", api.handleGetSchemaRegistrySchemaTypes())
648649
r.Get("/schema-registry/schemas/ids/{id}/versions", api.handleGetSchemaUsagesByID())

backend/pkg/console/schema_registry.go

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ type SchemaRegistrySubject struct {
4444
IsSoftDeleted bool `json:"isSoftDeleted"`
4545
}
4646

47+
// SchemaRegistryContext represents a schema registry context along with
48+
// its mode and compatibility settings.
49+
type SchemaRegistryContext struct {
50+
Name string `json:"name"`
51+
Mode string `json:"mode"`
52+
Compatibility string `json:"compatibility"`
53+
}
54+
4755
// For Schema Registry compatibility level and mode we have 2 custom responses:
4856
// - DEFAULT: there is no per-subject configuration set.
4957
// - UNKNOWN: there is an error, and we are unable to get the configuration.
@@ -674,24 +682,27 @@ func (s *Service) CreateSchemaRegistrySchema(ctx context.Context, subjectName st
674682
ctx = sr.WithParams(ctx, sr.Normalize)
675683
}
676684

677-
subjectSchema, err := srClient.CreateSchema(ctx, subjectName, schema)
685+
// Use RegisterSchema instead of CreateSchema to avoid a follow-up
686+
// SchemaUsagesByID call that fails for named contexts (schema IDs
687+
// are context-scoped, but the lookup doesn't include context).
688+
schemaID, err := srClient.RegisterSchema(ctx, subjectName, schema, -1, -1)
678689
if err != nil {
679690
// If metadata was included and we got a parse error, retry without metadata.
680691
// Older Redpanda versions don't support the metadata field.
681692
if schema.SchemaMetadata != nil {
682693
s.logger.WarnContext(ctx, "retrying schema creation without metadata (unsupported by this Redpanda version)",
683694
slog.String("subject", subjectName))
684695
schema.SchemaMetadata = nil
685-
subjectSchema, err = srClient.CreateSchema(ctx, subjectName, schema)
696+
schemaID, err = srClient.RegisterSchema(ctx, subjectName, schema, -1, -1)
686697
if err != nil {
687698
return nil, err
688699
}
689-
return &CreateSchemaResponse{ID: subjectSchema.ID}, nil
700+
return &CreateSchemaResponse{ID: schemaID}, nil
690701
}
691702
return nil, err
692703
}
693704

694-
return &CreateSchemaResponse{ID: subjectSchema.ID}, nil
705+
return &CreateSchemaResponse{ID: schemaID}, nil
695706
}
696707

697708
// SchemaRegistrySchemaValidation is the response to a schema validation.
@@ -878,6 +889,46 @@ func (s *Service) CheckSchemaRegistryACLSupport(ctx context.Context) bool {
878889
return true
879890
}
880891

892+
// GetSchemaRegistryContexts returns all contexts available in the schema registry,
893+
// enriched with per-context mode and compatibility settings.
894+
func (s *Service) GetSchemaRegistryContexts(ctx context.Context) ([]SchemaRegistryContext, error) {
895+
srClient, err := s.schemaClientFactory.GetSchemaRegistryClient(ctx)
896+
if err != nil {
897+
return nil, err
898+
}
899+
900+
names, err := srClient.Contexts(ctx)
901+
if err != nil {
902+
return nil, err
903+
}
904+
905+
results := make([]SchemaRegistryContext, len(names))
906+
grp, grpCtx := errgroup.WithContext(ctx)
907+
grp.SetLimit(10)
908+
909+
for i, name := range names {
910+
grp.Go(func() error {
911+
// For default context ".", query with empty subject to get global values.
912+
// For named contexts, use qualified syntax :.contextName:
913+
qualifiedSubject := ""
914+
if name != "." {
915+
qualifiedSubject = ":" + name + ":"
916+
}
917+
results[i] = SchemaRegistryContext{
918+
Name: name,
919+
Mode: s.getSubjectMode(grpCtx, srClient, qualifiedSubject),
920+
Compatibility: s.getSubjectCompatibilityLevel(grpCtx, srClient, qualifiedSubject),
921+
}
922+
return nil
923+
})
924+
}
925+
926+
if err := grp.Wait(); err != nil {
927+
return nil, err
928+
}
929+
return results, nil
930+
}
931+
881932
// CheckSchemaRegistryContextsSupport checks if the Schema Registry supports
882933
// the Contexts feature. For Redpanda clusters with Admin API, it checks the
883934
// cluster config. For Kafka clusters, it probes the /contexts endpoint.
@@ -915,8 +966,7 @@ func (s *Service) CheckSchemaRegistryContextsSupport(ctx context.Context) bool {
915966
return false
916967
}
917968

918-
var contexts []string
919-
err = srClient.Do(ctx, http.MethodGet, "/contexts", nil, &contexts)
969+
_, err = srClient.Contexts(ctx)
920970
if err != nil {
921971
var se *sr.ResponseError
922972
if errors.As(err, &se) && se.StatusCode == http.StatusNotFound {

backend/pkg/console/servicer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ type SchemaRegistryServicer interface {
109109
CreateSchemaRegistrySchema(ctx context.Context, subjectName string, schema sr.Schema, params CreateSchemaRequestParams) (*CreateSchemaResponse, error)
110110
ValidateSchemaRegistrySchema(ctx context.Context, subjectName string, version int, schema sr.Schema) (*SchemaRegistrySchemaValidation, error)
111111
GetSchemaUsagesByID(ctx context.Context, schemaID int, subject string) ([]SchemaVersion, error)
112+
GetSchemaRegistryContexts(ctx context.Context) ([]SchemaRegistryContext, error)
112113

113114
// Custom Redpanda-only methods for managing ACLs within the schema registry.
114115

frontend/bun.lock

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
"@hono/node-server": "^1.19.10",
5353
"lodash": "^4.17.23",
5454
"lodash-es": "^4.17.23",
55-
"mdast-util-to-hast": "^13.2.1",
5655
"qs": "^6.14.2"
5756
},
5857
"dependencies": {

frontend/src/components/layout/header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ function AppPageHeader() {
113113
className={cn('mr-2', lastBreadcrumb.options?.canBeTruncated ? 'break-spaces break-all' : 'nowrap')}
114114
level={1}
115115
>
116-
{lastBreadcrumb.title}
116+
{lastBreadcrumb.titleNode ?? lastBreadcrumb.title}
117117
</Heading>
118118
) : null}
119119
{lastBreadcrumb ? (
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright 2026 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
import { Text } from 'components/redpanda-ui/components/typography';
13+
14+
import PageContent from '../../misc/page-content';
15+
16+
export function ContextsNotSupportedPage() {
17+
return (
18+
<PageContent>
19+
<div className="flex flex-col items-center gap-4" data-testid="contexts-not-supported">
20+
<Text className="font-bold text-lg">Not Supported</Text>
21+
<Text className="text-center">Schema Registry contexts are not supported in this cluster.</Text>
22+
</div>
23+
</PageContent>
24+
);
25+
}

0 commit comments

Comments
 (0)