(chore) setup kmsvc-cli
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// queueSummary is the subset of a Queue CRD's spec/status this CLI surfaces.
|
||||
// Read directly off the unstructured object rather than a generated
|
||||
// clientset, since kmsvc-cli otherwise has no dependency on the
|
||||
// kafaka-management-service module's API types.
|
||||
type queueSummary struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Phase string `json:"phase"`
|
||||
FIFO bool `json:"fifoQueue"`
|
||||
ShardCount int `json:"shardCount"`
|
||||
MaxReceives int64 `json:"maxReceiveCount"`
|
||||
}
|
||||
|
||||
func newQueueCmd(flags *globalFlags) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "queue",
|
||||
Short: "List and describe Queue CRDs (the queue-operator's source of truth)",
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newQueueListCmd(flags),
|
||||
newQueueDescribeCmd(flags),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// queueGVK is the apiVersion/kind pair for the Queue CRD, matching queueGVR
|
||||
// (kmsvc.io/v1, plural "queues") by standard k8s singular-Kind convention.
|
||||
const (
|
||||
queueAPIVersion = "kmsvc.io/v1"
|
||||
queueKind = "Queue"
|
||||
)
|
||||
|
||||
func newQueueCreateCmd(flags *globalFlags) *cobra.Command {
|
||||
var namespace string
|
||||
var setFields []string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create-queue [name]",
|
||||
Short: "Create a Queue CRD (operator defaults apply unless overridden with --set)",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cl, err := newDynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spec := map[string]any{}
|
||||
for _, kv := range setFields {
|
||||
if err := applySetField(spec, kv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
obj := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": queueAPIVersion,
|
||||
"kind": queueKind,
|
||||
"metadata": map[string]any{
|
||||
"name": args[0],
|
||||
"namespace": namespace,
|
||||
},
|
||||
"spec": spec,
|
||||
}}
|
||||
|
||||
created, err := cl.Resource(queueGVR).Namespace(namespace).Create(cmd.Context(), obj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("create queue %s: %w", args[0], err)
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "queue/%s created\n", created.GetName())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&namespace, "namespace", "n", "sqs", "namespace to create the Queue CRD in")
|
||||
cmd.Flags().StringArrayVar(&setFields, "set", nil, "override a spec field, key=value (e.g. --set fifoQueue=true), repeatable")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newQueueDeleteCmd(flags *globalFlags) *cobra.Command {
|
||||
var namespace string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete-queue [name]",
|
||||
Short: "Delete a Queue CRD",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cl, err := newDynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cl.Resource(queueGVR).Namespace(namespace).Delete(cmd.Context(), args[0], metav1.DeleteOptions{}); err != nil {
|
||||
return fmt.Errorf("delete queue %s: %w", args[0], err)
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "queue/%s deleted\n", args[0])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&namespace, "namespace", "n", "sqs", "namespace the Queue CRD lives in")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// applySetField parses a "key=value" pair and writes it into spec, coercing
|
||||
// value to bool/int64 when it parses as one, else leaving it as a string —
|
||||
// mirrors helm --set's pragmatic type inference since the CRD schema isn't
|
||||
// known to this CLI.
|
||||
func applySetField(spec map[string]any, kv string) error {
|
||||
key, value, ok := strings.Cut(kv, "=")
|
||||
if !ok {
|
||||
return fmt.Errorf("--set %q: expected key=value", kv)
|
||||
}
|
||||
if b, err := strconv.ParseBool(value); err == nil {
|
||||
spec[key] = b
|
||||
return nil
|
||||
}
|
||||
if i, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
spec[key] = i
|
||||
return nil
|
||||
}
|
||||
spec[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func newQueueListCmd(flags *globalFlags) *cobra.Command {
|
||||
var namespace string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List Queue CRDs in a namespace",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cl, err := newDynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := cl.Resource(queueGVR).Namespace(namespace).List(cmd.Context(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("list queues: %w", err)
|
||||
}
|
||||
|
||||
summaries := make([]queueSummary, 0, len(list.Items))
|
||||
for _, item := range list.Items {
|
||||
summaries = append(summaries, summarizeQueue(item.Object))
|
||||
}
|
||||
return renderQueues(cmd.OutOrStdout(), flags.output, summaries)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&namespace, "namespace", "n", "sqs", "namespace the Queue CRDs live in")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newQueueDescribeCmd(flags *globalFlags) *cobra.Command {
|
||||
var namespace string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "describe [name]",
|
||||
Short: "Show full status (shards, phase) for one Queue CRD",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cl, err := newDynamicClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj, err := cl.Resource(queueGVR).Namespace(namespace).Get(cmd.Context(), args[0], metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get queue %s: %w", args[0], err)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(obj.Object, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(cmd.OutOrStdout(), string(data))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&namespace, "namespace", "n", "sqs", "namespace the Queue CRD lives in")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func summarizeQueue(obj map[string]any) queueSummary {
|
||||
name, _, _ := unstructured.NestedString(obj, "metadata", "name")
|
||||
namespace, _, _ := unstructured.NestedString(obj, "metadata", "namespace")
|
||||
phase, _, _ := unstructured.NestedString(obj, "status", "phase")
|
||||
fifo, _, _ := unstructured.NestedBool(obj, "spec", "fifoQueue")
|
||||
maxReceives, _, _ := unstructured.NestedInt64(obj, "spec", "maxReceiveCount")
|
||||
|
||||
shardCount := 0
|
||||
if shards, ok, _ := unstructured.NestedSlice(obj, "status", "shards"); ok {
|
||||
shardCount = len(shards)
|
||||
}
|
||||
|
||||
return queueSummary{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Phase: phase,
|
||||
FIFO: fifo,
|
||||
ShardCount: shardCount,
|
||||
MaxReceives: maxReceives,
|
||||
}
|
||||
}
|
||||
|
||||
func renderQueues(w io.Writer, format string, queues []queueSummary) error {
|
||||
if format == "json" {
|
||||
return json.NewEncoder(w).Encode(queues)
|
||||
}
|
||||
|
||||
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(tw, "NAME\tNAMESPACE\tPHASE\tFIFO\tSHARDS\tMAX_RECEIVES")
|
||||
for _, q := range queues {
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%t\t%d\t%d\n", q.Name, q.Namespace, q.Phase, q.FIFO, q.ShardCount, q.MaxReceives)
|
||||
}
|
||||
return tw.Flush()
|
||||
}
|
||||
Reference in New Issue
Block a user