Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bootstrap Get command for tink-cli #406

Merged
merged 14 commits into from
Jan 25, 2021
Merged
3 changes: 3 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export TINKERBELL_GRPC_AUTHORITY=127.0.0.1:42113
export TINKERBELL_CERT_URL=http://127.0.0.1:42114/cert

which nix &>/dev/null && use nix && unset GOPATH
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,7 @@ test:
verify:
goimports -d .
golint ./...

protos/gen_mock:
go generate ./protos/**/*
goimports -w ./protos/**/mock.go
Comment on lines +58 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great if these were proper deps for the necessary binaries so that they would always be up to date. Doesn't have to be now, I really want to see this PR done with :D. I can open up an issue when this PR is merged though

78 changes: 78 additions & 0 deletions client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"

"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/tinkerbell/tink/protos/events"
"github.com/tinkerbell/tink/protos/hardware"
"github.com/tinkerbell/tink/protos/template"
Expand All @@ -24,6 +25,83 @@ var (
EventsClient events.EventsServiceClient
)

// FullClient aggregates all the grpc clients available from Tinkerbell Server
type FullClient struct {
TemplateClient template.TemplateServiceClient
WorkflowClient workflow.WorkflowServiceClient
HardwareClient hardware.HardwareServiceClient
EventsClient events.EventsServiceClient
}

// NewFullClientFromGlobal is a dirty hack that returns a FullClient using the
// global variables exposed by the client package. Globals should be avoided
// and we will deprecated them at some point replacing this function with
// NewFullClient. If you are strating a new project please use the last one
func NewFullClientFromGlobal() (*FullClient, error) {
// This is required because we use init() too often, even more in the
// CLI and based on where you are sometime the clients are not initialised
if TemplateClient == nil {
err := Setup()
if err != nil {
panic(err)
}
}
return &FullClient{
TemplateClient: TemplateClient,
WorkflowClient: WorkflowClient,
HardwareClient: HardwareClient,
EventsClient: EventsClient,
}, nil
}

// NewFullClient returns a FullClient. A structure that contains all the
// clients made available from tink-server. This is the function you should use
// instead of NewFullClientFromGlobal that will be deprecated soon
func NewFullClient(conn grpc.ClientConnInterface) *FullClient {
return &FullClient{
TemplateClient: template.NewTemplateServiceClient(conn),
WorkflowClient: workflow.NewWorkflowServiceClient(conn),
HardwareClient: hardware.NewHardwareServiceClient(conn),
EventsClient: events.NewEventsServiceClient(conn),
}
}

type ConnOptions struct {
CertURL string
GRPCAuthority string
}

func (o *ConnOptions) SetFlags(flagSet *pflag.FlagSet) {
flagSet.StringVar(&o.CertURL, "tinkerbell-cert-url", "http://127.0.0.1:42114/cert", "The URL where the certificate is located")
flagSet.StringVar(&o.GRPCAuthority, "tinkerbell-grpc-authority", "127.0.0.1:42113", "Link to tink-server grcp api")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
flagSet.StringVar(&o.GRPCAuthority, "tinkerbell-grpc-authority", "127.0.0.1:42113", "Link to tink-server grcp api")
flagSet.StringVar(&o.GRPCAuthority, "tinkerbell-grpc-authority", "127.0.0.1:42113", "Link to tink-server grpc api")

Sigh I do this all the time

}

func NewClientConn(opt *ConnOptions) (*grpc.ClientConn, error) {
resp, err := http.Get(opt.CertURL)
if err != nil {
return nil, errors.Wrap(err, "fetch cert")
}
defer resp.Body.Close()

certs, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read cert")
}

cp := x509.NewCertPool()
ok := cp.AppendCertsFromPEM(certs)
if !ok {
return nil, errors.Wrap(err, "parse cert")
}

creds := credentials.NewClientTLSFromCert(cp, "")
conn, err := grpc.Dial(opt.GRPCAuthority, grpc.WithTransportCredentials(creds))
if err != nil {
return nil, errors.Wrap(err, "connect to tinkerbell server")
}
return conn, nil
}

