Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 6 additions & 1 deletion src/go/rpk/pkg/cli/security/role/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ go_library(
"list.go",
"role.go",
"unassign.go",
"validation.go",
],
importpath = "github.com/redpanda-data/redpanda/src/go/rpk/pkg/cli/security/role",
visibility = ["//visibility:public"],
Expand All @@ -26,12 +27,16 @@ go_library(
"@com_github_spf13_cobra//:cobra",
"@com_github_twmb_franz_go_pkg_kadm//:kadm",
"@com_github_twmb_types//:types",
"@io_k8s_apimachinery//pkg/util/validation",
],
)

go_test(
name = "role_test",
srcs = ["role_test.go"],
srcs = [
"role_test.go",
"validation_test.go",
],
embed = [":role"],
deps = ["@com_github_stretchr_testify//require"],
)
10 changes: 10 additions & 0 deletions src/go/rpk/pkg/cli/security/role/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package role

import (
"fmt"
"os"

dataplanev1 "buf.build/gen/go/redpandadata/dataplane/protocolbuffers/go/redpanda/api/dataplane/v1"
"connectrpc.com/connect"
Expand Down Expand Up @@ -45,6 +46,15 @@ flag in the 'rpk security acl create' command.`,
config.CheckExitServerlessAdmin(prof)

roleName := args[0]
if msgs := validateRoleNameForK8s(roleName); len(msgs) > 0 {
Comment thread
david-yu marked this conversation as resolved.
Outdated
fmt.Fprintf(os.Stderr, "Warning: role name %q is not a valid DNS-1123 subdomain (RFC 1123):\n", roleName)
for _, m := range msgs {
fmt.Fprintf(os.Stderr, " - %s\n", m)
}
fmt.Fprintln(os.Stderr, " This role cannot be adopted by a RedpandaRole CR in the Redpanda Kubernetes operator,")
fmt.Fprintln(os.Stderr, " since the operator binds the role name to the CR's metadata.name. Consider using a")
fmt.Fprintln(os.Stderr, " lowercase name (letters, digits, '-', '.') if you may migrate to operator-managed roles.")
Comment thread
david-yu marked this conversation as resolved.
Outdated
}
if prof.CheckFromCloud() {
cl, err := publicapi.DataplaneClientFromRpkProfile(prof)
out.MaybeDie(err, "unable to initialize cloud API client: %v", err)
Expand Down
27 changes: 27 additions & 0 deletions src/go/rpk/pkg/cli/security/role/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2026 Redpanda Data, Inc.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.md
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0

package role

import (
"k8s.io/apimachinery/pkg/util/validation"
)

// validateRoleNameForK8s reports whether the role name is a valid DNS-1123
// subdomain. Returns nil if the name is compliant; otherwise returns the
// validation messages from k8s.io/apimachinery so the caller can surface them
// to the user.
//
// The Redpanda Kubernetes operator binds a RedpandaRole CR's role name to its
// metadata.name, which the Kubernetes API server constrains to DNS-1123. Names
// that don't satisfy this constraint cannot be adopted by a RedpandaRole CR
// and will block migration to operator-managed roles.
func validateRoleNameForK8s(name string) []string {
return validation.IsDNS1123Subdomain(name)
}
59 changes: 59 additions & 0 deletions src/go/rpk/pkg/cli/security/role/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2026 Redpanda Data, Inc.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.md
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0

package role

import (
"strings"
"testing"
)

func TestValidateRoleNameForK8s(t *testing.T) {
for _, tc := range []struct {
name string
input string
wantOK bool
mustHave string
}{
{name: "lowercase simple", input: "my-role", wantOK: true},
{name: "lowercase with dots", input: "team.read-only", wantOK: true},
{name: "alphanumeric", input: "role01", wantOK: true},
{name: "single char", input: "a", wantOK: true},

{name: "uppercase rejected", input: "MyRole", wantOK: false, mustHave: "lower case"},
{name: "leading hyphen rejected", input: "-foo", wantOK: false},
{name: "trailing hyphen rejected", input: "foo-", wantOK: false},
{name: "underscore rejected", input: "App_Role", wantOK: false},
{name: "space rejected", input: "my role", wantOK: false},
{name: "empty rejected", input: "", wantOK: false},
{name: "too long rejected", input: strings.Repeat("a", 254), wantOK: false},
} {
t.Run(tc.name, func(t *testing.T) {
msgs := validateRoleNameForK8s(tc.input)
gotOK := len(msgs) == 0
if gotOK != tc.wantOK {
t.Fatalf("validateRoleNameForK8s(%q) compliant=%v msgs=%v; want compliant=%v",
tc.input, gotOK, msgs, tc.wantOK)
}
if tc.mustHave != "" {
found := false
for _, m := range msgs {
if strings.Contains(m, tc.mustHave) {
found = true
break
}
}
if !found {
t.Fatalf("validateRoleNameForK8s(%q) msgs=%v; expected one to contain %q",
tc.input, msgs, tc.mustHave)
}
}
})
}
}
Loading