Skip to content

Commit 4cc6d1b

Browse files
author
Shetty Gaurav Jagadeesha Shetty
committed
feat: add get command for ODH/RHOAI resources
Support listing notebooks, inferenceservices, servingruntimes, and datasciencepipelinesapplications with table/JSON/YAML output formats.
1 parent 765c061 commit 4cc6d1b

File tree

7 files changed

+960
-0
lines changed

7 files changed

+960
-0
lines changed

cmd/get/get.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package get
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"k8s.io/cli-runtime/pkg/genericclioptions"
7+
"k8s.io/cli-runtime/pkg/genericiooptions"
8+
9+
getpkg "github.com/opendatahub-io/odh-cli/pkg/get"
10+
clierrors "github.com/opendatahub-io/odh-cli/pkg/util/errors"
11+
)
12+
13+
const (
14+
cmdName = "get"
15+
cmdShort = "Get ODH/RHOAI resources"
16+
maxArgs = 2
17+
)
18+
19+
const cmdLong = `
20+
Display one or more ODH/RHOAI resources without knowing CRD group/version details.
21+
22+
Supported resources:
23+
notebooks (nb) Kubeflow Notebooks
24+
inferenceservices (isvc) KServe InferenceServices
25+
servingruntimes (sr) KServe ServingRuntimes
26+
datasciencepipelinesapplications (pipeline) Data Science Pipelines
27+
28+
Table output shows ODH-relevant columns tuned for each resource type.
29+
If the CRD is not installed, a friendly message is shown instead of an error.
30+
`
31+
32+
const cmdExample = `
33+
# List notebooks in the current namespace
34+
kubectl odh get nb
35+
36+
# List inference services across all namespaces
37+
kubectl odh get isvc -A
38+
39+
# Get a specific notebook in a namespace
40+
kubectl odh get nb my-notebook -n my-project
41+
42+
# List serving runtimes with label filter
43+
kubectl odh get sr -l app=my-model
44+
45+
# List pipelines as JSON
46+
kubectl odh get pipeline -o json
47+
`
48+
49+
// AddCommand adds the get command to the root command.
50+
func AddCommand(root *cobra.Command, flags *genericclioptions.ConfigFlags) {
51+
streams := genericiooptions.IOStreams{
52+
In: root.InOrStdin(),
53+
Out: root.OutOrStdout(),
54+
ErrOut: root.ErrOrStderr(),
55+
}
56+
57+
command := getpkg.NewCommand(streams, flags)
58+
59+
cmd := &cobra.Command{
60+
Use: "get RESOURCE [NAME]",
61+
Short: cmdShort,
62+
Long: cmdLong,
63+
Example: cmdExample,
64+
SilenceUsage: true,
65+
SilenceErrors: true,
66+
Args: cobra.RangeArgs(1, maxArgs),
67+
ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
68+
if len(args) == 0 {
69+
return getpkg.Names(), cobra.ShellCompDirectiveNoFileComp
70+
}
71+
72+
return nil, cobra.ShellCompDirectiveNoFileComp
73+
},
74+
RunE: func(cmd *cobra.Command, args []string) error {
75+
command.ResourceName = args[0]
76+
if len(args) > 1 {
77+
command.ItemName = args[1]
78+
}
79+
80+
if err := command.Complete(); err != nil {
81+
clierrors.WriteTextError(cmd.ErrOrStderr(), err)
82+
83+
return clierrors.NewAlreadyHandledError(err)
84+
}
85+
86+
if err := command.Validate(); err != nil {
87+
clierrors.WriteTextError(cmd.ErrOrStderr(), err)
88+
89+
return clierrors.NewAlreadyHandledError(err)
90+
}
91+
92+
if err := command.Run(cmd.Context()); err != nil {
93+
clierrors.WriteTextError(cmd.ErrOrStderr(), err)
94+
95+
return clierrors.NewAlreadyHandledError(err)
96+
}
97+
98+
return nil
99+
},
100+
}
101+
102+
command.AddFlags(cmd.Flags())
103+
104+
root.AddCommand(cmd)
105+
}

cmd/main.go

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

99
"k8s.io/cli-runtime/pkg/genericclioptions"
1010

