Skip to content
Closed
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
8 changes: 3 additions & 5 deletions cli/cmd/create.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package cmd

import (
"encoding/json"
"fmt"

"github.com/spf13/cobra"

"github.com/openkcm/krypton/cli/output"
"github.com/openkcm/krypton/pkg/api/admin"
)

Expand All @@ -30,17 +30,15 @@ func createTenantCmd() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
c := admin.NewClient(serverURL)

tenant, err := c.CreateTenant(cmd.Context(), admin.CreateTenantRequest{
resp, err := c.CreateTenant(cmd.Context(), admin.CreateTenantRequest{
Name: name,
Labels: labels,
})
if err != nil {
return fmt.Errorf("failed to create tenant: %w", err)
}

enc := json.NewEncoder(cmd.OutOrStdout())
enc.SetIndent("", " ")
return enc.Encode(tenant)
return output.PrintTable(cmd.OutOrStdout(), resp)
},
}

Expand Down
30 changes: 24 additions & 6 deletions cli/cmd/get.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"

"github.com/spf13/cobra"

"github.com/openkcm/krypton/cli/output"
"github.com/openkcm/krypton/pkg/api/admin"
)

func getCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "get",
Short: "Get a resource",
Short: "Get a resource or a list of resources",
}

cmd.AddCommand(getTenantCmd())
cmd.AddCommand(getTenantsCmd())

return cmd
}
Expand All @@ -29,7 +30,7 @@ func getTenantCmd() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
c := admin.NewClient(serverURL)

tenant, err := c.GetTenant(cmd.Context(), admin.GetTenantRequest{
resp, err := c.GetTenant(cmd.Context(), admin.GetTenantRequest{
ID: args[0],
})
if err != nil {
Expand All @@ -39,9 +40,26 @@ func getTenantCmd() *cobra.Command {
return fmt.Errorf("failed to get tenant: %w", err)
}

enc := json.NewEncoder(cmd.OutOrStdout())
enc.SetIndent("", " ")
return enc.Encode(tenant)
return output.PrintTable(cmd.OutOrStdout(), resp)
},
}

return cmd
}

func getTenantsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "tenants",
Short: "Get all tenants",
RunE: func(cmd *cobra.Command, args []string) error {
c := admin.NewClient(serverURL)

resp, err := c.ListTenants(cmd.Context(), admin.ListTenantsRequest{})
if err != nil {
return fmt.Errorf("failed to list tenants: %w", err)
}

return output.PrintTable(cmd.OutOrStdout(), resp)
},
}

Expand Down
7 changes: 7 additions & 0 deletions cli/output/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package output

// FormatRelativeTime is exported for testing.
var FormatRelativeTime = formatRelativeTime

// FormatLabels is exported for testing.
var FormatLabels = formatLabels
38 changes: 38 additions & 0 deletions cli/output/labels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package output

import (
"sort"
"strings"
)

// formatLabels formats labels as "key1=val1,key2=val2" and truncates
// to maxLen with "..." suffix if necessary.
func formatLabels(labels map[string]string, maxLen int) string {
if len(labels) == 0 {
return "<none>"
}

// Sort keys for consistent output
keys := make([]string, 0, len(labels))
for k := range labels {
keys = append(keys, k)
}
sort.Strings(keys)

// Build label string
var sb strings.Builder
for i, k := range keys {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(k)
sb.WriteString("=")
sb.WriteString(labels[k])
}

result := sb.String()
if len(result) > maxLen {
return result[:maxLen-3] + "..."
}
return result
}
65 changes: 65 additions & 0 deletions cli/output/labels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package output_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/openkcm/krypton/cli/output"
)

func TestFormatLabels(t *testing.T) {
tests := []struct {
name string
labels map[string]string
maxLen int
expected string
}{
{
name: "nil labels returns <none>",
labels: nil,
maxLen: 25,
expected: "<none>",
},
{
name: "empty labels returns <none>",
labels: map[string]string{},
maxLen: 25,
expected: "<none>",
},
{
name: "single label",
labels: map[string]string{"env": "prod"},
maxLen: 25,
expected: "env=prod",
},
{
name: "multiple labels sorted alphabetically",
labels: map[string]string{"env": "prod", "app": "web"},
maxLen: 25,
expected: "app=web,env=prod",
},
{
name: "does not truncate when exactly at maxLen",
labels: map[string]string{"a": "12345678901234567890123"},
maxLen: 25,
expected: "a=12345678901234567890123", // exactly 25 chars, not truncated
},
{
name: "truncates when one char over maxLen",
labels: map[string]string{"a": "123456789012345678901234"},
maxLen: 25,
expected: "a=12345678901234567890...", // 26 chars -> truncated to 22 + "..." = 25
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// when
result := output.FormatLabels(tt.labels, tt.maxLen)

// then
assert.Equal(t, tt.expected, result)
})
}
}
133 changes: 133 additions & 0 deletions cli/output/table.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package output

