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
2 changes: 1 addition & 1 deletion apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ package apply
import "os"

type Cmd struct {
Filename *os.File `short:"f" predictor:"file"`
Filename *os.File `short:"f" completion-predictor:"file"`
FromFile fromFile `cmd:"" default:"1" name:"-f <file>" help:"Apply any resource from a yaml or json file."`
}
2 changes: 1 addition & 1 deletion auth/set_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

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

func (s *SetProjectCmd) Run(ctx context.Context, client *api.Client) error {
Expand Down
4 changes: 2 additions & 2 deletions copy/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ type Cmd struct {
}

type resourceCmd struct {
Name string `arg:"" help:"Name of the resource to copy." default:"" predictor:"resource_name"`
Name string `arg:"" help:"Name of the resource to copy." default:"" completion-predictor:"resource_name"`
TargetName string `help:"Target name of the new resource. A random name is generated if omitted." default:""`
TargetProject string `help:"Target project of the new resource. The current project is used if omitted." default:"" predictor:"project_name"`
TargetProject string `help:"Target project of the new resource. The current project is used if omitted." default:"" completion-predictor:"project_name"`
}

func getName(name string) string {
Expand Down
2 changes: 1 addition & 1 deletion create/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ type gitConfig struct {
Username *string `help:"Username to use when authenticating to the git repository over HTTPS." env:"GIT_USERNAME"`
Password *string `help:"Password to use when authenticating to the git repository over HTTPS. In case of GitHub or GitLab, this can also be an access token." env:"GIT_PASSWORD"`
SSHPrivateKey *string `help:"Private key in PEM format to connect to the git repository via SSH." env:"GIT_SSH_PRIVATE_KEY" xor:"SSH_KEY"`
SSHPrivateKeyFromFile *string `help:"Path to a file containing a private key in PEM format to connect to the git repository via SSH." env:"GIT_SSH_PRIVATE_KEY_FROM_FILE" xor:"SSH_KEY" predictor:"file"`
SSHPrivateKeyFromFile *string `help:"Path to a file containing a private key in PEM format to connect to the git repository via SSH." env:"GIT_SSH_PRIVATE_KEY_FROM_FILE" xor:"SSH_KEY" completion-predictor:"file"`
}

type healthProbe struct {
Expand Down
4 changes: 2 additions & 2 deletions create/cloudvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ type cloudVMCmd struct {
BootDiskSize *resource.Quantity `default:"20Gi" help:"Configures the size of the boot disk."`
Disks map[string]resource.Quantity `default:"" help:"Additional disks to mount to the machine."`
PublicKeys []string `default:"" help:"SSH public keys to connect to the CloudVM as root. The keys are expected to be in SSH format as defined in RFC4253. Immutable after creation."`
PublicKeysFromFiles []*os.File `default:"" predictor:"file" help:"SSH public key files to connect to the VM as root. The keys are expected to be in SSH format as defined in RFC4253. Immutable after creation."`
PublicKeysFromFiles []*os.File `default:"" completion-predictor:"file" help:"SSH public key files to connect to the VM as root. The keys are expected to be in SSH format as defined in RFC4253. Immutable after creation."`
CloudConfig string `default:"" help:"Pass custom cloud config data (https://cloudinit.readthedocs.io/en/latest/topics/format.html#cloud-config-data) to the cloud VM. If a CloudConfig is passed, the PublicKey parameter is ignored. Immutable after creation."`
CloudConfigFromFile *os.File `default:"" predictor:"file" help:"Pass custom cloud config data (https://cloudinit.readthedocs.io/en/latest/topics/format.html#cloud-config-data) from a file. Takes precedence. If a CloudConfig is passed, the PublicKey parameter is ignored. Immutable after creation."`
CloudConfigFromFile *os.File `default:"" completion-predictor:"file" help:"Pass custom cloud config data (https://cloudinit.readthedocs.io/en/latest/topics/format.html#cloud-config-data) from a file. Takes precedence. If a CloudConfig is passed, the PublicKey parameter is ignored. Immutable after creation."`
}

func (cmd *cloudVMCmd) Run(ctx context.Context, client *api.Client) error {
Expand Down
2 changes: 1 addition & 1 deletion create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
)

type Cmd struct {
Filename *os.File `short:"f" help:"Create any resource from a yaml or json file." predictor:"file"`
Filename *os.File `short:"f" help:"Create any resource from a yaml or json file." completion-predictor:"file"`
FromFile fromFile `cmd:"" default:"1" name:"-f <file>" help:"Create any resource from a yaml or json file."`
VCluster vclusterCmd `cmd:"" group:"infrastructure.nine.ch" name:"vcluster" help:"Create a new vcluster."`
APIServiceAccount apiServiceAccountCmd `cmd:"" group:"iam.nine.ch" name:"apiserviceaccount" aliases:"asa" help:"Create a new API Service Account."`
Expand Down
2 changes: 1 addition & 1 deletion create/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type mySQLCmd struct {
MachineType string `placeholder:"${mysql_machine_default}" help:"Sizing for a particular MySQL instance. Available types: ${mysql_machine_types}"`
AllowedCidrs []meta.IPv4CIDR `placeholder:"203.0.113.1/32" help:"IP addresses allowed to connect to the instance."`
SSHKeys []storage.SSHKey `help:"SSH public keys allowed to connect to the database server in order to up-/download and directly restore database backups."`
SSHKeysFile *os.File `predictor:"file" help:"Path to a file containing a list of SSH public keys (see above), separated by newlines. Lines prefixed with # are ignored."`
SSHKeysFile *os.File `completion-predictor:"file" help:"Path to a file containing a list of SSH public keys (see above), separated by newlines. Lines prefixed with # are ignored."`
SQLMode *[]storage.MySQLMode `placeholder:"\"MODE1, MODE2, ...\"" help:"Configures the sql_mode setting. Modes affect the SQL syntax MySQL supports and the data validation checks it performs. Defaults to: ${mysql_mode}"`
CharacterSetName string `placeholder:"${mysql_charset}" help:"Configures the character_set_server variable."`
CharacterSetCollation string `placeholder:"${mysql_collation}" help:"Configures the collation_server variable."`
Expand Down
2 changes: 1 addition & 1 deletion create/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type postgresCmd struct {
MachineType string `placeholder:"${postgres_machine_default}" help:"Defines the sizing for a particular PostgreSQL instance. Available types: ${postgres_machine_types}"`
AllowedCidrs []meta.IPv4CIDR `placeholder:"203.0.113.1/32" help:"IP addresses allowed to connect to the instance."`
SSHKeys []storage.SSHKey `help:"SSH public keys allowed to connect to the database server in order to up-/download and directly restore database backups."`
SSHKeysFile *os.File `predictor:"file" help:"Path to a file containing a list of SSH public keys (see above), separated by newlines. Lines prefixed with # are ignored."`
SSHKeysFile *os.File `completion-predictor:"file" help:"Path to a file containing a list of SSH public keys (see above), separated by newlines. Lines prefixed with # are ignored."`
PostgresVersion storage.PostgresVersion `placeholder:"${postgres_version_default}" help:"Release version with which the PostgreSQL instance is created. Available versions: ${postgres_versions}"`
KeepDailyBackups *int `placeholder:"${postgres_backup_retention_days}" help:"Number of daily database backups to keep. Note that setting this to 0, backup will be disabled and existing dumps deleted immediately."`
}
Expand Down
4 changes: 2 additions & 2 deletions delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

type Cmd struct {
Filename *os.File `short:"f" predictor:"file"`
Filename *os.File `short:"f" completion-predictor:"file"`
FromFile fromFile `cmd:"" default:"1" name:"-f <file>" help:"Delete any resource from a yaml or json file."`
VCluster vclusterCmd `cmd:"" group:"infrastructure.nine.ch" name:"vcluster" help:"Delete a vcluster."`
APIServiceAccount apiServiceAccountCmd `cmd:"" group:"iam.nine.ch" name:"apiserviceaccount" aliases:"asa" help:"Delete an API Service Account."`
Expand All @@ -33,7 +33,7 @@ type Cmd struct {
}

type resourceCmd struct {
Name string `arg:"" predictor:"resource_name" help:"Name of the resource to delete."`
Name string `arg:"" completion-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 edit/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type Cmd struct {
}

type resourceCmd struct {
Name string `arg:"" predictor:"resource_name" help:"Name of the resource to edit." required:""`
Name string `arg:"" completion-predictor:"resource_name" help:"Name of the resource to edit." required:""`
}

const header = `# Please edit the %s below.
Expand Down
2 changes: 1 addition & 1 deletion exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ type Cmd struct {
}

type resourceCmd struct {
Name string `arg:"" predictor:"resource_name" help:"Name of the application to exec command/shell in." required:""`
Name string `arg:"" completion-predictor:"resource_name" help:"Name of the application to exec command/shell in." required:""`
}
2 changes: 1 addition & 1 deletion get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ type output struct {
}

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

type outputFormat string
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/grafana/loki/v3 v3.6.3
github.com/hashicorp/go-multierror v1.1.1
github.com/int128/kubelogin v1.35.2
github.com/jotaen/kong-completion v0.0.7
github.com/jotaen/kong-completion v0.0.11
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de
github.com/lucasepe/codename v0.2.1-0.20230220151621-5e31bf1e775f
github.com/mattn/go-isatty v0.0.20
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,8 @@ github.com/jaegertracing/jaeger-idl v0.6.0 h1:LOVQfVby9ywdMPI9n3hMwKbyLVV3BL1XH2
github.com/jaegertracing/jaeger-idl v0.6.0/go.mod h1:mpW0lZfG907/+o5w5OlnNnig7nHJGT3SfKmRqC42HGQ=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jotaen/kong-completion v0.0.7 h1:l2UrG51q0gWD8Ph0CykBvGOb1YlXDc789AQnWkq+U9M=
github.com/jotaen/kong-completion v0.0.7/go.mod h1:dtitX9zCkffI5AON0IKsqHOFEEaL/S2AudgzJfCVFA4=
github.com/jotaen/kong-completion v0.0.11 h1:ZRyQt+IwjcAObbiyxJZ3YR7r/o/f6HYidTK1+7YNtnE=
github.com/jotaen/kong-completion v0.0.11/go.mod h1:dyIG20e3qq128SUBtF8jzI7YtkfzjWMlgbqkAJd6xHQ=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
Expand Down
2 changes: 1 addition & 1 deletion logs/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Cmd struct {
}

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

type logsCmd struct {
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import (
)

type flags struct {
Project string `help:"Limit commands to a specific project." short:"p" predictor:"project_name"`
Project string `help:"Limit commands to a specific project." short:"p" completion-predictor:"project_name"`
APICluster string `help:"Context name of the API cluster." default:"${api_cluster}" env:"NCTL_API_CLUSTER" hidden:""`
LogAPIAddress string `help:"Address of the deplo.io logging API server." default:"https://logs.deplo.io" env:"NCTL_LOG_ADDR" hidden:""`
LogAPIInsecure bool `help:"Don't verify TLS connection to the logging API server." hidden:"" default:"false" env:"NCTL_LOG_INSECURE"`
Expand Down
33 changes: 33 additions & 0 deletions predictor/predictor.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,45 @@ func findProject(args complete.Args) (string, bool) {
if args.LastCompleted == "-p" || args.LastCompleted == "--project" {
return "", true
}

// try to find project in args.All first
if p := findProjectInSlice(args.All); p != "" {
return p, false
}

// Fall back to parsing COMP_LINE if args.All is empty.
//
// When completing positional arguments in nested subcommands like
// "nctl exec application <name>", the posener/complete library slices
// args.All as it descends through each subcommand (nctl -> exec ->
// application). By the time our predictor is called, args.All is empty
// because all arguments have been "consumed" by the subcommand matching.
// COMP_LINE always contains the full command line, so we use it as a
// fallback to find the --project flag.
if p := findProjectInSlice(argsFromENV()); p != "" {
return p, false
}

return "", false
}

// argsFromENV returns all arguments in command line (not including the command itself)
//
// When completing positional arguments in nested subcommands like
// "nctl exec application <name>", the posener/complete library slices
// args.All as it descends through each subcommand (nctl -> exec ->
// application). By the time our predictor is called, args.All is empty
// because all arguments have been "consumed" by the subcommand matching.
// COMP_LINE always contains the full command line, so we use it as a
// fallback to find the --project flag.
func argsFromENV() []string {
if line := os.Getenv("COMP_LINE"); line != "" {
return strings.Fields(line)
}

return nil
}

// findProjectInSlice searches for -p or --project flag and returns its value.
func findProjectInSlice(args []string) string {
for i, arg := range args {
Expand Down
152 changes: 151 additions & 1 deletion predictor/predictor_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,156 @@
package predictor

import "testing"
import (
"bytes"
"strconv"
"testing"

"github.com/posener/complete"
)

func TestFindProjectWithCompleteLibrary(t *testing.T) {
tests := []struct {
name string
compLine string
wantProject string
}{
{
name: "project flag before positional arg",
compLine: "nctl exec --project myproject application ",
wantProject: "myproject",
},
{
name: "short project flag",
compLine: "nctl exec -p myproject application ",
wantProject: "myproject",
},
{
name: "no project flag",
compLine: "nctl exec application ",
wantProject: "",
},
{
name: "project flag after subcommand",
compLine: "nctl exec application --project otherproject ",
wantProject: "otherproject",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
capture := &capturePredictor{predictions: []string{"test-result"}}

// Build a command structure similar to what kong-completion generates
// for "nctl exec application <name>".
cmd := complete.Command{
Sub: map[string]complete.Command{
"exec": {
Flags: map[string]complete.Predictor{
"--project": complete.PredictAnything,
"-p": complete.PredictAnything,
},
Sub: map[string]complete.Command{
"application": {
Args: capture,
},
},
},
},
}

// Simulate shell completion
t.Setenv("COMP_LINE", tt.compLine)
t.Setenv("COMP_POINT", strconv.Itoa(len(tt.compLine)))

cmp := complete.New("nctl", cmd)
cmp.Out = &bytes.Buffer{} // discard output
cmp.Complete()

if !capture.called {
t.Fatal("predictor was not called")
}

gotProject, _ := findProject(capture.captured)
if gotProject != tt.wantProject {
t.Errorf("findProject() = %q, want %q", gotProject, tt.wantProject)
}
})
}
}

func TestFindProjectIncomplete(t *testing.T) {
tests := []struct {
name string
compLine string
wantIncomplete bool
}{
{
name: "incomplete --project flag",
compLine: "nctl exec --project ",
wantIncomplete: true,
},
{
name: "incomplete -p flag",
compLine: "nctl exec -p ",
wantIncomplete: true,
},
{
name: "complete project flag",
compLine: "nctl exec --project myproject ",
wantIncomplete: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
capture := &capturePredictor{predictions: []string{}}

// For incomplete flags, the completion happens at the exec level
// (completing the project name), not at the positional arg level.
cmd := complete.Command{
Sub: map[string]complete.Command{
"exec": {
Flags: map[string]complete.Predictor{
"--project": capture, // capture here for incomplete flag tests
"-p": capture,
},
Sub: map[string]complete.Command{
"application": {
Args: capture,
},
},
},
},
}

t.Setenv("COMP_LINE", tt.compLine)
t.Setenv("COMP_POINT", strconv.Itoa(len(tt.compLine)))

cmp := complete.New("nctl", cmd)
cmp.Out = &bytes.Buffer{}
cmp.Complete()

_, gotIncomplete := findProject(capture.captured)
if gotIncomplete != tt.wantIncomplete {
t.Errorf("findProject() incomplete = %v, want %v (LastCompleted=%q)",
gotIncomplete, tt.wantIncomplete, capture.captured.LastCompleted)
}
})
}
}

// capturePredictor is a test predictor that captures the args it receives.
type capturePredictor struct {
captured complete.Args
predictions []string
called bool
}

func (c *capturePredictor) Predict(args complete.Args) []string {
c.captured = args
c.called = true
return c.predictions
}

func TestFindProjectInSlice(t *testing.T) {
tests := []struct {
Expand Down
2 changes: 1 addition & 1 deletion update/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ type gitConfig struct {
Username *string `help:"Username to use when authenticating to the git repository over HTTPS." env:"GIT_USERNAME"`
Password *string `help:"Password to use when authenticating to the git repository over HTTPS. In case of GitHub or GitLab, this can also be an access token." env:"GIT_PASSWORD"`
SSHPrivateKey *string `help:"Private key in x509 format to connect to the git repository via SSH." env:"GIT_SSH_PRIVATE_KEY"`
SSHPrivateKeyFromFile *string `help:"Path to a file containing a private key in PEM format to connect to the git repository via SSH." env:"GIT_SSH_PRIVATE_KEY_FROM_FILE" xor:"SSH_KEY" predictor:"file"`
SSHPrivateKeyFromFile *string `help:"Path to a file containing a private key in PEM format to connect to the git repository via SSH." env:"GIT_SSH_PRIVATE_KEY_FROM_FILE" xor:"SSH_KEY" completion-predictor:"file"`
}

func (g gitConfig) sshPrivateKey() (*string, error) {
Expand Down
Loading