Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
49 changes: 46 additions & 3 deletions predictor/predictor.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package predictor provides shell completion predictors for nctl resources.
package predictor

import (
Expand Down Expand Up @@ -28,22 +29,27 @@ var argResourceMap = map[string]string{
"clusters": "kubernetesclusters",
}

// Resource is a predictor that completes resource names by querying the API.
type Resource struct {
client *api.Client
knownGVK *schema.GroupVersionKind
}

// NewResourceName returns a predictor that infers the resource kind from the
// command arguments.
func NewResourceName(client *api.Client) complete.Predictor {
return &Resource{client: client}
}

// NewResourceNameWithKind returns a predictor for a specific resource kind.
func NewResourceNameWithKind(client *api.Client, gvk schema.GroupVersionKind) complete.Predictor {
return &Resource{
client: client,
knownGVK: ptr.To(gvk),
}
}

// Predict returns a list of resource names for shell completion.
func (r *Resource) Predict(args complete.Args) []string {
u := &unstructured.UnstructuredList{}
if r.knownGVK != nil {
Expand All @@ -63,6 +69,16 @@ func (r *Resource) Predict(args complete.Args) []string {
return []string{}
}
ns = org
} else {
// if there is a project set in the args use this
p, incomplete := findProject(args)
if incomplete {
// user is still typing the project flag, don't complete resources
return []string{}
}
if p != "" {
ns = p
}
}

if err := r.client.List(ctx, u, client.InNamespace(ns)); err != nil {
Expand All @@ -77,6 +93,7 @@ func (r *Resource) Predict(args complete.Args) []string {
return resources
}

// findKind looks up the GroupVersionKind for a given resource argument.
func (r *Resource) findKind(arg string) schema.GroupVersionKind {
if v, ok := argResourceMap[arg]; ok {
arg = v
Expand All @@ -95,14 +112,40 @@ func (r *Resource) findKind(arg string) schema.GroupVersionKind {
return schema.GroupVersionKind{}
}

// listKindToResource converts a list kind name to its resource name.
func listKindToResource(kind string) string {
return flect.Pluralize(strings.TrimSuffix(strings.ToLower(kind), listSuffix))
}

// findProject extracts the project from the completion args. It returns the
// project name and a boolean indicating if the project flag is incomplete
// (user is still typing it).
func findProject(args complete.Args) (string, bool) {
// if the last completed argument is -p or --project, the user is still
// specifying the project, so we shouldn't complete resources yet
if args.LastCompleted == "-p" || args.LastCompleted == "--project" {
return "", true
}
if p := findProjectInSlice(args.All); p != "" {
return p, false
}
return "", false
}

// findProjectInSlice searches for -p or --project flag and returns its value.
func findProjectInSlice(args []string) string {
for i, arg := range args {
if (arg == "-p" || arg == "--project") && i+1 < len(args) {
return args[i+1]
}
Comment on lines +138 to +140
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All nice and good, but did you test it? It doesn't actually work, for the reasons I stated in https://github.com/ninech/nctl/pull/334/changes#r2721243293

The args don't include the global flags like -p / --project.

}
return ""
}

// NewClient creates an API client configured for shell completion. It uses a
// static token since dynamic exec config breaks with some shells during
// completion.
func NewClient(ctx context.Context, defaultAPICluster string) (*api.Client, error) {
// the client for the predictor requires a static token in the client config
// since dynamic exec config seems to break with some shells during completion.
// The exact reason for that is unknown.
apiCluster := defaultAPICluster
if v, ok := os.LookupEnv("NCTL_API_CLUSTER"); ok {
apiCluster = v
Expand Down
65 changes: 65 additions & 0 deletions predictor/predictor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package predictor

import "testing"

func TestFindProjectInSlice(t *testing.T) {
tests := []struct {
name string
args []string
want string
}{
{
name: "empty args",
args: []string{},
want: "",
},
{
name: "no project flag",
args: []string{"nctl", "get", "applications"},
want: "",
},
{
name: "short flag with value",
args: []string{"nctl", "-p", "myproject", "get", "applications"},
want: "myproject",
},
{
name: "long flag with value",
args: []string{"nctl", "--project", "myproject", "get", "applications"},
want: "myproject",
},
{
name: "flag at end with value",
args: []string{"nctl", "get", "applications", "-p", "myproject"},
want: "myproject",
},
{
name: "short flag without value (incomplete)",
args: []string{"nctl", "get", "applications", "-p"},
want: "",
},
{
name: "long flag without value (incomplete)",
args: []string{"nctl", "get", "applications", "--project"},
want: "",
},
{
name: "flag in middle of args",
args: []string{"nctl", "get", "-p", "proj", "applications"},
want: "proj",
},
{
name: "multiple flags takes first",
args: []string{"nctl", "-p", "first", "get", "-p", "second"},
want: "first",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := findProjectInSlice(tt.args); got != tt.want {
t.Errorf("findProjectInSlice() = %q, want %q", got, tt.want)
}
})
}
}