Skip to content

Commit 6308e16

Browse files
committed
rpk/ai: don't require a cloud cluster to run the ai plugin
`rpk ai <cmd>` died with unable to prepare rpk ai invocation: no cluster selected for this rpk profile; run 'rpk cloud cluster use <id>' or pass --rpai-endpoint whenever no rpk cloud cluster was selected. Before exec'ing the plugin, the managed-plugin hook resolved the AI Gateway endpoint from the selected cloud cluster and injected it (plus the cached cloud token) as RPAI_ENDPOINT / RPAI_TOKEN, and hard-failed when there was no cluster to resolve from. That injection is dead weight. The rpk ai plugin is self-contained: it has its own login (`rpk ai auth login`) and its own environment selection (`rpk ai env use`), and in plugin mode it deliberately ignores an ambient RPAI_ENDPOINT and RPAI_TOKEN. So rpk was computing two values the plugin throws away; the only observable effect left was a gate that blocked the plugin from ever starting without a selected cluster -- including `rpk ai auth login`, the one command whose whole job is to establish that context in the first place. The gate also fired inconsistently (it depended on which dispatch path a given invocation took), which is why it reproduced for some users and not others. Drop the injection entirely. rpk now only strips its own global flags and execs the plugin, which owns auth and endpoint selection end to end.
1 parent b290c75 commit 6308e16

4 files changed

Lines changed: 10 additions & 405 deletions

File tree

