diff --git a/command-signatures/json/kubecolor.json b/command-signatures/json/kubecolor.json index 461171dd..431ef886 100644 --- a/command-signatures/json/kubecolor.json +++ b/command-signatures/json/kubecolor.json @@ -6989,7 +6989,9 @@ { "name": "--cluster", "args": { - "name": "cluster" + "name": "cluster", + "generatorName": "cluster", + "skipGeneratorValidation": false }, "description": "The name of the kubeconfig cluster to use", "isPersistent": true @@ -6997,7 +6999,9 @@ { "name": "--context", "args": { - "name": "context" + "name": "context", + "generatorName": "context", + "skipGeneratorValidation": false }, "description": "The name of the kubeconfig context to use", "isPersistent": true @@ -7125,7 +7129,9 @@ { "name": "--user", "args": { - "name": "user" + "name": "user", + "generatorName": "user", + "skipGeneratorValidation": false }, "description": "The name of the kubeconfig user to use", "isPersistent": true diff --git a/command-signatures/json/kubectl.json b/command-signatures/json/kubectl.json index 1a2e3439..a40423db 100644 --- a/command-signatures/json/kubectl.json +++ b/command-signatures/json/kubectl.json @@ -7180,7 +7180,9 @@ { "name": "--user", "args": { - "name": "user" + "name": "user", + "generatorName": "user", + "skipGeneratorValidation": false }, "description": "The name of the kubeconfig user to use", "isPersistent": true diff --git a/command-signatures/json/oc.json b/command-signatures/json/oc.json index 709bab76..2cea187d 100644 --- a/command-signatures/json/oc.json +++ b/command-signatures/json/oc.json @@ -8768,7 +8768,9 @@ { "name": "--cluster", "args": { - "name": "cluster" + "name": "cluster", + "generatorName": "cluster", + "skipGeneratorValidation": false }, "description": "The name of the kubeconfig cluster to use", "isPersistent": true @@ -8776,7 +8778,9 @@ { "name": "--context", "args": { - "name": "context" + "name": "context", + "generatorName": "context", + "skipGeneratorValidation": false }, "description": "The name of the kubeconfig context to use", "isPersistent": true @@ -8905,7 +8909,9 @@ { "name": "--user", "args": { - "name": "user" + "name": "user", + "generatorName": "user", + "skipGeneratorValidation": false }, "description": "The name of the kubeconfig user to use", "isPersistent": true diff --git a/command-signatures/src/generators/kubecolor.rs b/command-signatures/src/generators/kubecolor.rs index db82d78c..54ee42ab 100644 --- a/command-signatures/src/generators/kubecolor.rs +++ b/command-signatures/src/generators/kubecolor.rs @@ -5,7 +5,7 @@ use warp_completion_metadata::CommandSignatureGenerators; use super::kubectl::{ CLUSTER_GENERATOR, CLUSTER_ROLE_GENERATOR, CONTEXT_GENERATOR, DEPLOYMENTS_GENERATOR, NAMESPACE_GENERATOR, NODE_GENERATOR, RESOURCE_GENERATOR, RESOURCE_TYPE_GENERATOR, - ROLE_GENERATOR, RUNNING_PODS_GENERATOR, TYPE_OR_TYPE_SLASH_NAME, + ROLE_GENERATOR, RUNNING_PODS_GENERATOR, TYPE_OR_TYPE_SLASH_NAME, USER_GENERATOR, }; pub fn generator() -> CommandSignatureGenerators { @@ -19,6 +19,7 @@ pub fn generator() -> CommandSignatureGenerators { .add_generator("resource", RESOURCE_GENERATOR.clone()) .add_generator("context", CONTEXT_GENERATOR.clone()) .add_generator("cluster", CLUSTER_GENERATOR.clone()) + .add_generator("user", USER_GENERATOR.clone()) .add_generator("namespace", NAMESPACE_GENERATOR.clone()) .add_generator("type_or_type_slash_name", TYPE_OR_TYPE_SLASH_NAME.clone()) } diff --git a/command-signatures/src/generators/kubectl.rs b/command-signatures/src/generators/kubectl.rs index fd4cfe5c..d57a17b7 100644 --- a/command-signatures/src/generators/kubectl.rs +++ b/command-signatures/src/generators/kubectl.rs @@ -29,23 +29,35 @@ fn space_or_equals_delimited_option_value<'a>( option_name: &str, ) -> Option<&'a str> { let option_name_equals = format!("{option_name}="); - let option_idx = tokens + let scan_range = tokens .iter() - .position(|token| *token == option_name || token.starts_with(&option_name_equals)); + .position(|token| *token == "--") + .unwrap_or(tokens.len()); + let candidates = &tokens[..scan_range]; + let option_idx = candidates + .iter() + .rposition(|token| *token == option_name || token.starts_with(&option_name_equals)); option_idx.and_then(|idx| { // This option is equals delimited, so position is option_name=value - if let Some(equals_value) = tokens + if let Some(equals_value) = candidates .get(idx) .and_then(|token| token.strip_prefix(&option_name_equals)) { Some(equals_value) } else { // This option is space delimited, so value is the next token - tokens.get(idx + 1).copied() + candidates.get(idx + 1).copied() } }) } +fn is_safe_unquoted(value: &str) -> bool { + !value.is_empty() + && value.chars().all(|c| { + c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | ':' | '/' | '@' | '+') + }) +} + /// Returns the value of a given `key` from a list of environment variables formatted as /// `KEY=VALUE`. fn env_var_value<'a>(env_vars: &'a [String], key: &str) -> Option<&'a str> { @@ -53,31 +65,49 @@ fn env_var_value<'a>(env_vars: &'a [String], key: &str) -> Option<&'a str> { env_vars.iter().find_map(|env| env.strip_prefix(&prefix)) } -/// Returns a command string to run the given `subcommand` string with the same `--namespace` and/or -/// `--kubeconfig` values as specified in the incomplete command being entered (`tokens`), which -/// scopes down suggestions to be more helpful based on the already-specified namespace or -/// kubeconfig file. Also reads the `KUBECONFIG` environment variable if `--kubeconfig` is not -/// explicitly specified in the tokens. +/// Returns a command string to run the given `subcommand` string with the same `--namespace`, +/// `--context`, `--cluster`, `--user`, and/or `--kubeconfig` values as specified in the +/// incomplete command being entered (`tokens`), which scopes down suggestions to be more helpful +/// based on the already-specified namespace, context, cluster, user, or kubeconfig file. Also +/// reads the `KUBECONFIG` environment variable if `--kubeconfig` is not explicitly specified in +/// the tokens. fn kubectl_script( env_vars: &[String], tokens: &[&str], subcommand: CommandBuilder, ) -> CommandBuilder { + // A value that is not safe to interpolate bare is dropped rather than quoted, because no one + // quoting style is correct for every shell this command is built for: cmd.exe has no single + // quotes, so quoting there would leave `&` and friends live. let kubeconfig_value = space_or_equals_delimited_option_value(tokens, "--kubeconfig") .or_else(|| env_var_value(env_vars, "KUBECONFIG")) + .filter(|value| is_safe_unquoted(value)) .map(|value| format!("--kubeconfig={value} ")) // Fall back to the $KUBECONFIG shell variable, which is set when session environment // variables are forwarded to the child process. .unwrap_or_else(|| r#"${KUBECONFIG:+--kubeconfig="$KUBECONFIG"} "#.to_owned()); + let context_value = space_or_equals_delimited_option_value(tokens, "--context") + .filter(|value| is_safe_unquoted(value)) + .map(|value| format!("--context={value} ")) + .unwrap_or_else(|| "".to_owned()); + let cluster_value = space_or_equals_delimited_option_value(tokens, "--cluster") + .filter(|value| is_safe_unquoted(value)) + .map(|value| format!("--cluster={value} ")) + .unwrap_or_else(|| "".to_owned()); + let user_value = space_or_equals_delimited_option_value(tokens, "--user") + .filter(|value| is_safe_unquoted(value)) + .map(|value| format!("--user={value} ")) + .unwrap_or_else(|| "".to_owned()); let namespace_value = space_or_equals_delimited_option_value(tokens, "--namespace") .or(space_or_equals_delimited_option_value(tokens, "-n")) + .filter(|value| is_safe_unquoted(value)) .map(|value| format!("--namespace={value} ")) .unwrap_or_else(|| "".to_owned()); let env_vars_str = env_vars.iter().join(" "); CommandBuilder::concat( CommandBuilder::single_command(format!( - "{env_vars_str} kubectl {kubeconfig_value}{namespace_value}" + "{env_vars_str} kubectl {kubeconfig_value}{context_value}{cluster_value}{user_value}{namespace_value}" )), subcommand, ) @@ -200,6 +230,21 @@ lazy_static! { |tokens, _, env_vars| kubectl_script(env_vars, tokens, CommandBuilder::single_command("get namespace -o custom-columns=:.metadata.name")), |output| kubectl_post_process(output, None), ); + pub(super) static ref USER_GENERATOR: Generator = + Generator::command_from_tokens( + |tokens, _, env_vars| kubectl_script(env_vars, tokens, CommandBuilder::single_command("config get-users")), + |output| match KubetctlStatus::from_output(output) { + KubetctlStatus::ConnectedToCluster | KubetctlStatus::GeneralError => { + GeneratorResults::default() + } + KubetctlStatus::Other => output + .lines() + .map(str::trim) + .filter(|line| !line.is_empty() && *line != "NAME") + .map(Suggestion::new) + .collect_unordered_results(), + }, + ); pub(super) static ref TYPE_OR_TYPE_SLASH_NAME: Generator = Generator::command_from_tokens( |tokens, _, env_vars| { @@ -259,6 +304,7 @@ pub fn generator() -> CommandSignatureGenerators { .add_generator("context", CONTEXT_GENERATOR.clone()) .add_generator("cluster", CLUSTER_GENERATOR.clone()) .add_generator("namespace", NAMESPACE_GENERATOR.clone()) + .add_generator("user", USER_GENERATOR.clone()) .add_generator("type_or_type_slash_name", TYPE_OR_TYPE_SLASH_NAME.clone()) .add_generator( "kubectl_builtin_completion", @@ -425,6 +471,10 @@ mod tests { CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), ); let built = cmd.build(Shell::Posix); + assert!( + built.contains("--context=staging-cluster"), + "Expected --context=staging-cluster, got: {built}" + ); assert!( built.contains("--namespace=project1"), "Expected --namespace=project1, got: {built}" @@ -446,4 +496,452 @@ mod tests { "Expected --namespace=kube-system from equals syntax, got: {built}" ); } + + #[test] + fn test_context_flag_before_subcommand() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--context", "staging-cluster", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + built.contains("--context=staging-cluster"), + "Expected --context=staging-cluster from --context flag before subcommand, got: {built}" + ); + } + + #[test] + fn test_context_equals_syntax() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--context=staging-cluster", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + built.contains("--context=staging-cluster"), + "Expected --context=staging-cluster from equals syntax, got: {built}" + ); + } + + #[test] + fn test_cluster_flag_before_subcommand() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--cluster", "prod-cluster", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + built.contains("--cluster=prod-cluster"), + "Expected --cluster=prod-cluster from --cluster flag before subcommand, got: {built}" + ); + } + + #[test] + fn test_cluster_equals_syntax() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--cluster=prod-cluster", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + built.contains("--cluster=prod-cluster"), + "Expected --cluster=prod-cluster from equals syntax, got: {built}" + ); + } + + #[test] + fn test_user_flag_before_subcommand() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--user", "jane-doe", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + built.contains("--user=jane-doe"), + "Expected --user=jane-doe from --user flag before subcommand, got: {built}" + ); + } + + #[test] + fn test_user_equals_syntax() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--user=jane-doe", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + built.contains("--user=jane-doe"), + "Expected --user=jane-doe from equals syntax, got: {built}" + ); + } + + #[test] + fn test_context_cluster_user_and_namespace_flags_all_forwarded() { + let env_vars = vec![]; + let tokens = vec![ + "kubectl", + "--context", + "staging-cluster", + "--cluster", + "prod-cluster", + "--user", + "jane-doe", + "-n", + "project1", + "get", + "pods", + ]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + built.contains("--context=staging-cluster"), + "Expected --context=staging-cluster, got: {built}" + ); + assert!( + built.contains("--cluster=prod-cluster"), + "Expected --cluster=prod-cluster, got: {built}" + ); + assert!( + built.contains("--user=jane-doe"), + "Expected --user=jane-doe, got: {built}" + ); + assert!( + built.contains("--namespace=project1"), + "Expected --namespace=project1, got: {built}" + ); + } + + // --- is_safe_unquoted: which values are forwarded at all --- + + /// The shapes real kubeconfig names take -- EKS ARNs, GKE names, POSIX paths, user@host -- are + /// all safe to forward, so this is the path virtually every completion takes. + #[test] + fn test_is_safe_unquoted_accepts_realistic_names() { + for value in [ + "prod", + "minikube", + "docker-desktop", + "gke_my-project_us-central1-a_prod", + "arn:aws:eks:us-east-1:1234:cluster/prod", + "admin@prod.local", + "/home/me/.kube/config", + "kube-system", + ] { + assert!( + is_safe_unquoted(value), + "Expected `{value}` to be forwarded" + ); + } + } + + /// Every one of these would need shell quoting to forward safely, and no single quoting style is + /// correct across POSIX shells, PowerShell and cmd.exe, so they are not forwarded at all. + #[test] + fn test_is_safe_unquoted_rejects_values_needing_quoting() { + for value in [ + "", + "prod west", + "it's-prod", + "$HOME", + "prod; rm -rf /", + "prod&whoami", + r#"`whoami`-"prod""#, + r"C:\Users\me\.kube\config", + ] { + assert!( + !is_safe_unquoted(value), + "Expected `{value}` not to be forwarded" + ); + } + } + + // --- unsafe values are dropped from the generated command, not quoted into it --- + + #[test] + fn test_context_value_with_command_separator_is_not_forwarded() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--context", "prod; rm -rf /", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + !built.contains("--context"), + "Expected the --context flag to be dropped entirely, got: {built}" + ); + assert!( + !built.contains("rm -rf"), + "Expected the injected command not to reach the generated command at all, got: {built}" + ); + } + + /// `cmd.exe` has no single-quote concept, so quoting a `&` would not have protected it there. + /// Dropping the value protects every shell. + #[test] + fn test_context_value_with_cmd_exe_separator_is_not_forwarded() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--context", "prod&whoami", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + for (name, shell) in [ + ("posix", Shell::Posix), + ("powershell", Shell::Powershell), + ("cmd.exe", Shell::CmdExe), + ] { + let built = cmd.build(shell); + assert!( + !built.contains("whoami"), + "Expected the injected command to be absent for {name}, got: {built}" + ); + } + } + + #[test] + fn test_namespace_value_with_embedded_quote_is_not_forwarded() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--namespace", "it's-a-namespace", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + !built.contains("--namespace"), + "Expected the --namespace flag to be dropped entirely, got: {built}" + ); + } + + /// An unsafe explicit `--kubeconfig` is dropped, which leaves the pre-existing `$KUBECONFIG` + /// fallback in place -- the same command as if no `--kubeconfig` had been typed. + #[test] + fn test_kubeconfig_value_with_dollar_sign_is_not_forwarded() { + let env_vars = vec![]; + let tokens = vec![ + "kubectl", + "--kubeconfig", + "$HOME/.kube/config", + "config", + "use-context", + ]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("config get-contexts -o name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + !built.contains("--kubeconfig=$HOME"), + "Expected the $HOME value not to be interpolated, got: {built}" + ); + assert!( + built.contains("${KUBECONFIG:+--kubeconfig="), + "Expected the $KUBECONFIG shell fallback to remain, got: {built}" + ); + } + + #[test] + fn test_cluster_value_with_whitespace_is_not_forwarded() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--cluster", "prod west", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + !built.contains("--cluster"), + "Expected the --cluster flag to be dropped entirely, got: {built}" + ); + } + + // --- space_or_equals_delimited_option_value: last-occurrence-wins and `--` termination --- + + #[test] + fn test_space_or_equals_delimited_option_value_takes_last_occurrence() { + let tokens = vec!["kubectl", "--context", "old", "--context", "new"]; + assert_eq!( + space_or_equals_delimited_option_value(&tokens, "--context"), + Some("new") + ); + } + + #[test] + fn test_space_or_equals_delimited_option_value_ignores_tokens_after_double_dash() { + let tokens = vec!["kubectl", "get", "pods", "--", "--context", "fake"]; + assert_eq!( + space_or_equals_delimited_option_value(&tokens, "--context"), + None + ); + } + + #[test] + fn test_repeated_context_space_form_uses_last_occurrence() { + let env_vars = vec![]; + let tokens = vec![ + "kubectl", + "--context", + "old", + "--context", + "new", + "get", + "pods", + ]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + built.contains("--context=new"), + "Expected the last --context occurrence, matching kubectl's own flag-overwrite semantics, got: {built}" + ); + assert!( + !built.contains("--context=old"), + "Did not expect the superseded --context value to be forwarded, got: {built}" + ); + } + + #[test] + fn test_repeated_context_equals_form_uses_last_occurrence() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--context=old", "--context=new", "get", "pods"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + built.contains("--context=new"), + "Expected the last --context=... occurrence, got: {built}" + ); + assert!( + !built.contains("--context=old"), + "Did not expect the superseded --context value to be forwarded, got: {built}" + ); + } + + #[test] + fn test_flag_lookalike_after_double_dash_terminator_is_not_forwarded() { + let env_vars = vec![]; + // pflag stops parsing flags at a bare `--`; a `--context`-looking token after it is a + // literal positional argument, not a flag, and must not be forwarded. + let tokens = vec![ + "kubectl", + "--context", + "real", + "get", + "pods", + "--", + "--context", + "not-a-real-context", + ]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get pods -o custom-columns=:.metadata.name"), + ); + let built = cmd.build(Shell::Posix); + assert!( + built.contains("--context=real"), + "Expected the --context before the `--` terminator to still be forwarded, got: {built}" + ); + assert!( + !built.contains("not-a-real-context"), + "Did not expect the flag-lookalike after `--` to be forwarded, got: {built}" + ); + } + + // --- USER_GENERATOR's output parser --- + + #[test] + fn test_user_generator_filters_name_header() { + let results = USER_GENERATOR.on_complete("NAME\nalice\nbob\n"); + let names: Vec<&str> = results + .suggestions + .iter() + .map(|s| s.exact_string.as_str()) + .collect(); + assert_eq!( + names, + vec!["alice", "bob"], + "Expected the NAME header to be filtered out and only user names kept" + ); + } + + #[test] + fn test_user_generator_returns_empty_when_connected_to_cluster() { + let results = + USER_GENERATOR.on_complete("The connection to the server localhost:8080 was refused"); + assert!( + results.suggestions.is_empty(), + "Expected no suggestions when kubectl actually reached a server, got: {:?}", + results.suggestions + ); + } + + #[test] + fn test_user_generator_returns_empty_on_general_error() { + let results = + USER_GENERATOR.on_complete("error: You must be logged in to the server (Unauthorized)"); + assert!( + results.suggestions.is_empty(), + "Expected no suggestions on a general kubectl error, got: {:?}", + results.suggestions + ); + } + + /// The whole generated command for the case reported in warpdotdev/warp#5186 and + /// warpdotdev/warp#3929: completing `--namespace` after a `--context` has been written on the + /// line has to query the cluster that context names, not the shell's active one. Asserting the + /// entire string (rather than a `contains`) also pins the spacing and the unquoted form, so a + /// regression in either shows up here. + #[test] + fn test_full_generated_command_forwards_context_to_namespace_query() { + let env_vars = vec![]; + let tokens = vec!["kubectl", "--context", "staging-cluster", "--namespace"]; + let cmd = kubectl_script( + &env_vars, + &tokens, + CommandBuilder::single_command("get namespace -o custom-columns=:.metadata.name"), + ); + assert_eq!( + cmd.build(Shell::Posix), + concat!( + r#" kubectl ${KUBECONFIG:+--kubeconfig="$KUBECONFIG"} "#, + "--context=staging-cluster ", + "get namespace -o custom-columns=:.metadata.name", + ) + ); + } } diff --git a/command-signatures/src/generators/oc.rs b/command-signatures/src/generators/oc.rs index 9bd2b11c..d3e1403f 100644 --- a/command-signatures/src/generators/oc.rs +++ b/command-signatures/src/generators/oc.rs @@ -10,7 +10,7 @@ use warp_completion_metadata::{ use super::kubectl::{ CLUSTER_GENERATOR, CLUSTER_ROLE_GENERATOR, CONTEXT_GENERATOR, DEPLOYMENTS_GENERATOR, NAMESPACE_GENERATOR, NODE_GENERATOR, RESOURCE_GENERATOR, RESOURCE_TYPE_GENERATOR, - ROLE_GENERATOR, RUNNING_PODS_GENERATOR, TYPE_OR_TYPE_SLASH_NAME, + ROLE_GENERATOR, RUNNING_PODS_GENERATOR, TYPE_OR_TYPE_SLASH_NAME, USER_GENERATOR, }; fn oc_post_process(output: &str) -> GeneratorResults { @@ -81,6 +81,7 @@ pub fn generator() -> CommandSignatureGenerators { .add_generator("resource", RESOURCE_GENERATOR.clone()) .add_generator("context", CONTEXT_GENERATOR.clone()) .add_generator("cluster", CLUSTER_GENERATOR.clone()) + .add_generator("user", USER_GENERATOR.clone()) .add_generator("namespace", NAMESPACE_GENERATOR.clone()) .add_generator("type_or_type_slash_name", TYPE_OR_TYPE_SLASH_NAME.clone()) .add_generator("oc_builtin_completion", oc_builtin_completion)