Skip to content

Commit

Permalink
Add tenant subcommand (#173)
Browse files Browse the repository at this point in the history
  • Loading branch information
majst01 authored Sep 8, 2023
1 parent a4907b7 commit 89d1ed6
Show file tree
Hide file tree
Showing 18 changed files with 1,121 additions and 15 deletions.
18 changes: 18 additions & 0 deletions cmd/completion/tenant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package completion

import (
"github.com/metal-stack/metal-go/api/client/tenant"
"github.com/spf13/cobra"
)

func (c *Completion) TenantListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
resp, err := c.client.Tenant().ListTenants(tenant.NewListTenantsParams(), nil)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, p := range resp.Payload {
names = append(names, p.Meta.ID+"\t/"+p.Name)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ metalctl machine list -o template --template "{{ .id }}:{{ .size.id }}"
rootCmd.AddCommand(newMachineCmd(c))
rootCmd.AddCommand(newFirewallCmd(c))
rootCmd.AddCommand(newProjectCmd(c))
rootCmd.AddCommand(newTenantCmd(c))
rootCmd.AddCommand(newSizeCmd(c))
rootCmd.AddCommand(newFilesystemLayoutCmd(c))
rootCmd.AddCommand(newImageCmd(c))
Expand Down
21 changes: 21 additions & 0 deletions cmd/sorters/tenant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package sorters

import (
"github.com/metal-stack/metal-go/api/models"
"github.com/metal-stack/metal-lib/pkg/multisort"
p "github.com/metal-stack/metal-lib/pkg/pointer"
)

func TenantSorter() *multisort.Sorter[*models.V1TenantResponse] {
return multisort.New(multisort.FieldMap[*models.V1TenantResponse]{
"id": func(a, b *models.V1TenantResponse, descending bool) multisort.CompareResult {
return multisort.Compare(p.SafeDeref(a.Meta).ID, p.SafeDeref(b.Meta).ID, descending)
},
"name": func(a, b *models.V1TenantResponse, descending bool) multisort.CompareResult {
return multisort.Compare(a.Name, b.Name, descending)
},
"description": func(a, b *models.V1TenantResponse, descending bool) multisort.CompareResult {
return multisort.Compare(a.Description, b.Description, descending)
},
}, multisort.Keys{{ID: "id"}})
}
4 changes: 4 additions & 0 deletions cmd/tableprinters/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin
return t.ProjectTable(pointer.WrapInSlice(d), wide)
case []*models.V1ProjectResponse:
return t.ProjectTable(d, wide)
case *models.V1TenantResponse:
return t.TenantTable(pointer.WrapInSlice(d), wide)
case []*models.V1TenantResponse:
return t.TenantTable(d, wide)
case []*models.V1MachineIPMIResponse:
return t.MachineIPMITable(d, wide)
case *models.V1MachineIPMIResponse:
Expand Down
66 changes: 66 additions & 0 deletions cmd/tableprinters/tenant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tableprinters

import (
"strconv"
"strings"

"github.com/metal-stack/metal-go/api/models"
"github.com/metal-stack/metal-lib/pkg/genericcli"
)

func (t *TablePrinter) TenantTable(data []*models.V1TenantResponse, wide bool) ([]string, [][]string, error) {
var (
rows [][]string
)

header := []string{"ID", "Name", "Description", "Labels", "Annotations"}
if wide {
header = []string{"ID", "Name", "Description", "Labels", "Annotations", "Quotas"}
}

for _, pr := range data {
var (
clusterQuota = "∞"
machineQuota = "∞"
ipQuota = "∞"
)

if pr.Quotas != nil {
qs := pr.Quotas
if qs.Cluster != nil {
if qs.Cluster.Quota != 0 {
clusterQuota = strconv.FormatInt(int64(qs.Cluster.Quota), 10)
}
}
if qs.Machine != nil {
if qs.Machine.Quota != 0 {
machineQuota = strconv.FormatInt(int64(qs.Machine.Quota), 10)
}
}
if qs.IP != nil {
if qs.IP.Quota != 0 {
ipQuota = strconv.FormatInt(int64(qs.IP.Quota), 10)
}
}
}

quotas := []string{
clusterQuota + " Cluster(s)",
machineQuota + " Machine(s)",
ipQuota + " IP(s)",
}

labels := strings.Join(pr.Meta.Labels, "\n")

as := genericcli.MapToLabels(pr.Meta.Annotations)
annotations := strings.Join(as, "\n")

if wide {
rows = append(rows, []string{pr.Meta.ID, pr.Name, pr.Description, labels, annotations, strings.Join(quotas, "\n")})
} else {
rows = append(rows, []string{pr.Meta.ID, pr.Name, pr.Description, labels, annotations})
}
}

return header, rows, nil
}
210 changes: 210 additions & 0 deletions cmd/tenant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package cmd

import (
"errors"
"fmt"

tenantmodel "github.com/metal-stack/metal-go/api/client/tenant"
"github.com/metal-stack/metal-go/api/models"
"github.com/metal-stack/metal-lib/pkg/genericcli"
"github.com/metal-stack/metal-lib/pkg/genericcli/printers"
"github.com/metal-stack/metalctl/cmd/sorters"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

type tenantCmd struct {
*config
}

func newTenantCmd(c *config) *cobra.Command {
w := tenantCmd{
config: c,
}

cmdsConfig := &genericcli.CmdsConfig[*models.V1TenantCreateRequest, *models.V1TenantUpdateRequest, *models.V1TenantResponse]{
BinaryName: binaryName,
GenericCLI: genericcli.NewGenericCLI[*models.V1TenantCreateRequest, *models.V1TenantUpdateRequest, *models.V1TenantResponse](w).WithFS(c.fs),
Singular: "tenant",
Plural: "tenants",
Description: "a tenant belongs to a tenant and groups together entities in metal-stack.",
Sorter: sorters.TenantSorter(),
ValidArgsFn: c.comp.TenantListCompletion,
DescribePrinter: func() printers.Printer { return c.describePrinter },
ListPrinter: func() printers.Printer { return c.listPrinter },
CreateRequestFromCLI: w.createFromCLI,
CreateCmdMutateFn: func(cmd *cobra.Command) {
cmd.Flags().String("id", "", "id of the tenant, max 10 characters.")
cmd.Flags().String("name", "", "name of the tenant, max 10 characters.")
cmd.Flags().String("description", "", "description of the tenant.")
cmd.Flags().StringSlice("labels", nil, "add initial label, can be given multiple times to add multiple labels, e.g. --label=foo --label=bar")
cmd.Flags().StringSlice("annotations", nil, "add initial annotations, must be in the form of key=value, can be given multiple times to add multiple annotations, e.g. --annotation key=value --annotation foo=bar")
cmd.Flags().Int32("cluster-quota", 0, "cluster quota")
cmd.Flags().Int32("machine-quota", 0, "machine quota")
cmd.Flags().Int32("ip-quota", 0, "ip quota")

cmd.MarkFlagsMutuallyExclusive("file", "name")
cmd.MarkFlagsRequiredTogether("name", "description")
},
ListCmdMutateFn: func(cmd *cobra.Command) {
cmd.Flags().StringP("name", "", "", "Name of the tenant.")
cmd.Flags().StringP("id", "", "", "ID of the tenant.")
cmd.Flags().StringSliceP("annotations", "", []string{}, "annotations")
},
}

return genericcli.NewCmds(cmdsConfig)
}

func (c tenantCmd) Get(id string) (*models.V1TenantResponse, error) {
resp, err := c.client.Tenant().GetTenant(tenantmodel.NewGetTenantParams().WithID(id), nil)
if err != nil {
return nil, err
}

return resp.Payload, nil
}

func (c tenantCmd) List() ([]*models.V1TenantResponse, error) {
var annotations map[string]string
if viper.IsSet("annotations") {
var err error
annotations, err = genericcli.LabelsToMap(viper.GetStringSlice("annotations"))
if err != nil {
return nil, err
}
}

resp, err := c.client.Tenant().FindTenants(tenantmodel.NewFindTenantsParams().WithBody(&models.V1TenantFindRequest{
ID: viper.GetString("id"),
Name: viper.GetString("name"),
Annotations: annotations,
}), nil)
if err != nil {
return nil, err
}

return resp.Payload, nil
}

func (c tenantCmd) Delete(id string) (*models.V1TenantResponse, error) {
resp, err := c.client.Tenant().DeleteTenant(tenantmodel.NewDeleteTenantParams().WithID(id), nil)
if err != nil {
return nil, err
}

return resp.Payload, nil
}

func (c tenantCmd) Create(rq *models.V1TenantCreateRequest) (*models.V1TenantResponse, error) {
resp, err := c.client.Tenant().CreateTenant(tenantmodel.NewCreateTenantParams().WithBody(rq), nil)
if err != nil {
var r *tenantmodel.CreateTenantConflict
if errors.As(err, &r) {
return nil, genericcli.AlreadyExistsError()
}
return nil, err
}

return resp.Payload, nil
}

func (c tenantCmd) Update(rq *models.V1TenantUpdateRequest) (*models.V1TenantResponse, error) {
if rq.Meta == nil {
return nil, fmt.Errorf("tenant meta is nil")
}

getResp, err := c.Get(rq.Meta.ID)
if err != nil {
return nil, err
}

rq.Meta.Version = getResp.Meta.Version

updateResp, err := c.client.Tenant().UpdateTenant(tenantmodel.NewUpdateTenantParams().WithBody(rq), nil)
if err != nil {
return nil, err
}

return updateResp.Payload, nil
}

func (c tenantCmd) Convert(r *models.V1TenantResponse) (string, *models.V1TenantCreateRequest, *models.V1TenantUpdateRequest, error) {
if r.Meta == nil {
return "", nil, nil, fmt.Errorf("meta is nil")
}
return r.Meta.ID, tenantResponseToCreate(r), tenantResponseToUpdate(r), nil
}

func tenantResponseToCreate(r *models.V1TenantResponse) *models.V1TenantCreateRequest {
return &models.V1TenantCreateRequest{
Meta: &models.V1Meta{
Apiversion: r.Meta.Apiversion,
Kind: r.Meta.Kind,
ID: r.Meta.ID,
Annotations: r.Meta.Annotations,
Labels: r.Meta.Labels,
Version: r.Meta.Version,
},
Description: r.Description,
Name: r.Name,
Quotas: r.Quotas,
}
}

func tenantResponseToUpdate(r *models.V1TenantResponse) *models.V1TenantUpdateRequest {
return &models.V1TenantUpdateRequest{
Name: r.Name,
Meta: &models.V1Meta{
Apiversion: r.Meta.Apiversion,
Kind: r.Meta.Kind,
ID: r.Meta.ID,
Annotations: r.Meta.Annotations,
Labels: r.Meta.Labels,
Version: r.Meta.Version,
},
Description: r.Description,
IamConfig: r.IamConfig,
DefaultQuotas: r.DefaultQuotas,
Quotas: r.Quotas,
}
}

func (w *tenantCmd) createFromCLI() (*models.V1TenantCreateRequest, error) {
var (
clusterQuota, machineQuota, ipQuota *models.V1Quota
)
if viper.IsSet("cluster-quota") {
clusterQuota = &models.V1Quota{Quota: viper.GetInt32("cluster-quota")}
}
if viper.IsSet("machine-quota") {
machineQuota = &models.V1Quota{Quota: viper.GetInt32("machine-quota")}
}
if viper.IsSet("ip-quota") {
ipQuota = &models.V1Quota{Quota: viper.GetInt32("ip-quota")}
}

annotations, err := genericcli.LabelsToMap(viper.GetStringSlice("annotations"))
if err != nil {
return nil, err
}

return &models.V1TenantCreateRequest{
Name: viper.GetString("name"),
Description: viper.GetString("description"),
Quotas: &models.V1QuotaSet{
Cluster: clusterQuota,
Machine: machineQuota,
IP: ipQuota,
},
Meta: &models.V1Meta{
Kind: "Tenant",
Apiversion: "v1",
Annotations: annotations,
Labels: viper.GetStringSlice("labels"),
ID: viper.GetString("id"),
},
},
nil
}
Loading

0 comments on commit 89d1ed6

Please sign in to comment.