talos/cmd/osctl/pkg/client/client.go
Andrey Smirnov f5969d2c6c
fix(osctl): avoid panic on empty 'talosconfig' (#725)
When talosconfig doesn't exist, `osctl` creates empty one behind the
scenes, but that leads to immediate panic if the command tries to build
osd client:

```
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x11d6786]

goroutine 1 [running]:
github.com/talos-systems/talos/cmd/osctl/pkg/client.NewDefaultClientCredentials(0x7ffd720f5100, 0xb, 0xc000559ce8, 0x757014, 0xc0000d5500)
	/src/cmd/osctl/pkg/client/client.go:50 +0xa6
github.com/talos-systems/talos/cmd/osctl/cmd.setupClient(0x16ca3f0)
	/src/cmd/osctl/cmd/root.go:100 +0x3d
github.com/talos-systems/talos/cmd/osctl/cmd.glob..func22(0x24ad7c0, 0xc00058c240, 0x0, 0x3)
	/src/cmd/osctl/cmd/ps.go:32 +0x37
github.com/spf13/cobra.(*Command).execute(0x24ad7c0, 0xc0005f8a00, 0x3, 0x4, 0x24ad7c0, 0xc0005f8a00)
	/toolchain/gopath/pkg/mod/github.com/spf13/cobra@v0.0.3/command.go:766 +0x2ae
github.com/spf13/cobra.(*Command).ExecuteC(0x24ae140, 0x2507030, 0x162f2d7, 0xb)
	/toolchain/gopath/pkg/mod/github.com/spf13/cobra@v0.0.3/command.go:852 +0x2ec
github.com/spf13/cobra.(*Command).Execute(...)
	/toolchain/gopath/pkg/mod/github.com/spf13/cobra@v0.0.3/command.go:800
github.com/talos-systems/talos/cmd/osctl/cmd.Execute()
	/src/cmd/osctl/cmd/root.go:93 +0x24f
main.main()
	/src/cmd/osctl/main.go:10 +0x20
```

Fix that by returning explicit error:

```
error getting client credentials: 'context' key is not set in the config
```

Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
2019-06-07 17:40:28 +03:00

256 lines
7.0 KiB
Go

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package client
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/gob"
"fmt"
"github.com/golang/protobuf/ptypes/empty"
"github.com/talos-systems/talos/cmd/osctl/pkg/client/config"
initproto "github.com/talos-systems/talos/internal/app/init/proto"
"github.com/talos-systems/talos/internal/app/osd/proto"
"github.com/talos-systems/talos/internal/pkg/proc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
// Credentials represents the set of values required to initialize a vaild
// Client.
type Credentials struct {
Target string
ca []byte
crt []byte
key []byte
}
// Client implements the proto.OSDClient interface. It serves as the
// concrete type with the required methods.
type Client struct {
conn *grpc.ClientConn
client proto.OSDClient
initClient initproto.InitClient
}
// NewDefaultClientCredentials initializes ClientCredentials using default paths
// to the required CA, certificate, and key.
func NewDefaultClientCredentials(p string) (creds *Credentials, err error) {
c, err := config.Open(p)
if err != nil {
return
}
if c.Context == "" {
return nil, fmt.Errorf("'context' key is not set in the config")
}
context := c.Contexts[c.Context]
if context == nil {
return nil, fmt.Errorf("context %q is not defined in 'contexts' key in config", c.Context)
}
caBytes, err := base64.StdEncoding.DecodeString(context.CA)
if err != nil {
return
}
crtBytes, err := base64.StdEncoding.DecodeString(context.Crt)
if err != nil {
return
}
keyBytes, err := base64.StdEncoding.DecodeString(context.Key)
if err != nil {
return
}
creds = &Credentials{
Target: context.Target,
ca: caBytes,
crt: crtBytes,
key: keyBytes,
}
return creds, nil
}
// NewClient initializes a Client.
func NewClient(port int, clientcreds *Credentials) (c *Client, err error) {
grpcOpts := []grpc.DialOption{}
c = &Client{}
crt, err := tls.X509KeyPair(clientcreds.crt, clientcreds.key)
if err != nil {
return nil, fmt.Errorf("could not load client key pair: %s", err)
}
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM(clientcreds.ca); !ok {
return nil, fmt.Errorf("failed to append client certs")
}
// TODO(andrewrynhard): Do not parse the address. Pass the IP and port in as separate
// parameters.
creds := credentials.NewTLS(&tls.Config{
ServerName: clientcreds.Target,
Certificates: []tls.Certificate{crt},
// Set the root certificate authorities to use the self-signed
// certificate.
RootCAs: certPool,
})
grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(creds))
c.conn, err = grpc.Dial(fmt.Sprintf("%s:%d", clientcreds.Target, port), grpcOpts...)
if err != nil {
return
}
c.client = proto.NewOSDClient(c.conn)
c.initClient = initproto.NewInitClient(c.conn)
return c, nil
}
// Close shuts down client protocol
func (c *Client) Close() error {
return c.conn.Close()
}
// Kubeconfig implements the proto.OSDClient interface.
func (c *Client) Kubeconfig(ctx context.Context) ([]byte, error) {
r, err := c.client.Kubeconfig(ctx, &empty.Empty{})
if err != nil {
return nil, err
}
return r.Bytes, nil
}
// Stats implements the proto.OSDClient interface.
func (c *Client) Stats(ctx context.Context, namespace string) (reply *proto.StatsReply, err error) {
reply, err = c.client.Stats(ctx, &proto.StatsRequest{Namespace: namespace})
return
}
// Processes implements the proto.OSDClient interface.
func (c *Client) Processes(ctx context.Context, namespace string) (reply *proto.ProcessesReply, err error) {
reply, err = c.client.Processes(ctx, &proto.ProcessesRequest{Namespace: namespace})
return
}
// Restart implements the proto.OSDClient interface.
func (c *Client) Restart(ctx context.Context, namespace, id string, timeoutSecs int32) (err error) {
_, err = c.client.Restart(ctx, &proto.RestartRequest{
Id: id,
Namespace: namespace,
Timeout: timeoutSecs,
})
return
}
// Reset implements the proto.OSDClient interface.
func (c *Client) Reset(ctx context.Context) (err error) {
_, err = c.client.Reset(ctx, &empty.Empty{})
return
}
// Reboot implements the proto.OSDClient interface.
func (c *Client) Reboot(ctx context.Context) (err error) {
_, err = c.initClient.Reboot(ctx, &empty.Empty{})
return
}
// Shutdown implements the proto.OSDClient interface.
func (c *Client) Shutdown(ctx context.Context) (err error) {
_, err = c.initClient.Shutdown(ctx, &empty.Empty{})
return
}
// Dmesg implements the proto.OSDClient interface.
func (c *Client) Dmesg(ctx context.Context) ([]byte, error) {
data, err := c.client.Dmesg(ctx, &empty.Empty{})
if err != nil {
return nil, err
}
return data.Bytes, nil
}
// Logs implements the proto.OSDClient interface.
func (c *Client) Logs(ctx context.Context, namespace, id string) (stream proto.OSD_LogsClient, err error) {
stream, err = c.client.Logs(ctx, &proto.LogsRequest{
Namespace: namespace,
Id: id,
})
return
}
// Version implements the proto.OSDClient interface.
func (c *Client) Version(ctx context.Context) ([]byte, error) {
data, err := c.client.Version(ctx, &empty.Empty{})
if err != nil {
return nil, err
}
return data.Bytes, nil
}
// Routes implements the proto.OSDClient interface.
func (c *Client) Routes(ctx context.Context) (reply *proto.RoutesReply, err error) {
reply, err = c.client.Routes(ctx, &empty.Empty{})
return
}
// Top implements the proto.OSDClient interface.
func (c *Client) Top(ctx context.Context) (pl []proc.ProcessList, err error) {
var reply *proto.TopReply
reply, err = c.client.Top(ctx, &empty.Empty{})
if err != nil {
return
}
buf := bytes.NewBuffer(reply.ProcessList.Bytes)
dec := gob.NewDecoder(buf)
err = dec.Decode(&pl)
return
}
// DF implements the proto.OSDClient interface.
func (c *Client) DF(ctx context.Context) (*proto.DFReply, error) {
return c.client.DF(ctx, &empty.Empty{})
}
// Upgrade initiates a Talos upgrade ... and implements the proto.OSDClient
// interface
func (c *Client) Upgrade(ctx context.Context, asseturl string) (string, error) {
reply, err := c.initClient.Upgrade(ctx, &initproto.UpgradeRequest{Url: asseturl})
if err != nil {
return "", err
}
return reply.Ack, nil
}
// ServiceList returns list of services with their state
func (c *Client) ServiceList(ctx context.Context) (*initproto.ServiceListReply, error) {
return c.initClient.ServiceList(ctx, &empty.Empty{})
}
// ServiceInfo returns info about a single service
//
// This is implemented via service list API, as we don't have many services
// If service with given id is not registered, function returns nil
func (c *Client) ServiceInfo(ctx context.Context, id string) (*initproto.ServiceInfo, error) {
reply, err := c.initClient.ServiceList(ctx, &empty.Empty{})
if err != nil {
return nil, err
}
for _, svc := range reply.Services {
if svc.Id == id {
return svc, nil
}
}
return nil, nil
}