Skip to content

Commit 5ed16da

Browse files
Merge branch 'v26.1.x' into backport-pr-29970-v26.1.x-827
2 parents 1eb21d9 + 17bb771 commit 5ed16da

71 files changed

Lines changed: 3946 additions & 1356 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bazel/pbgen/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@ func (g *headerGenerator) generateMessage(msg protoreflect.MessageDescriptor, w
635635
}
636636
w.Println()
637637
w.Printf("std::string_view full_name() const override { return %q; }\n", msg.FullName())
638+
w.Printf("static constexpr size_t field_count = %d;\n", msg.Fields().Len())
638639
w.Println("// Convert a field path into a path of field numbers.")
639640
w.Println("static bool convert_field_path_to_numbers(std::span<std::string_view> field_path, std::vector<int32_t>* out);")
640641
w.Println("// Convert a field path into a path of field numbers.")

proto/redpanda/core/admin/internal/cloud_topics/v1/metastore.proto

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,27 @@ service MetastoreService {
8888
authz: SUPERUSER
8989
};
9090
}
91+
92+
// List cloud topics from the topic table, returning their names, UUIDs,
93+
// and partition counts. Supports pagination by topic name.
94+
rpc ListCloudTopics(ListCloudTopicsRequest)
95+
returns (ListCloudTopicsResponse) {
96+
option (pbgen.rpc) = {
97+
authz: SUPERUSER
98+
};
99+
}
100+
101+
// Validate metastore state for a single partition. Checks invariants
102+
// such as extent contiguity, offset bounds, and term ordering.
103+
// Supports pagination via max_extents and resume_after_offset to
104+
// bound the work per call. Cross-page contiguity is enforced by
105+
// checking the boundary between consecutive pages.
106+
rpc ValidatePartition(ValidatePartitionRequest)
107+
returns (ValidatePartitionResponse) {
108+
option (pbgen.rpc) = {
109+
authz: SUPERUSER
110+
};
111+
}
91112
}
92113

93114
// GetOffsetsRequest is the request for looking up the offsets in the metastore
@@ -215,6 +236,7 @@ message MetadataValue {
215236
int64 next_offset = 2;
216237
int64 compaction_epoch = 3;
217238
uint64 size = 4;
239+
uint64 num_extents = 5;
218240
}
219241

