diff --git a/cmd/kubectl-fzf-completion/main.go b/cmd/kubectl-fzf-completion/main.go index 2d07acf..200ac23 100644 --- a/cmd/kubectl-fzf-completion/main.go +++ b/cmd/kubectl-fzf-completion/main.go @@ -20,7 +20,7 @@ import ( ) func completeFun(cmd *cobra.Command, args []string) { - header, comps, err := completion.ProcessCommandArgs(cmd.Use, args) + completionResults, err := completion.ProcessCommandArgs(cmd.Use, args) if e, ok := err.(resources.UnknownResourceError); ok { logrus.Warnf("Unknown resource type: %s", e) os.Exit(6) @@ -32,12 +32,13 @@ func completeFun(cmd *cobra.Command, args []string) { if err != nil { logrus.Fatalf("Completion error: %s", err) } - if len(comps) == 0 { + if len(completionResults.Completions) == 0 { logrus.Warn("No completion found") os.Exit(5) } - formattedComps := util.FormatCompletion(header, comps) + formattedComps := completionResults.GetFormattedOutput() + // TODO pass query fzfResult, err := fzf.CallFzf(formattedComps, "") if err != nil { logrus.Fatalf("Call fzf error: %s", err) diff --git a/pkg/completion/completion.go b/pkg/completion/completion.go index ece9683..0d0d06a 100644 --- a/pkg/completion/completion.go +++ b/pkg/completion/completion.go @@ -5,26 +5,15 @@ import ( "kubectlfzf/pkg/fetcher" "kubectlfzf/pkg/k8s/resources" "kubectlfzf/pkg/parse" - "kubectlfzf/pkg/util" "sort" - "strings" "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -func getNamespace(args []string) *string { - for k, arg := range args { - if (arg == "-n" || arg == "--namespace") && len(args) > k+1 { - return &args[k+1] - } - if strings.HasPrefix(arg, "--namespace=") { - return &strings.Split(arg, "=")[1] - } - } - return nil -} +const labelHeader = "Namespace\tLabel\tOccurrences" +const fieldSelectorHeader = "Namespace\tFieldSelector\tOccurrences" func getResourceCompletion(ctx context.Context, r resources.ResourceType, namespace *string, fetchConfig *fetcher.Fetcher) ([]string, error) { @@ -43,44 +32,44 @@ func getResourceCompletion(ctx context.Context, r resources.ResourceType, namesp } func processCommandArgsWithFetchConfig(ctx context.Context, fetchConfig *fetcher.Fetcher, - cmdVerb string, args []string) ([]string, []string, error) { - var comps []string + cmdVerb string, args []string) (*CompletionResult, error) { var err error resourceType, flagCompletion, err := parse.ParseFlagAndResources(cmdVerb, args) if err != nil { - return nil, nil, err + return nil, err } logrus.Debugf("Call Get Fun with %+v, resource type detected %s, flag detected %s", args, resourceType, flagCompletion) - labelHeader := []string{"Namespace", "Label", "Occurrences"} - fieldSelectorHeader := []string{"Namespace", "FieldSelector", "Occurrences"} - namespace := getNamespace(args) + completionResult := &CompletionResult{Cluster: fetchConfig.GetContext()} + namespace := parse.ParseNamespaceFromArgs(args) if flagCompletion == parse.FlagLabel { - comps, err := GetTagResourceCompletion(ctx, resourceType, namespace, fetchConfig, TagTypeLabel) - return labelHeader, comps, err + completionResult.Completions, err = GetTagResourceCompletion(ctx, resourceType, namespace, fetchConfig, TagTypeLabel) + completionResult.Header = labelHeader + return completionResult, err } else if flagCompletion == parse.FlagFieldSelector { - comps, err := GetTagResourceCompletion(ctx, resourceType, namespace, fetchConfig, TagTypeFieldSelector) - return fieldSelectorHeader, comps, err + completionResult.Completions, err = GetTagResourceCompletion(ctx, resourceType, namespace, fetchConfig, TagTypeFieldSelector) + completionResult.Header = fieldSelectorHeader + return completionResult, err } - header := resources.ResourceToHeader(resourceType) - comps, err = getResourceCompletion(ctx, resourceType, namespace, fetchConfig) + completionResult.Header = resources.ResourceToHeader(resourceType) + completionResult.Completions, err = getResourceCompletion(ctx, resourceType, namespace, fetchConfig) if err != nil { - return header, comps, errors.Wrap(err, "error getting resource completion") + return completionResult, errors.Wrap(err, "error getting resource completion") } - sort.Strings(comps) - return header, comps, err + sort.Strings(completionResult.Completions) + return completionResult, err } -func ProcessCommandArgs(cmdVerb string, args []string) (string, []string, error) { +func ProcessCommandArgs(cmdVerb string, args []string) (*CompletionResult, error) { fetchConfigCli := fetcher.GetFetchConfigCli() f := fetcher.NewFetcher(&fetchConfigCli) err := f.SetClusterNameFromCurrentContext() if err != nil { - return "", nil, err + return nil, err } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - header, comps, err := processCommandArgsWithFetchConfig(ctx, f, cmdVerb, args) + completionResult, err := processCommandArgsWithFetchConfig(ctx, f, cmdVerb, args) cancel() - return util.DumpLine(header), comps, err + return completionResult, err } diff --git a/pkg/completion/completion_results.go b/pkg/completion/completion_results.go new file mode 100644 index 0000000..a63def1 --- /dev/null +++ b/pkg/completion/completion_results.go @@ -0,0 +1,18 @@ +package completion + +import ( + "fmt" + "kubectlfzf/pkg/util" +) + +type CompletionResult struct { + Cluster string + Header string + Completions []string +} + +func (c *CompletionResult) GetFormattedOutput() string { + lines := []string{fmt.Sprintf("Cluster: %s", c.Cluster), c.Header} + lines = append(lines, c.Completions...) + return util.FormatCompletion(lines) +} diff --git a/pkg/completion/completion_test.go b/pkg/completion/completion_test.go index d6904d7..bd7c453 100644 --- a/pkg/completion/completion_test.go +++ b/pkg/completion/completion_test.go @@ -28,10 +28,10 @@ func TestProcessResourceName(t *testing.T) { {"exec", []string{"-ti", ""}}, } for _, cmdArg := range cmdArgs { - _, comps, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) + completionResults, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) require.NoError(t, err) - require.Greater(t, len(comps), 0) - require.Contains(t, comps[0], "kube-system\tcoredns-6d4b75cb6d-m6m4q\t172.17.0.3\t192.168.49.2\tminikube\tRunning\tBurstable\tcoredns\tCriticalAddonsOnly:,node-role.kubernetes.io/master:NoSchedule,node-role.kubernetes.io/control-plane:NoSchedule\tNone") + require.Greater(t, len(completionResults.Completions), 0) + require.Contains(t, completionResults.Completions[0], "kube-system\tcoredns-6d4b75cb6d-m6m4q\t172.17.0.3\t192.168.49.2\tminikube\tRunning\tBurstable\tcoredns\tCriticalAddonsOnly:,node-role.kubernetes.io/master:NoSchedule,node-role.kubernetes.io/control-plane:NoSchedule\tNone") } } @@ -44,10 +44,10 @@ func TestProcessNamespace(t *testing.T) { {"logs", []string{"--namespace="}}, } for _, cmdArg := range cmdArgs { - _, comps, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) + completionResults, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) require.NoError(t, err) - require.Greater(t, len(comps), 0) - require.Contains(t, comps[0], "default\t") + require.Greater(t, len(completionResults.Completions), 0) + require.Contains(t, completionResults.Completions[0], "default\t") } } @@ -61,10 +61,10 @@ func TestProcessLabelCompletion(t *testing.T) { {"get", []string{"pods", "--selector="}}, } for _, cmdArg := range cmdArgs { - _, comps, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) + completionResults, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) require.NoError(t, err) - require.Equal(t, "kube-system\ttier=control-plane\t4", comps[0]) - require.Len(t, comps, 12) + require.Equal(t, "kube-system\ttier=control-plane\t4", completionResults.Completions[0]) + require.Len(t, completionResults.Completions, 12) } } @@ -75,9 +75,9 @@ func TestProcessFieldSelectorCompletion(t *testing.T) { {"get", []string{"pods", "--field-selector="}}, } for _, cmdArg := range cmdArgs { - _, comps, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) + completionResults, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) require.NoError(t, err) - assert.Equal(t, "kube-system\tspec.nodeName=minikube\t7", comps[0]) + assert.Equal(t, "kube-system\tspec.nodeName=minikube\t7", completionResults.Completions[0]) } } @@ -91,7 +91,7 @@ func TestUnmanagedCompletion(t *testing.T) { {"get", []string{"--all-namespaces"}}, } for _, cmdArg := range cmdArgs { - _, _, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) + _, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) require.Error(t, err) require.IsType(t, parse.UnmanagedFlagError(""), err) } @@ -113,9 +113,9 @@ func TestManagedCompletion(t *testing.T) { {"get", []string{"-n", ""}}, } for _, cmdArg := range cmdArgs { - _, comps, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) + completionResults, err := processCommandArgsWithFetchConfig(context.Background(), fetchConfig, cmdArg.verb, cmdArg.args) require.NoError(t, err) - require.NotNil(t, comps) + require.NotNil(t, completionResults) } } diff --git a/pkg/fzf/call_fzf.go b/pkg/fzf/call_fzf.go index 95f5747..80ae46b 100644 --- a/pkg/fzf/call_fzf.go +++ b/pkg/fzf/call_fzf.go @@ -30,14 +30,14 @@ func setCompsInStdin(cmd *exec.Cmd, comps string) error { func CallFzf(comps string, query string) (string, error) { var result strings.Builder - header := strings.Split(comps, "\n")[0] + header := strings.Split(comps, "\n")[1] numFields := len(strings.Fields(header)) logrus.Debugf("header: %s, numFields: %d", header, numFields) previewWindow := fmt.Sprintf("--preview-window=down:%d", numFields) previewCmd := fmt.Sprintf("echo -e \"%s\n{}\" | sed -e \"s/'//g\" | awk '(NR==1){for (i=1; i<=NF; i++) a[i]=$i} (NR==2){for (i in a) {printf a[i] \": \" $i \"\\n\"} }' | column -t | fold -w $COLUMNS", header) // TODO Make fzf options configurable - fzfArgs := []string{"-1", "--header-lines=1", "--layout", "reverse", "-e", "--no-hscroll", "--no-sort", "--cycle", "-q", query, previewWindow, "--preview", previewCmd} + fzfArgs := []string{"-1", "--header-lines=2", "--layout", "reverse", "-e", "--no-hscroll", "--no-sort", "--cycle", "-q", query, previewWindow, "--preview", previewCmd} logrus.Infof("fzf args: %+v", fzfArgs) cmd := exec.Command("fzf", fzfArgs...) cmd.Stdout = &result diff --git a/pkg/k8s/resources/k8s_resources.go b/pkg/k8s/resources/k8s_resources.go index 8a201a3..a6b25a4 100644 --- a/pkg/k8s/resources/k8s_resources.go +++ b/pkg/k8s/resources/k8s_resources.go @@ -86,26 +86,26 @@ func (r *ResourceMeta) labelsString() string { return util.JoinSlicesOrNone(els, ",") } -func ResourceToHeader(r ResourceType) []string { - replicaSetHeader := []string{"Namespace", "Name", "Replicas", "AvailableReplicas", "ReadyReplicas", "Selector", "Age", "Labels"} - apiResourceHeader := []string{"Name", "Shortnames", "ApiVersion", "Namespaced", "Kind"} - configMapHeader := []string{"Namespace", "Name", "Age", "Labels"} - cronJobHeader := []string{"Namespace", "Name", "Schedule", "LastSchedule", "Containers", "Age", "Labels"} - daemonSetHeader := []string{"Namespace", "Name", "Desired", "Current", "Ready", "LabelSelector", "Containers", "Age", "Labels"} - deploymentHeader := []string{"Namespace", "Name", "Desired", "Current", "Up-to-date", "Available", "Age", "Labels"} - endpointsHeader := []string{"Namespace", "Name", "Age", "ReadyIps", "ReadyPods", "NotReadyIps", "NotReadyPods", "Labels"} - horizontalPodAutoscalerHeader := []string{"Namespace", "Name", "Reference", "Targets", "MinPods", "MaxPods", "Replicas", "Age", "Labels"} - ingressHeader := []string{"Namespace", "Name", "Address", "Age", "Labels"} - jobHeader := []string{"Namespace", "Name", "Completions", "Containers", "Age", "Labels"} - namespaceHeader := []string{"Name", "Age", "Labels"} - nodeHeader := []string{"Name", "Roles", "Status", "InstanceType", "Zone", "InternalIp", "Taints", "InstanceID", "Age", "Labels"} - podHeader := []string{"Namespace", "Name", "PodIp", "HostIp", "NodeName", "Phase", "QOSClass", "Containers", "Tolerations", "Claims", "Age", "Labels"} - persistentVolumeHeader := []string{"Name", "Status", "StorageClass", "Zone", "Claim", "Volume", "Affinities", "Age", "Labels"} - persistentVolumeClaimHeader := []string{"Namespace", "Name", "Status", "Capacity", "VolumeName", "StorageClass", "Age", "Labels"} - secretHeader := []string{"Namespace", "Name", "Type", "Data", "Age", "Labels"} - serviceHeader := []string{"Namespace", "Name", "Type", "ClusterIp", "Ports", "Selector", "Age", "Labels"} - serviceAccountHeader := []string{"Namespace", "Name", "Secrets", "Age", "Labels"} - statefulSetHeader := []string{"Namespace", "Name", "Replicas", "Selector", "Age", "Labels"} +func ResourceToHeader(r ResourceType) string { + replicaSetHeader := "Namespace\tName\tReplicas\tAvailableReplicas\tReadyReplicas\tSelector\tAge\tLabels" + apiResourceHeader := "Name\tShortnames\tApiVersion\tNamespaced\tKind" + configMapHeader := "Namespace\tName\tAge\tLabels" + cronJobHeader := "Namespace\tName\tSchedule\tLastSchedule\tContainers\tAge\tLabels" + daemonSetHeader := "Namespace\tName\tDesired\tCurrent\tReady\tLabelSelector\tContainers\tAge\tLabels" + deploymentHeader := "Namespace\tName\tDesired\tCurrent\tUp-to-date\tAvailable\tAge\tLabels" + endpointsHeader := "Namespace\tName\tAge\tReadyIps\tReadyPods\tNotReadyIps\tNotReadyPods\tLabels" + horizontalPodAutoscalerHeader := "Namespace\tName\tReference\tTargets\tMinPods\tMaxPods\tReplicas\tAge\tLabels" + ingressHeader := "Namespace\tName\tAddress\tAge\tLabels" + jobHeader := "Namespace\tName\tCompletions\tContainers\tAge\tLabels" + namespaceHeader := "Name\tAge\tLabels" + nodeHeader := "Name\tRoles\tStatus\tInstanceType\tZone\tInternalIp\tTaints\tInstanceID\tAge\tLabels" + podHeader := "Namespace\tName\tPodIp\tHostIp\tNodeName\tPhase\tQOSClass\tContainers\tTolerations\tClaims\tAge\tLabels" + persistentVolumeHeader := "Name\tStatus\tStorageClass\tZone\tClaim\tVolume\tAffinities\tAge\tLabels" + persistentVolumeClaimHeader := "Namespace\tName\tStatus\tCapacity\tVolumeName\tStorageClass\tAge\tLabels" + secretHeader := "Namespace\tName\tType\tData\tAge\tLabels" + serviceHeader := "Namespace\tName\tType\tClusterIp\tPorts\tSelector\tAge\tLabels" + serviceAccountHeader := "Namespace\tName\tSecrets\tAge\tLabels" + statefulSetHeader := "Namespace\tName\tReplicas\tSelector\tAge\tLabels" switch r { case ResourceTypeApiResource: return apiResourceHeader @@ -146,6 +146,6 @@ func ResourceToHeader(r ResourceType) []string { case ResourceTypeStatefulSet: return statefulSetHeader default: - return []string{"Unknown"} + return "Unknown" } } diff --git a/pkg/parse/parse_args.go b/pkg/parse/parse_args.go index f130a20..1e652d7 100644 --- a/pkg/parse/parse_args.go +++ b/pkg/parse/parse_args.go @@ -34,3 +34,15 @@ func ParseFlagAndResources(cmdVerb string, cmdArgs []string) (resourceType resou } return } + +func ParseNamespaceFromArgs(args []string) *string { + for k, arg := range args { + if (arg == "-n" || arg == "--namespace") && len(args) > k+1 { + return &args[k+1] + } + if strings.HasPrefix(arg, "--namespace=") { + return &strings.Split(arg, "=")[1] + } + } + return nil +} diff --git a/pkg/util/formatting.go b/pkg/util/formatting.go index a5bdbda..23c480f 100644 --- a/pkg/util/formatting.go +++ b/pkg/util/formatting.go @@ -8,13 +8,12 @@ import ( "github.com/sirupsen/logrus" ) -func FormatCompletion(header string, comps []string) string { +func FormatCompletion(lines []string) string { logrus.Info("formating completion") b := new(strings.Builder) w := tabwriter.NewWriter(b, 0, 0, 1, ' ', tabwriter.StripEscape) - fmt.Fprintln(w, header) - for _, c := range comps { - fmt.Fprintln(w, c) + for _, line := range lines { + fmt.Fprintln(w, line) } w.Flush() return b.String() diff --git a/pkg/util/formatting_test.go b/pkg/util/formatting_test.go index 15251d0..b4dca7c 100644 --- a/pkg/util/formatting_test.go +++ b/pkg/util/formatting_test.go @@ -7,7 +7,7 @@ import ( ) func TestFormatCompletion(t *testing.T) { - res := FormatCompletion("header1\thead2", []string{"comp1\tc1", "c2\tc22"}) + res := FormatCompletion([]string{"header1\thead2", "comp1\tc1", "c2\tc22"}) expected := `header1 head2 comp1 c1 c2 c22