// GetConnection returns a gRPC client connection
func GetConnection() (*grpc.ClientConn, error) {
certURL := os.Getenv("TINKERBELL_CERT_URL")
gianarb marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
5 changes: 5 additions & 0 deletions cmd/tink-cli/cmd/get/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Get is a reusable implementation of the Get command for the tink cli. The
// Get command lists and filters resources. It supports different kind of
// visualisation and it is designed to be extendible and usable across
// resources.
package get
157 changes: 157 additions & 0 deletions cmd/tink-cli/cmd/get/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package get

import (
"context"
"encoding/json"
"fmt"

"github.com/pkg/errors"

"github.com/jedib0t/go-pretty/table"
"github.com/spf13/cobra"
"github.com/tinkerbell/tink/client"
"google.golang.org/grpc"
)

type Options struct {
// Headers is the list of headers you want to print as part of the list
Headers []string
// RetrieveData reaches out to Tinkerbell and it gets the required data
RetrieveData func(context.Context, *client.FullClient) ([]interface{}, error)
// RetrieveByID is used when a get command has a list of arguments
RetrieveByID func(context.Context, *client.FullClient, string) (interface{}, error)
// PopulateTable populates a table with the data retrieved with the RetrieveData function.
PopulateTable func([]interface{}, table.Writer) error

clientConnOpt *client.ConnOptions
fullClient *client.FullClient

// Format specifies the format you want the list of resources printed
// out. By default it is table but it can be JSON ar CSV.
Format string
// NoHeaders does not print the header line
NoHeaders bool
}

func (o *Options) SetClientConnOpt(co *client.ConnOptions) {
o.clientConnOpt = co
}

func (o *Options) SetFullClient(cl *client.FullClient) {
o.fullClient = cl
}

const shortDescr = `display one or many resources`

const longDescr = `Prints a table containing the most important information about a specific
resource. You can specify the kind of output you want to receive. It can be
table, csv or json.
`

const exampleDescr = `# List all hardware in table output format.
tink hardware get

# List all workflow in csv output format.
tink template get --format csv

# List a single template in json output format.
tink workflow get --format json [id]
`

func NewGetCommand(opt Options) *cobra.Command {
cmd := &cobra.Command{
Use: "get",
Short: shortDescr,
Long: longDescr,
Example: exampleDescr,
DisableFlagsInUseLine: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if opt.fullClient != nil {
return nil
}
if opt.clientConnOpt == nil {
opt.SetClientConnOpt(&client.ConnOptions{})
}
opt.clientConnOpt.SetFlags(cmd.PersistentFlags())
return nil
},
PreRunE: func(cmd *cobra.Command, args []string) error {
if opt.fullClient == nil {
var err error
var conn *grpc.ClientConn
conn, err = client.NewClientConn(opt.clientConnOpt)
if err != nil {
println("Flag based client configuration failed with err: %s. Trying with env var legacy method...", err)
// Fallback to legacy Setup via env var
conn, err = client.GetConnection()
if err != nil {
return errors.Wrap(err, "failed to setup connection to tink-server")
}
}
opt.SetFullClient(client.NewFullClient(conn))
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
var err error
var data []interface{}

t := table.NewWriter()
t.SetOutputMirror(cmd.OutOrStdout())

if len(args) != 0 {
if opt.RetrieveByID == nil {
return errors.New("Get by ID is not implemented for this resource yet. Please have a look at the issue in GitHub or open a new one.")
}
for _, requestedID := range args {
s, err := opt.RetrieveByID(cmd.Context(), opt.fullClient, requestedID)
if err != nil {
continue
}
data = append(data, s)
}
} else {
data, err = opt.RetrieveData(cmd.Context(), opt.fullClient)
}
if err != nil {
return err
}

if !opt.NoHeaders {
header := table.Row{}
for _, h := range opt.Headers {
header = append(header, h)
}
t.AppendHeader(header)
}

// TODO(gianarb): Technically this is not needed for
// all the output formats but for now that's fine
if err := opt.PopulateTable(data, t); err != nil {
return err
}

switch opt.Format {
case "json":
// TODO(gianarb): the table library we use do
// not support JSON right now. I am not even
// sure I like tables! So complicated...
gianarb marked this conversation as resolved.
Show resolved Hide resolved
b, err := json.Marshal(struct {
Data interface{} `json:"data"`
}{Data: data})
gianarb marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
fmt.Fprint(cmd.OutOrStdout(), string(b))
gianarb marked this conversation as resolved.
Show resolved Hide resolved
case "csv":
t.RenderCSV()
default:
t.Render()
}
return nil
},
}
cmd.PersistentFlags().StringVarP(&opt.Format, "format", "", "table", "The format you expect the list to be printed out. Currently supported format are table, JSON and CSV")
cmd.PersistentFlags().BoolVar(&opt.NoHeaders, "no-headers", false, "Table contains an header with the columns' name. You can disable it from being printed out")
return cmd
}
Loading