220242
// Value for an extent row.
@@ -324,3 +346,96 @@ message ReadRowsResponse {
324346
// Raw key for the next page. Empty if no more rows.
325347
string next_key = 2;
326348
}
349+
350+
// ValidatePartitionRequest validates metastore invariants for a single
351+
// partition. Supports paginated validation via max_extents and
352+
// resume_at_offset.
353+
message ValidatePartitionRequest {
354+
// The topic UUID to validate.
355+
string topic_id = 1;
356+
// The partition ID to validate.
357+
int32 partition_id = 2;
358+
// If true, verify each extent's object_id exists in the metastore
359+
// object table and is not a preregistration.
360+
bool check_object_metadata = 3;
361+
// If true, verify each extent's L1 object exists in cloud storage
362+
// via HEAD request.
363+
bool check_object_storage = 4;
364+
// Resume validation at this base_offset (inclusive). On the next
365+
// page, the validator re-reads the extent at this offset to verify
366+
// cross-page contiguity, then continues forward. The re-read extent
367+
// is not counted toward max_extents. Omit to start from the
368+
// beginning of the partition.
369+
optional int64 resume_at_offset = 5;
370+
// Max extents to validate per call (excluding the re-read extent on
371+
// continuation pages). 0 means validate all remaining extents.
372+
uint32 max_extents = 6;
373+
}
374+
375+
// ValidatePartitionResponse contains the results of partition validation.
376+
message ValidatePartitionResponse {
377+
// Anomalies found during validation. Empty if the partition is valid.
378+
repeated MetastoreAnomaly anomalies = 1;
379+
// The base_offset of the last extent validated. If absent, all
380+
// extents have been validated. Pass this value as resume_at_offset
381+
// in the next call to continue validation.
382+
optional int64 resume_at_offset = 2;
383+
// Number of extents validated in this call (excluding the re-read
384+
// extent on continuation pages).
385+
uint32 extents_validated = 3;
386+
}
387+
388+
// Type of anomaly found during metastore validation.
389+
enum AnomalyType {
390+
ANOMALY_TYPE_UNSPECIFIED = 0;
391+
ANOMALY_TYPE_EXTENT_OVERLAP = 1;
392+
ANOMALY_TYPE_EXTENT_GAP = 2;
393+
ANOMALY_TYPE_NEXT_OFFSET_MISMATCH = 3;
394+
ANOMALY_TYPE_START_OFFSET_MISMATCH = 4;
395+
ANOMALY_TYPE_OBJECT_NOT_FOUND = 5;
396+
ANOMALY_TYPE_OBJECT_PREREGISTERED = 6;
397+
ANOMALY_TYPE_OBJECT_NOT_IN_STORAGE = 7;
398+
ANOMALY_TYPE_COMPACTION_RANGE_BELOW_START = 8;
399+
ANOMALY_TYPE_COMPACTION_RANGE_ABOVE_NEXT = 9;
400+
ANOMALY_TYPE_COMPACTION_TOMBSTONE_OVERLAP = 10;
401+
ANOMALY_TYPE_COMPACTION_TOMBSTONE_OUTSIDE_CLEANED = 11;
402+
ANOMALY_TYPE_TERM_ORDERING = 12;
403+
ANOMALY_TYPE_COMPACTION_STATE_UNEXPECTED = 13;
404+
}
405+
406+
// A single anomaly found during metastore validation.
407+
message MetastoreAnomaly {
408+
// The type of anomaly.
409+
AnomalyType anomaly_type = 1;
410+
// Human-readable description of the anomaly.
411+
string description = 2;
412+
}
413+
414+
// ListCloudTopicsRequest lists cloud topics from the cluster's topic table
415+
// (not the metastore). Hosted here for convenience since the metastore
416+
// admin service already has topic_table access.
417+
message ListCloudTopicsRequest {
418+
// Optional: resume listing after this topic name (exclusive).
419+
// Omit to start from the beginning.
420+
string after_topic_name = 1;
421+
// Maximum number of topics to return. 0 uses a default of 100.
422+
uint32 max_topics = 2;
423+
}
424+
425+
// ListCloudTopicsResponse contains cloud topics with their metadata.
426+
message ListCloudTopicsResponse {
427+
repeated CloudTopicInfo topics = 1;
428+
// If true, more topics are available. Call again with after_topic_name
429+
// set to the last topic's name to continue.
430+
bool has_more = 2;
431+
}
432+
433+
// Information about a single cloud topic.
434+
message CloudTopicInfo {
435+
// The Kafka topic name.
436+
string topic_name = 1;
437+
// The topic UUID.
438+
string topic_id = 2;
439+
// Number of partitions.
440+
int32 partition_count = 3;
441+
}

src/go/rpk/pkg/cli/cloud/auth/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ go_library(
99
"edit.go",
1010
"list.go",
1111
"rename.go",
12+
"token.go",
1213
"use.go",
1314
],
1415
importpath = "github.com/redpanda-data/redpanda/src/go/rpk/pkg/cli/cloud/auth",

src/go/rpk/pkg/cli/cloud/auth/auth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ only use a single SSO based login.
3838
newEditCommand(fs, p),
3939
newListCommand(fs, p),
4040
newRenameToCommand(fs, p),
41+
newTokenCommand(fs, p),
4142
newUseCommand(fs, p),
4243
)
4344

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2025 Redpanda Data, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.md
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0
9+
10+
package auth
11+
12+
import (
13+
"fmt"
14+
15+
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/config"
16+
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/out"
17+
"github.com/spf13/afero"
18+
"github.com/spf13/cobra"
19+
)
20+
21+
func newTokenCommand(fs afero.Fs, p *config.Params) *cobra.Command {
22+
return &cobra.Command{
23+
Use: "token",
24+
Short: "Print the cloud auth token of the current profile",
25+
Long: `Print the cloud auth token of the current profile.
26+
27+
This command prints the auth token for the currently selected cloud
28+
authentication to stdout. This is useful for piping the token into other
29+
commands or scripts that need to authenticate with Redpanda Cloud.`,
30+
Args: cobra.ExactArgs(0),
31+
Run: func(_ *cobra.Command, _ []string) {
32+
cfg, err := p.Load(fs)
33+
out.MaybeDie(err, "rpk unable to load config: %v", err)
34+
35+
y, ok := cfg.ActualRpkYaml()
36+
if !ok {
37+
out.Die("rpk.yaml file does not exist")
38+
}
39+
40+
a := y.CurrentAuth()
41+
if a == nil {
42+
out.Die("no current cloud auth selected, use 'rpk cloud login' to log in")
43+
}
44+
if a.AuthToken == "" {
45+
out.Die("current auth has no token, use 'rpk cloud login' to log in")
46+
}
47+
fmt.Print(a.AuthToken)
48+
},
49+
}
50+
}

