Skip to content

Commit

Permalink
CLI: Improvements to lxc config and lxc profile shell completions (
Browse files Browse the repository at this point in the history
…#14542)

This PR includes updates to `lxc/completion.go`, improving the shell
completion functionality for instance configuration keys.

Summary of changes:
- Refactorings that improve performance and correctness;
- Updates `cmpInstanceKeys` and `cmpInstanceAllKeys` functions to use
the metadata API (resolves
#14537);
- Adds `cmpInstanceSetKeys` function for improved `lxc config unset`
completions.
  • Loading branch information
tomponline authored Nov 27, 2024
2 parents d9cacea + e9d18f2 commit 5159972
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 31 deletions.
148 changes: 119 additions & 29 deletions lxc/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

"github.com/spf13/cobra"

"github.com/canonical/lxd/lxd/instance/instancetype"
"github.com/canonical/lxd/shared"
"github.com/canonical/lxd/shared/api"
)
Expand Down Expand Up @@ -225,16 +224,12 @@ func (g *cmdGlobal) cmpImages(toComplete string) ([]string, cobra.ShellCompDirec
// cmpInstanceKeys provides shell completion for all instance configuration keys.
// It takes an instance name to determine instance type and returns a list of all instance configuration keys along with a shell completion directive.
func (g *cmdGlobal) cmpInstanceKeys(instanceName string) ([]string, cobra.ShellCompDirective) {
var keys []string
cmpDirectives := cobra.ShellCompDirectiveNoFileComp

// Early return when completing server keys.
_, instanceNameOnly, found := strings.Cut(instanceName, ":")
if instanceNameOnly == "" && found {
serverKeys, directives := g.cmpServerAllKeys(instanceName)
keys = append(keys, serverKeys...)
cmpDirectives = directives

return keys, cmpDirectives
return g.cmpServerAllKeys(instanceName)
}

resources, err := g.ParseServers(instanceName)
Expand All @@ -250,44 +245,140 @@ func (g *cmdGlobal) cmpInstanceKeys(instanceName string) ([]string, cobra.ShellC
return nil, cobra.ShellCompDirectiveError
}

// Complete keys based on instance type.
instanceType := instance.Type

if instanceType == "container" {
for k := range instancetype.InstanceConfigKeysContainer {
keys = append(keys, k)
}
} else if instanceType == "virtual-machine" {
for k := range instancetype.InstanceConfigKeysVM {
keys = append(keys, k)
}
metadataConfiguration, err := client.GetMetadataConfiguration()
if err != nil {
return nil, cobra.ShellCompDirectiveError
}

for k := range instancetype.InstanceConfigKeysAny {
keys = append(keys, k)
instanceConfig, ok := metadataConfiguration.Configs["instance"]
if !ok {
return nil, cobra.ShellCompDirectiveError
}

return keys, cmpDirectives
// Pre-allocate configKeys slice capacity.
keyCount := 0
for _, field := range instanceConfig {
keyCount += len(field.Keys)
}

configKeys := make([]string, 0, keyCount)

for _, field := range instanceConfig {
for _, key := range field.Keys {
for configKey, configKeyField := range key {
configKey = strings.TrimSuffix(configKey, "*")

// InstanceTypeAny config keys.
if configKeyField.Condition == "" {
configKeys = append(configKeys, configKey)
continue
}

if instanceType == string(api.InstanceTypeContainer) && configKeyField.Condition == "container" {
configKeys = append(configKeys, configKey)
} else if instanceType == string(api.InstanceTypeVM) && configKeyField.Condition == "virtual machine" {
configKeys = append(configKeys, configKey)
}
}
}
}

return configKeys, cmpDirectives | cobra.ShellCompDirectiveNoSpace
}

// cmpInstanceAllKeys provides shell completion for all possible instance configuration keys.
// It returns a list of all possible instance configuration keys along with a shell completion directive.
func (g *cmdGlobal) cmpInstanceAllKeys() ([]string, cobra.ShellCompDirective) {
keys := make([]string, 0, len(instancetype.InstanceConfigKeysContainer)+len(instancetype.InstanceConfigKeysVM)+len(instancetype.InstanceConfigKeysAny))
func (g *cmdGlobal) cmpInstanceAllKeys(profileName string) ([]string, cobra.ShellCompDirective) {
cmpDirectives := cobra.ShellCompDirectiveNoFileComp

for k := range instancetype.InstanceConfigKeysContainer {
keys = append(keys, k)
// Parse remote
resources, err := g.ParseServers(profileName)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}

resource := resources[0]
client := resource.server

metadataConfiguration, err := client.GetMetadataConfiguration()
if err != nil {
return nil, cobra.ShellCompDirectiveError
}

for k := range instancetype.InstanceConfigKeysVM {
keys = append(keys, k)
instanceConfig, ok := metadataConfiguration.Configs["instance"]
if !ok {
return nil, cobra.ShellCompDirectiveError
}

for k := range instancetype.InstanceConfigKeysAny {
keys = append(keys, k)
// Pre-allocate configKeys slice capacity.
keyCount := 0
for _, field := range instanceConfig {
keyCount += len(field.Keys)
}

return keys, cmpDirectives
configKeys := make([]string, 0, keyCount)

for _, field := range instanceConfig {
for _, key := range field.Keys {
for configKey := range key {
configKey = strings.TrimSuffix(configKey, "*")
configKeys = append(configKeys, configKey)
}
}
}

return configKeys, cmpDirectives | cobra.ShellCompDirectiveNoSpace
}

// cmpInstanceSetKeys provides shell completion for instance configuration keys which are currently set.
// It takes an instance name to determine instance type and returns a list of instance configuration keys along with a shell completion directive.
func (g *cmdGlobal) cmpInstanceSetKeys(instanceName string) ([]string, cobra.ShellCompDirective) {
cmpDirectives := cobra.ShellCompDirectiveNoFileComp

// Early return when completing server keys.
_, instanceNameOnly, found := strings.Cut(instanceName, ":")
if instanceNameOnly == "" && found {
return g.cmpServerAllKeys(instanceName)
}

resources, err := g.ParseServers(instanceName)
if err != nil || len(resources) == 0 {
return nil, cobra.ShellCompDirectiveError
}

resource := resources[0]
client := resource.server

instance, _, err := client.GetInstance(instanceName)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}

// Fetch all config keys that can be set by a user.
allInstanceConfigKeys, _ := g.cmpInstanceAllKeys(instanceName)

// Convert slice to map[string]struct{} for O(1) lookups.
keySet := make(map[string]struct{}, len(allInstanceConfigKeys))
for _, key := range allInstanceConfigKeys {
keySet[key] = struct{}{}
}

// Pre-allocate configKeys slice capacity.
keyCount := len(instance.Config)
configKeys := make([]string, 0, keyCount)

for configKey := range instance.Config {
// We only want to return the intersection between allInstanceConfigKeys and configKeys to avoid returning the full instance config.
_, exists := keySet[configKey]
if exists {
configKeys = append(configKeys, configKey)
}
}

return configKeys, cmpDirectives | cobra.ShellCompDirectiveNoSpace
}

// cmpServerAllKeys provides shell completion for all server configuration keys.
Expand Down Expand Up @@ -469,9 +560,8 @@ func (g *cmdGlobal) cmpInstances(toComplete string) ([]string, cobra.ShellCompDi
}

if !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(false)
remotes, _ := g.cmpRemotes(false)
results = append(results, remotes...)
cmpDirectives |= directives
}

return results, cmpDirectives
Expand Down
3 changes: 2 additions & 1 deletion lxc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,8 @@ func (c *cmdConfigUnset) command() *cobra.Command {
}

if len(args) == 1 {
return c.global.cmpInstanceKeys(args[0])
// Only complete config keys which are currently set.
return c.global.cmpInstanceSetKeys(args[0])
}

return nil, cobra.ShellCompDirectiveNoFileComp
Expand Down
2 changes: 1 addition & 1 deletion lxc/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,7 @@ For backward compatibility, a single configuration key may still be set with:
}

if len(args) == 1 {
return c.global.cmpInstanceAllKeys()
return c.global.cmpInstanceAllKeys(args[0])
}

return nil, cobra.ShellCompDirectiveNoFileComp
Expand Down

0 comments on commit 5159972

Please sign in to comment.