Skip to content

Commit

Permalink
feat: add dynamic completion
Browse files Browse the repository at this point in the history
This adds initial support for dynamic completion. For example `nctl update
application <tab><tab>` will return a list of applications in the
current project. There are some edge-cases where the arg/flag name does
not match the resource name. To make these cases work, they can be added
to a map in the predictor.
  • Loading branch information
ctrox committed Jun 17, 2024
1 parent e9565e2 commit 52167fa
Show file tree
Hide file tree
Showing 16 changed files with 148 additions and 18 deletions.
13 changes: 10 additions & 3 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ type Client struct {
KubeconfigPath string
Project string
Log *log.Client
Token string
KubeconfigContext string
}

Expand All @@ -48,7 +47,7 @@ func New(ctx context.Context, apiClusterContext, project string, opts ...ClientO
if err != nil {
return nil, err
}
client.Token = token
client.Config.BearerToken = token

scheme, err := NewScheme()
if err != nil {
Expand All @@ -75,7 +74,7 @@ func New(ctx context.Context, apiClusterContext, project string, opts ...ClientO
// LogClient sets up a log client connected to the provided address.
func LogClient(address string, insecure bool) ClientOpt {
return func(c *Client) error {
logClient, err := log.NewClient(address, c.Token, c.Project, insecure)
logClient, err := log.NewClient(address, c.Config.BearerToken, c.Project, insecure)
if err != nil {
return fmt.Errorf("unable to create log client: %w", err)
}
Expand Down Expand Up @@ -137,6 +136,14 @@ func (c *Client) GetConnectionSecret(ctx context.Context, mg resource.Managed) (
return secret, nil
}

func (c *Client) Token() string {
if c.Config == nil {
return ""
}

return c.Config.BearerToken
}

func LoadingRules() (*clientcmd.ClientConfigLoadingRules, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
if _, ok := os.LookupEnv("HOME"); !ok {
Expand Down
2 changes: 1 addition & 1 deletion auth/print_access_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ import (
type PrintAccessTokenCmd struct{}

func (o *PrintAccessTokenCmd) Run(ctx context.Context, client *api.Client) error {
fmt.Println(client.Token)
fmt.Println(client.Token())
return nil
}
2 changes: 1 addition & 1 deletion auth/set_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

type SetProjectCmd struct {
Name string `arg:"" help:"Name of the default project to be used."`
Name string `arg:"" predictor:"resource_name" help:"Name of the default project to be used."`
}

func (s *SetProjectCmd) Run(ctx context.Context, client *api.Client) error {
Expand Down
2 changes: 1 addition & 1 deletion auth/whoami.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (s *WhoAmICmd) Run(ctx context.Context, client *api.Client) error {
return err
}

userInfo, err := api.GetUserInfoFromToken(client.Token)
userInfo, err := api.GetUserInfoFromToken(client.Token())
if err != nil {
return err
}
Expand Down
4 changes: 3 additions & 1 deletion auth/whoami_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import (
"github.com/ninech/nctl/auth"
"github.com/ninech/nctl/internal/test"
"github.com/stretchr/testify/require"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestWhoAmICmd_Run(t *testing.T) {
client := fake.NewClientBuilder().Build()
apiClient := &api.Client{WithWatch: client, Project: "default", Token: auth.FakeJWTToken, KubeconfigPath: "*-kubeconfig.yaml"}
apiClient := &api.Client{WithWatch: client, Project: "default", KubeconfigPath: "*-kubeconfig.yaml"}
apiClient.Config = &rest.Config{BearerToken: auth.FakeJWTToken}

kubeconfig, err := test.CreateTestKubeconfig(apiClient, "test")
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion create/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (app *applicationCmd) Run(ctx context.Context, client *api.Client) error {
if !app.SkipRepoAccessCheck {
validator := &validation.RepositoryValidator{
GitInformationServiceURL: app.GitInformationServiceURL,
Token: client.Token,
Token: client.Token(),
Debug: app.Debug,
}
if err := validator.Validate(ctx, &newApp.Spec.ForProvider.Git.GitTarget, auth); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Cmd struct {
}

type resourceCmd struct {
Name string `arg:"" help:"Name of the resource to delete."`
Name string `arg:"" predictor:"resource_name" help:"Name of the resource to delete."`
Force bool `default:"false" help:"Do not ask for confirmation of deletion."`
Wait bool `default:"true" help:"Wait until resource is fully deleted"`
WaitTimeout time.Duration `default:"5m" help:"Duration to wait for the deletion. Only relevant if wait is set."`
Expand Down
2 changes: 1 addition & 1 deletion get/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func pullImage(ctx context.Context, apiClient *api.Client, build *apps.Build) er
registryAuth, err := registry.EncodeAuthConfig(registry.AuthConfig{
// technically the username does not matter, it just needs to be set to something
Username: "registry",
Password: apiClient.Token,
Password: apiClient.Token(),
})
if err != nil {
return err
Expand Down
6 changes: 4 additions & 2 deletions get/clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import (
"github.com/ninech/nctl/internal/format"
)

type clustersCmd struct{}
type clustersCmd struct {
resourceCmd
}

func (l *clustersCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error {
clusterList := &infrastructure.KubernetesClusterList{}

if err := get.list(ctx, client, clusterList); err != nil {
if err := get.list(ctx, client, clusterList, matchName(l.Name)); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type Cmd struct {
}

type resourceCmd struct {
Name string `arg:"" help:"Name of the resource to get. If omitted all in the project will be listed." default:""`
Name string `arg:"" predictor:"resource_name" help:"Name of the resource to get. If omitted all in the project will be listed." default:""`
}

type output string
Expand Down
2 changes: 2 additions & 0 deletions internal/test/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/ninech/nctl/api/util"
"github.com/ninech/nctl/auth"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -22,6 +23,7 @@ func SetupClient(initObjs ...client.Object) (*api.Client, error) {
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).Build()

return &api.Client{
Config: &rest.Config{BearerToken: "fake"},
WithWatch: client, Project: "default",
}, nil
}
Expand Down
2 changes: 1 addition & 1 deletion logs/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Cmd struct {
}

type resourceCmd struct {
Name string `arg:"" help:"Name of the resource." default:""`
Name string `arg:"" predictor:"resource_name" help:"Name of the resource." default:""`
}

type logsCmd struct {
Expand Down
21 changes: 19 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import (
"github.com/ninech/nctl/get"
"github.com/ninech/nctl/internal/format"
"github.com/ninech/nctl/logs"
"github.com/ninech/nctl/predictor"
"github.com/ninech/nctl/update"
"github.com/posener/complete"
"github.com/willabides/kongplete"
)

type flags struct {
Project string `help:"Limit commands to a specific project." short:"p"`
Project string `predictor:"resource_name" help:"Limit commands to a specific project." short:"p"`
APICluster string `help:"Context name of the API cluster." default:"nineapis.ch" env:"NCTL_API_CLUSTER"`
LogAPIAddress string `help:"Address of the deplo.io logging API server." default:"https://logs.deplo.io" env:"NCTL_LOG_ADDR"`
LogAPIInsecure bool `help:"Don't verify TLS connection to the logging API server." hidden:"" default:"false" env:"NCTL_LOG_INSECURE"`
Expand Down Expand Up @@ -67,8 +68,24 @@ func main() {
kong.BindTo(ctx, (*context.Context)(nil)),
)

resourceNamePredictor := predictor.NewResourceName(func() (*api.Client, error) {
// for the resourcePredictor to use the correct APICluster, we need to
// call parse already. Note that this won't parse the flag for
// completion but it will work for the default and env.
_, _ = parser.Parse(os.Args[1:])
c, err := api.New(ctx, nctl.APICluster, nctl.Project)
if err != nil {
return nil, err
}

return c, nil
})

// completion handling
kongplete.Complete(parser, kongplete.WithPredictor("file", complete.PredictFiles("*")))
kongplete.Complete(parser,
kongplete.WithPredictor("file", complete.PredictFiles("*")),
kongplete.WithPredictor("resource_name", resourceNamePredictor),
)

kongCtx, err := parser.Parse(os.Args[1:])
if err != nil {
Expand Down
100 changes: 100 additions & 0 deletions predictor/predictor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package predictor

import (
"context"
"reflect"
"strings"
"time"

"github.com/gobuffalo/flect"
"github.com/ninech/apis/management/v1alpha1"
"github.com/ninech/nctl/api"
"github.com/ninech/nctl/auth"
"github.com/posener/complete"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
listSuffix = "list"
groupSuffix = "nine.ch"
)

// argResourceMap maps certain unusual args to resource names to aid with
// completion.
var argResourceMap = map[string]string{
"clusters": "kubernetesclusters",
"set-project": "projects",
"-p": "projects",
"--project": "projects",
}

type Resource struct {
client *api.Client
}

func NewResourceName(clientCreator func() (*api.Client, error)) *Resource {
c, err := clientCreator()
if err != nil {
return &Resource{}
}

return &Resource{client: c}
}

func (r *Resource) Predict(args complete.Args) []string {
if r.client == nil {
return []string{}
}

u := &unstructured.UnstructuredList{}
u.SetGroupVersionKind(r.findKind(args.LastCompleted))

ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()

ns := r.client.Project
// if we're looking for projects, we need to use the org as the namespace
if u.GetObjectKind().GroupVersionKind().Kind == reflect.TypeOf(v1alpha1.ProjectList{}).Name() {
cfg, err := auth.ReadConfig(r.client.KubeconfigPath, r.client.KubeconfigContext)
if err != nil {
return []string{}
}
ns = cfg.Organization
}

if err := r.client.List(ctx, u, client.InNamespace(ns)); err != nil {
return []string{}
}

resources := make([]string, len(u.Items))
for _, res := range u.Items {
resources = append(resources, res.GetName())
}

return resources
}

func (r *Resource) findKind(arg string) schema.GroupVersionKind {
if v, ok := argResourceMap[arg]; ok {
arg = v
}

// fmt.Println(flect.Pluralize(arg))
for gvk := range r.client.Scheme().AllKnownTypes() {
if !strings.HasSuffix(strings.ToLower(gvk.Kind), listSuffix) {
continue
}
if strings.HasSuffix(strings.ToLower(gvk.Group), groupSuffix) &&
listKindToResource(gvk.Kind) == flect.Pluralize(arg) {
return gvk
}
}

return schema.GroupVersionKind{}
}

func listKindToResource(kind string) string {
return flect.Pluralize(strings.TrimSuffix(strings.ToLower(kind), listSuffix))
}
2 changes: 1 addition & 1 deletion update/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (cmd *applicationCmd) Run(ctx context.Context, client *api.Client) error {
if !cmd.SkipRepoAccessCheck {
validator := &validation.RepositoryValidator{
GitInformationServiceURL: cmd.GitInformationServiceURL,
Token: client.Token,
Token: client.Token(),
Debug: cmd.Debug,
}

Expand Down
2 changes: 1 addition & 1 deletion update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Cmd struct {
}

type resourceCmd struct {
Name string `arg:"" help:"Name of the resource to update."`
Name string `arg:"" predictor:"resource_name" help:"Name of the resource to update."`
}

type updater struct {
Expand Down

0 comments on commit 52167fa

Please sign in to comment.