src/go/rpk/pkg/cli/security/role/BUILD

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("@rules_go//go:def.bzl", "go_library")
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
22

33
go_library(
44
name = "role",
@@ -19,8 +19,6 @@ go_library(
1919
"//src/go/rpk/pkg/kafka",
2020
"//src/go/rpk/pkg/out",
2121
"//src/go/rpk/pkg/publicapi",
22-
"//src/go/rpk/pkg/redpanda",
23-
"@build_buf_gen_go_redpandadata_core_protocolbuffers_go//redpanda/core/admin/v2:admin",
2422
"@build_buf_gen_go_redpandadata_dataplane_protocolbuffers_go//redpanda/api/dataplane/v1:dataplane",
2523
"@com_connectrpc_connect//:connect",
2624
"@com_github_redpanda_data_common_go_rpadmin//:rpadmin",
@@ -30,3 +28,10 @@ go_library(
3028
"@com_github_twmb_types//:types",
3129
],
3230
)
31+
32+
go_test(
33+
name = "role_test",
34+
srcs = ["role_test.go"],
35+
embed = [":role"],
36+
deps = ["@com_github_stretchr_testify//require"],
37+
)

src/go/rpk/pkg/cli/security/role/assign.go

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ package role
1212
import (
1313
"fmt"
1414

15-
adminv2 "buf.build/gen/go/redpandadata/core/protocolbuffers/go/redpanda/core/admin/v2"
1615
dataplanev1 "buf.build/gen/go/redpandadata/dataplane/protocolbuffers/go/redpanda/api/dataplane/v1"
1716
"connectrpc.com/connect"
1817
"github.com/redpanda-data/common-go/rpadmin"
@@ -26,21 +25,15 @@ import (
2625

2726
func assignCommand(fs afero.Fs, p *config.Params) *cobra.Command {
2827
var principals []string
29-
var groups []string
3028
cmd := &cobra.Command{
31-
Use: "assign [ROLE] --principal [PRINCIPALS...] --group [GROUPS...]",
29+
Use: "assign [ROLE] --principal [PRINCIPALS...]",
3230
Aliases: []string{"add"},
33-
Short: "Assign a Redpanda role to a principal or group",
34-
Long: `Assign a Redpanda role to a principal or group.
31+
Short: "Assign a Redpanda role to a principal",
32+
Long: `Assign a Redpanda role to a principal.
3533
3634
The '--principal' flag accepts principals with the format
3735
'<PrincipalPrefix>:<Principal>'. If 'PrincipalPrefix' is not provided, then
3836
defaults to 'User:'.
39-
40-
The '--group' flag assigns the role to an identity provider group, granting all members
41-
of that group the permissions associated with the role. Group assignments are only
42-
supported on local (non-cloud) clusters running
43-
Redpanda ` + minGroupVersion.String() + ` or later.
4437
`,
4538
Example: `
4639
Assign role "redpanda-admin" to user "red"
@@ -49,11 +42,8 @@ Assign role "redpanda-admin" to user "red"
4942
Assign role "redpanda-admin" to users "red" and "panda"
5043
rpk security role assign redpanda-admin --principal red,panda
5144
52-
Assign role "data-reader" to group "engineering"
53-
rpk security role assign data-reader --group engineering
54-
55-
Assign role "data-reader" to both a user and a group
56-
rpk security role assign data-reader --principal alice --group engineering
45+
Assign role "redpanda-admin" to group "pandas"
46+
rpk security role assign redpanda-admin --principal Group:pandas
5747
`,
5848
Args: cobra.ExactArgs(1),
5949
Run: func(cmd *cobra.Command, args []string) {
@@ -89,28 +79,6 @@ Assign role "data-reader" to both a user and a group
8979
}
9080
}
9181

92-
// Handle groups (local-only).
93-
if len(groups) > 0 {
94-
if prof.CheckFromCloud() {
95-
out.Die("--group is not supported for cloud clusters")
96-
}
97-
cl, err := adminapi.NewClient(cmd.Context(), fs, prof)
98-
out.MaybeDie(err, "unable to initialize admin api client: %v", err)
99-
100-
if !adminapi.HasMinimumVersion(cmd.Context(), cl, minGroupVersion) {
101-
out.Die("--group requires Redpanda version %s or later", minGroupVersion.String())
102-
}
103-
members := make([]*adminv2.RoleMember, len(groups))
104-
for i, g := range groups {
105-
members[i] = &adminv2.RoleMember{Member: &adminv2.RoleMember_Group{Group: &adminv2.RoleGroup{Name: g}}}
106-
}
107-
_, err = cl.SecurityService().AddRoleMembers(cmd.Context(), connect.NewRequest(&adminv2.AddRoleMembersRequest{
108-
RoleName: roleName,
109-
Members: members,
110-
}))
111-
out.MaybeDie(err, "unable to assign group(s) to role %q: %v", roleName, err)
112-
}
113-
11482
// Output principals.
11583
if len(toAdd) > 0 {
11684
if isText, _, s, err := f.Format(toAdd); !isText {
@@ -124,16 +92,10 @@ Assign role "data-reader" to both a user and a group
12492
tw.PrintStructFields(m)
12593
}
12694
}
127-
128-
// Output groups.
129-
if len(groups) > 0 {
130-
fmt.Printf("Successfully assigned role %q to group(s) %v\n", roleName, groups)
131-
}
13295
},
13396
}
13497

13598
cmd.Flags().StringSliceVar(&principals, "principal", nil, "Principal to assign the role to (repeatable)")
136-
cmd.Flags().StringSliceVar(&groups, "group", nil, "group to assign the role to (repeatable)")
137-
cmd.MarkFlagsOneRequired("principal", "group")
99+
cmd.MarkFlagRequired("principal")
138100
return cmd
139101
}

src/go/rpk/pkg/cli/security/role/role.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,16 @@ import (
1616
"github.com/redpanda-data/common-go/rpadmin"
1717

1818
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/config"
19-
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/redpanda"
2019
"github.com/spf13/afero"
2120
"github.com/spf13/cobra"
2221
)
2322

2423
const (
25-
rolePrefix = "RedpandaRole:"
26-
userPrefix = "User:"
24+
rolePrefix = "RedpandaRole:"
25+
userPrefix = "User:"
26+
groupPrefix = "Group:"
2727
)
2828

29-
var minGroupVersion = redpanda.Version{Major: 26, Feature: 1}
30-
3129
func NewCommand(fs afero.Fs, p *config.Params) *cobra.Command {
3230
cmd := &cobra.Command{
3331
Use: "role",
@@ -51,9 +49,13 @@ func NewCommand(fs afero.Fs, p *config.Params) *cobra.Command {
5149
// parsePrincipal returns the prefix, and principal. If no prefix is present,
5250
// returns 'User'.
5351
func parsePrincipal(p string) (principalType string, name string) {
54-
if strings.HasPrefix(p, userPrefix) {
55-
return "User", strings.TrimPrefix(p, userPrefix)
52+
if s, ok := strings.CutPrefix(p, userPrefix); ok {
53+
return "User", s
54+
}
55+
if s, ok := strings.CutPrefix(p, groupPrefix); ok {
56+
return "Group", s
5657
}
58+
5759
return "User", p
5860
}
5961

0 commit comments

Comments
 (0)