11+
"github.com/opendatahub-io/odh-cli/cmd/get"
1112
"github.com/opendatahub-io/odh-cli/cmd/lint"
1213
"github.com/opendatahub-io/odh-cli/cmd/version"
1314
clierrors "github.com/opendatahub-io/odh-cli/pkg/util/errors"
@@ -29,6 +30,7 @@ func main() {
2930

3031
version.AddCommand(cmd, flags)
3132
lint.AddCommand(cmd, flags)
33+
get.AddCommand(cmd, flags)
3234

3335
if err := cmd.Execute(); err != nil {
3436
if !errors.Is(err, clierrors.ErrAlreadyHandled) {

pkg/get/command.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package get
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/spf13/pflag"
9+
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
11+
"k8s.io/apimachinery/pkg/api/meta"
12+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13+
"k8s.io/cli-runtime/pkg/genericclioptions"
14+
"k8s.io/cli-runtime/pkg/genericiooptions"
15+
16+
"github.com/opendatahub-io/odh-cli/pkg/cmd"
17+
"github.com/opendatahub-io/odh-cli/pkg/resources"
18+
"github.com/opendatahub-io/odh-cli/pkg/util/client"
19+
"github.com/opendatahub-io/odh-cli/pkg/util/iostreams"
20+
)
21+
22+
var _ cmd.Command = (*Command)(nil)
23+
24+
const (
25+
outputFormatTable = "table"
26+
outputFormatJSON = "json"
27+
outputFormatYAML = "yaml"
28+
29+
msgCRDNotInstalled = "resource type %q is not installed on this cluster\n"
30+
)
31+
32+
// Command contains the get command configuration.
33+
type Command struct {
34+
IO iostreams.Interface
35+
ConfigFlags *genericclioptions.ConfigFlags
36+
Client client.Client
37+
38+
// ResourceName is the positional arg identifying the resource type (e.g. "nb").
39+
ResourceName string
40+
// ItemName is the optional positional arg for a specific resource name.
41+
ItemName string
42+
43+
// Namespace is the resolved target namespace (populated during Complete).
44+
Namespace string
45+
// AllNamespaces lists resources across all namespaces when true.
46+
AllNamespaces bool
47+
// LabelSelector filters resources by label (e.g. "app=my-model").
48+
LabelSelector string
49+
// OutputFormat controls output rendering: table, json, or yaml.
50+
OutputFormat string
51+
52+
// ResolvedType is the ResourceType resolved from ResourceName during Complete.
53+
ResolvedType resources.ResourceType
54+
}
55+
56+
// NewCommand creates a new get Command with defaults.
57+
func NewCommand(
58+
streams genericiooptions.IOStreams,
59+
configFlags *genericclioptions.ConfigFlags,
60+
) *Command {
61+
return &Command{
62+
IO: iostreams.NewIOStreams(streams.In, streams.Out, streams.ErrOut),
63+
ConfigFlags: configFlags,
64+
OutputFormat: outputFormatTable,
65+
}
66+
}
67+
68+
// AddFlags registers command-specific flags.
69+
// Note: -n/--namespace is already registered by ConfigFlags on the root command.
70+
func (c *Command) AddFlags(fs *pflag.FlagSet) {
71+
fs.BoolVarP(&c.AllNamespaces, "all-namespaces", "A", false, "List resources across all namespaces")
72+
fs.StringVarP(&c.LabelSelector, "selector", "l", "", "Label selector to filter resources (e.g. app=my-model)")
73+
fs.StringVarP(&c.OutputFormat, "output", "o", outputFormatTable, "Output format: table, json, or yaml")
74+
}
75+
76+
// Complete resolves derived fields after flag parsing.
77+
func (c *Command) Complete() error {
78+
rt, err := Resolve(c.ResourceName)
79+
if err != nil {
80+
return err
81+
}
82+
83+
c.ResolvedType = rt
84+
85+
if !c.AllNamespaces {
86+
ns, _, err := c.ConfigFlags.ToRawKubeConfigLoader().Namespace()
87+
if err != nil {
88+
return fmt.Errorf("determining namespace: %w", err)
89+
}
90+
91+
c.Namespace = ns
92+
}
93+
94+
k8sClient, err := client.NewClient(c.ConfigFlags)
95+
if err != nil {
96+
return fmt.Errorf("creating Kubernetes client: %w", err)
97+
}
98+
99+
c.Client = k8sClient
100+
101+
return nil
102+
}
103+
104+
// Validate checks that all options are valid before execution.
105+
func (c *Command) Validate() error {
106+
if c.AllNamespaces && c.ConfigFlags.Namespace != nil && *c.ConfigFlags.Namespace != "" {
107+
return errors.New("--all-namespaces and --namespace are mutually exclusive")
108+
}
109+
110+
switch c.OutputFormat {
111+
case outputFormatTable, outputFormatJSON, outputFormatYAML:
112+
default:
113+
return fmt.Errorf("invalid output format %q (must be one of: table, json, yaml)", c.OutputFormat)
114+
}
115+
116+
return nil
117+
}
118+
119+
// Run executes the get command.
120+
func (c *Command) Run(ctx context.Context) error {
121+
if c.ItemName != "" {
122+
return c.getResource(ctx)
123+
}
124+
125+
return c.listResources(ctx)
126+
}
127+
128+
// listResources lists resources of the resolved type.
129+
func (c *Command) listResources(ctx context.Context) error {
130+
opts := []client.ListResourcesOption{}
131+
if c.Namespace != "" {
132+
opts = append(opts, client.WithNamespace(c.Namespace))
133+
}
134+
135+
if c.LabelSelector != "" {
136+
opts = append(opts, client.WithLabelSelector(c.LabelSelector))
137+
}
138+
139+
items, err := c.Client.List(ctx, c.ResolvedType, opts...)
140+
if err != nil {
141+
if client.IsResourceTypeNotFound(err) {
142+
c.IO.Fprintf(msgCRDNotInstalled, c.ResolvedType.Kind)
143+
144+
return nil
145+
}
146+
147+
if !client.IsPermissionError(err) {
148+
return fmt.Errorf("listing %s: %w", c.ResolvedType.Kind, err)
149+
}
150+
151+
c.IO.Errorf("Warning: insufficient permissions to list %s", c.ResolvedType.Kind)
152+
items = []*unstructured.Unstructured{}
153+
}
154+
155+
return c.renderOutput(items)
156+
}
157+
158+
// getResource retrieves a single named resource.
159+
func (c *Command) getResource(ctx context.Context) error {
160+
opts := []client.GetOption{}
161+
if c.Namespace != "" {
162+
opts = append(opts, client.InNamespace(c.Namespace))
163+
}
164+
165+
item, err := c.Client.GetResource(ctx, c.ResolvedType, c.ItemName, opts...)
166+
if err != nil {
167+
if meta.IsNoMatchError(err) {
168+
c.IO.Fprintf(msgCRDNotInstalled, c.ResolvedType.Kind)
169+
170+
return nil
171+
}
172+
173+
if apierrors.IsNotFound(err) {
174+
return fmt.Errorf("%s %q not found in namespace %q", c.ResolvedType.Kind, c.ItemName, c.Namespace)
175+
}
176+
177+
return fmt.Errorf("getting %s %q: %w", c.ResolvedType.Kind, c.ItemName, err)
178+
}
179+
180+
if item == nil {
181+
c.IO.Errorf("Warning: insufficient permissions to get %s %q", c.ResolvedType.Kind, c.ItemName)
182+
183+
return nil
184+
}
185+
186+
return c.renderOutput([]*unstructured.Unstructured{item})
187+
}
188+
189+
// renderOutput dispatches to the appropriate output formatter.
190+
func (c *Command) renderOutput(items []*unstructured.Unstructured) error {
191+
switch c.OutputFormat {
192+
case outputFormatJSON:
193+
return outputJSON(c.IO.Out(), items)
194+
case outputFormatYAML:
195+
return outputYAML(c.IO.Out(), items)
196+
default:
197+
return outputTable(c.IO.Out(), items, c.ResolvedType, c.AllNamespaces)
198+
}
199+
}

0 commit comments

Comments
 (0)