src/go/rpk/pkg/cli/ai/BUILD

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ go_library(
1919
"//src/go/rpk/pkg/osutil",
2020
"//src/go/rpk/pkg/out",
2121
"//src/go/rpk/pkg/plugin",
22-
"//src/go/rpk/pkg/publicapi",
2322
"//src/go/rpk/pkg/redpanda",
2423
"@com_github_spf13_afero//:afero",
2524
"@com_github_spf13_cobra//:cobra",
@@ -37,10 +36,8 @@ go_test(
3736
embed = [":ai"],
3837
deps = [
3938
"//src/go/rpk/pkg/config",
40-
"@build_buf_gen_go_redpandadata_cloud_protocolbuffers_go//redpanda/api/controlplane/v1:controlplane",
4139
"@com_github_spf13_afero//:afero",
4240
"@com_github_spf13_cobra//:cobra",
4341
"@com_github_stretchr_testify//require",
44-
"@org_golang_google_protobuf//proto",
4542
],
4643
)

src/go/rpk/pkg/cli/ai/ai.go

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,15 @@ import (
2727

2828
func init() {
2929
// Whenever a `rpk ai <subcommand>` managed-plugin leaf is dispatched,
30-
// inject the plugin's token/endpoint env vars and strip rpk global
31-
// flags before the child process is exec'd. Reaching this wrapper
32-
// means cobra already routed to a real plugin leaf, so unless the user
33-
// asked for --help / --version on the leaf itself (in which case the
34-
// plugin renders its own local help), we always need cloud context —
35-
// regardless of whether the leaf takes positional args.
36-
plugin.RegisterManaged(rpaiPluginSlug, []string{"ai"}, func(cmd *cobra.Command, fs afero.Fs, p *config.Params) *cobra.Command {
30+
// strip rpk's global flags before the child process is exec'd. rpk does
31+
// not inject any cloud context: the rpk ai plugin owns its own login
32+
// (`rpk ai auth login`) and environment selection (`rpk ai env use`), so
33+
// it runs without a selected rpk cloud cluster.
34+
plugin.RegisterManaged(rpaiPluginSlug, []string{"ai"}, func(cmd *cobra.Command, _ afero.Fs, p *config.Params) *cobra.Command {
3735
run := cmd.Run
3836
cmd.Run = func(cmd *cobra.Command, args []string) {
3937
pluginArgs, err := parseFlags(p, cmd, args)
4038
out.MaybeDie(err, "unable to prepare rpk ai invocation: %v", err)
41-
if !skipCloudForHelp(pluginArgs) {
42-
err = resolveAndInjectEnv(cmd.Context(), fs, p, pluginArgs)
43-
out.MaybeDie(err, "unable to prepare rpk ai invocation: %v", err)
44-
}
4539
run(cmd, pluginArgs)
4640
}
4741
return cmd
@@ -83,11 +77,6 @@ func NewCommand(fs afero.Fs, p *config.Params, execFn func(string, []string) err
8377
return
8478
}
8579

86-
if !skipCloudForHelp(pluginArgs) {
87-
err = resolveAndInjectEnv(cmd.Context(), fs, p, pluginArgs)
88-
out.MaybeDie(err, "unable to prepare rpk ai invocation: %v", err)
89-
}
90-
9180
var pluginPath string
9281
if !pluginExists {
9382
// FIPS is gated here, after the help/version short-circuits,

src/go/rpk/pkg/cli/ai/hook.go

Lines changed: 5 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -10,93 +10,21 @@
1010
package ai
1111

1212
import (
13-
"context"
14-
"fmt"
15-
"os"
1613
"slices"
17-
"strings"
1814

1915
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/cobraext"
2016
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/config"
21-
"github.com/redpanda-data/redpanda/src/go/rpk/pkg/publicapi"
22-
"github.com/spf13/afero"
2317
"github.com/spf13/cobra"
2418
"go.uber.org/zap"
2519
)
2620

27-
// The rpk ai plugin reads the following environment variables and flag:
28-
//
29-
// - envAuthToken: bearer token forwarded as Authorization to the AI
30-
// Gateway. rpk reads the cached token off the active cloud profile
31-
// and exports it; a missing or stale token is reported by the
32-
// gateway as a 401, at which point the user runs `rpk cloud login`.
33-
// - envEndpoint: AI Gateway v2 base URL. rpk resolves it from the
34-
// active cloud profile's cached AIGatewayURL (or falls back to a
35-
// publicapi lookup) and exports it before exec.
36-
// - flagEndpoint: same intent as envEndpoint, but on the command
37-
// line. rpk only watches for it so we can skip the cluster lookup;
38-
// the flag is parsed and consumed by the plugin itself.
39-
//
40-
// Any explicit value the user supplies (env or flag) wins — rpk only
41-
// fills in missing pieces.
42-
const (
43-
envAuthToken = "RPAI_TOKEN"
44-
envEndpoint = "RPAI_ENDPOINT"
45-
46-
flagEndpoint = "rpai-endpoint"
47-
)
48-
49-
// Exports RPAI_TOKEN and RPAI_ENDPOINT for the child plugin using cached
50-
// values from rpk.yaml and the profile's AIGatewayURL (no OAuth refresh).
51-
// Skips env var writes if the vars are already set or --rpai-endpoint is
52-
// present.
53-
func resolveAndInjectEnv(ctx context.Context, fs afero.Fs, p *config.Params, pluginArgs []string) error {
54-
cfg, err := p.Load(fs)
55-
if err != nil {
56-
return fmt.Errorf("unable to load rpk config: %w", err)
57-
}
58-
59-
if os.Getenv(envAuthToken) == "" {
60-
// Mirrors the lookup chain in oauth.LoadCloudToken (without the
61-
// refresh) so --config, -X cloud_auth.*, and RPK_PROFILE all work.
62-
auth := cfg.VirtualProfile().VirtualAuth()
63-
if auth == nil {
64-
auth = cfg.VirtualRpkYaml().CurrentAuth()
65-
}
66-
if auth != nil && auth.AuthToken != "" {
67-
if err := os.Setenv(envAuthToken, auth.AuthToken); err != nil {
68-
return fmt.Errorf("unable to set %s: %w", envAuthToken, err)
69-
}
70-
}
71-
}
72-
73-
if os.Getenv(envEndpoint) == "" && !hasEndpointFlag(pluginArgs) {
74-
endpoint, err := resolveAigwEndpoint(ctx, cfg)
75-
if err != nil {
76-
return err
77-
}
78-
if err := os.Setenv(envEndpoint, endpoint); err != nil {
79-
return fmt.Errorf("unable to set %s: %w", envEndpoint, err)
80-
}
81-
}
82-
83-
return nil
84-
}
85-
86-
// skipCloudForHelp reports whether a --help / -h / --version flag is present,
87-
// in which case we must not reach out to the cloud API or trigger OAuth. The
88-
// rpk ai plugin child process renders its own help/version output locally.
89-
func skipCloudForHelp(args []string) bool {
90-
for _, a := range args {
91-
if a == "--help" || a == "-h" || a == "--version" {
92-
return true
93-
}
94-
}
95-
return false
96-
}
97-
9821
// parseFlags splits args into plugin args + rpk-global-flags consumed by rpk,
9922
// and parses the rpk-global-flags so the logger and config loader pick them up.
23+
//
24+
// rpk does not inject any cloud context (token or endpoint) into the plugin:
25+
// the rpk ai plugin owns its own login (`rpk ai auth login`) and environment
26+
// selection (`rpk ai env use`), so it runs without a selected rpk cloud
27+
// cluster. Everything except rpk's own globals is forwarded untouched.
10028
func parseFlags(p *config.Params, cmd *cobra.Command, args []string) ([]string, error) {
10129
f := cmd.Flags()
10230

@@ -114,44 +42,3 @@ func parseFlags(p *config.Params, cmd *cobra.Command, args []string) ([]string,
11442
}
11543
return keepForPlugin, nil
11644
}
117-
118-
// hasEndpointFlag reports whether the plugin args carry an explicit endpoint
119-
// flag (in any supported form: --flag=value or --flag followed by its
120-
// value).
121-
func hasEndpointFlag(args []string) bool {
122-
prefix := "--" + flagEndpoint
123-
for _, a := range args {
124-
if a == prefix || strings.HasPrefix(a, prefix+"=") {
125-
return true
126-
}
127-
}
128-
return false
129-
}
130-
131-
// resolveAigwEndpoint returns the AI Gateway v2 URL for the active rpk cloud
132-
// profile's cluster. It prefers the value cached on the profile at creation
133-
// time and only falls back to a live publicapi lookup when the cache is empty
134-
// (older profiles created before AIGatewayURL existed, or profiles whose
135-
// cluster had no AI Gateway attached at creation but does now).
136-
func resolveAigwEndpoint(ctx context.Context, cfg *config.Config) (string, error) {
137-
prof := cfg.VirtualProfile()
138-
if prof == nil || !prof.FromCloud || prof.CloudCluster.ClusterID == "" {
139-
return "", fmt.Errorf("no cluster selected for this rpk profile; run 'rpk cloud cluster use <id>' or pass --%s", flagEndpoint)
140-
}
141-
if prof.CloudCluster.AIGatewayURL != "" {
142-
return prof.CloudCluster.AIGatewayURL, nil
143-
}
144-
clusterID := prof.CloudCluster.ClusterID
145-
146-
token := os.Getenv(envAuthToken)
147-
cl := publicapi.NewCloudClientSet(cfg.DevOverrides().PublicAPIURL, token)
148-
cluster, err := cl.ClusterForID(ctx, clusterID)
149-
if err != nil {
150-
return "", fmt.Errorf("unable to resolve aigw endpoint for cluster %s: %w", clusterID, err)
151-
}
152-
endpoint := cluster.GetAiGateway().GetV2Url()
153-
if endpoint == "" {
154-
return "", fmt.Errorf("cluster %s does not have an AI Gateway v2 endpoint; pick a cluster that does, or pass --%s", clusterID, flagEndpoint)
155-
}
156-
return endpoint, nil
157-
}

0 commit comments

Comments
 (0)