import (
"errors"
"io"
"strings"
"text/tabwriter"

"github.com/openkcm/krypton/pkg/api/admin"
)

var ErrUnsupportedResponse = errors.New("unsupported response type")

// Table represents a type that can be displayed in table format.
type Table interface {
// Header returns the column headers for the table.
Header() []string
// Rows returns the rows of data for the table.
Rows() [][]string
}

// PrintTable writes the given value to the writer in table format.
func PrintTable(w io.Writer, v any) error {
var table Table

switch val := v.(type) {
case admin.CreateTenantResponse:
table = fromCreateTenantResponse(val)
case admin.GetTenantResponse:
table = fromGetTenantResponse(val)
case admin.ListTenantsResponse:
table = fromListTenantsResponse(val)
default:
return ErrUnsupportedResponse
}

return printTable(w, table)
}

func printTable(w io.Writer, t Table) error {
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)

rows := append([][]string{t.Header()}, t.Rows()...)
for _, row := range rows {
for i, cell := range row {
if i > 0 {
err := write(tw, "\t")
if err != nil {
return err
}
}
err := write(tw, cell)
if err != nil {
return err
}
}
err := write(tw, "\n")
if err != nil {
return err
}
}

return tw.Flush()
}

func write(w io.Writer, s string) error {
_, err := w.Write([]byte(s))
return err
}

// NamedRow represents a parsed table row with column values accessible by header name.
type NamedRow map[string]string

// ParsedTable holds parsed table output with header-keyed row access.
type ParsedTable struct {
Header []string
Rows []NamedRow
}

// ParseTable parses table output bytes into a ParsedTable structure.
// This function is primarily intended for testing purposes,
// allowing tests to verify table output structure and column ordering.
// It assumes column headers do not contain spaces.
func ParseTable(output []byte) ParsedTable {
lines := strings.Split(string(output), "\n")
if len(lines) == 0 {
return ParsedTable{}
}

headerLine := lines[0]
headers := strings.Fields(headerLine)

if len(lines) == 1 {
return ParsedTable{
Header: headers,
}
}

pos := make([]int, len(headers))
for i, h := range headers {
pos[i] = strings.Index(headerLine, h)
}

rows := make([]NamedRow, 0, len(lines)-1)
for _, line := range lines[1:] {
if line == "" {
continue
}
rows = append(rows, parseRow(line, headers, pos))
}

return ParsedTable{
Header: headers,
Rows: rows,
}
}

// parseRow extracts column values based on header positions and returns a Row keyed by header name.
func parseRow(line string, headers []string, pos []int) NamedRow {
row := make(NamedRow, len(pos))
for i, start := range pos {
end := len(line)
if i+1 < len(pos) {
end = pos[i+1]
}
value := ""
if start < len(line) {
value = strings.TrimSpace(line[start:end])
}
row[headers[i]] = value
}
return row
}
54 changes: 54 additions & 0 deletions cli/output/tenant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package output

import (
"github.com/openkcm/krypton/pkg/api/admin"
)

const labelMaxLen = 25

// TenantTable implements the Table interface for displaying tenant data.
type TenantTable struct {
TenantRows [][]string
}

// Header returns the column headers for tenant table output.
func (tt TenantTable) Header() []string {
return []string{"ID", "NAME", "LABELS", "CREATED", "UPDATED"}
}

// Rows returns the rows of tenant data for table output.
func (tt TenantTable) Rows() [][]string {
return tt.TenantRows
}

func fromCreateTenantResponse(r admin.CreateTenantResponse) TenantTable {
return TenantTable{
TenantRows: [][]string{tenantToRow(r.Tenant)},
}
}

func fromGetTenantResponse(r admin.GetTenantResponse) TenantTable {
return TenantTable{
TenantRows: [][]string{tenantToRow(r.Tenant)},
}
}

func fromListTenantsResponse(r admin.ListTenantsResponse) TenantTable {
rows := make([][]string, len(r.Tenants))
for i, t := range r.Tenants {
rows[i] = tenantToRow(t)
}
return TenantTable{
TenantRows: rows,
}
}

func tenantToRow(t admin.Tenant) []string {
return []string{
t.ID,
t.Name,
formatLabels(t.Labels, labelMaxLen),
formatRelativeTime(t.CreatedAt),
formatRelativeTime(t.UpdatedAt),
}
}
Loading
Loading