blob: eea5c01d22db396667b3c55a302c0808287d4fef [file] [log] [blame]
// Package raw wraps SSH commands necessary for K8s inspection.
package raw
import (
"bytes"
"fmt"
"io/ioutil"
"os/user"
"path/filepath"
"golang.org/x/crypto/ssh"
kh "golang.org/x/crypto/ssh/knownhosts"
"check"
"check/config"
)
const (
controlplane = "controlplane"
etcd = "etcd"
worker = "worker"
knownHostsFile = "~/.ssh/known_hosts"
)
// Raw implements Informer interface.
type Raw struct {
check.Informer
}
// GetAPIParams returns parameters of running Kubernetes API servers.
// It queries only cluster nodes with "controlplane" role.
func (r *Raw) GetAPIParams() ([]string, error) {
return getProcessParams(check.APIProcess)
}
// GetSchedulerParams returns parameters of running Kubernetes scheduler.
// It queries only cluster nodes with "controlplane" role.
func (r *Raw) GetSchedulerParams() ([]string, error) {
return getProcessParams(check.SchedulerProcess)
}
// GetControllerManagerParams returns parameters of running Kubernetes scheduler.
// It queries only cluster nodes with "controlplane" role.
func (r *Raw) GetControllerManagerParams() ([]string, error) {
return getProcessParams(check.ControllerManagerProcess)
}
// GetEtcdParams returns parameters of running etcd.
// It queries only cluster nodes with "controlplane" role.
func (r *Raw) GetEtcdParams() ([]string, error) {
return []string{}, check.ErrNotImplemented
}
func getProcessParams(process check.Command) ([]string, error) {
nodes, err := config.GetNodesInfo()
if err != nil {
return []string{}, err
}
for _, node := range nodes {
if isControlplaneNode(node.Role) {
cmd, err := getInspectCmdOutput(node, process)
if err != nil {
return []string{}, err
}
cmd = trimOutput(cmd) // TODO: improve `docker inspect` query format.
if len(cmd) > 0 {
i := bytes.Index(cmd, []byte(process.String()))
if i == -1 {
return []string{}, fmt.Errorf("missing %s command", process)
}
return btos(cmd[i+len(process.String()):]), nil
}
}
}
return []string{}, nil
}
func isControlplaneNode(roles []string) bool {
for _, role := range roles {
if role == controlplane {
return true
}
}
return false
}
func getInspectCmdOutput(node config.NodeInfo, cmd check.Command) ([]byte, error) {
path, err := expandPath(node.SSHKeyPath)
if err != nil {
return nil, err
}
pubKey, err := parsePublicKey(path)
if err != nil {
return nil, err
}
khPath, err := expandPath(knownHostsFile)
if err != nil {
return nil, err
}
hostKeyCallback, err := kh.New(khPath)
if err != nil {
return nil, err
}
config := &ssh.ClientConfig{
User: node.User,
Auth: []ssh.AuthMethod{pubKey},
HostKeyCallback: hostKeyCallback,
}
conn, err := ssh.Dial("tcp", node.Address+":"+node.Port, config)
if err != nil {
return nil, err
}
defer conn.Close()
out, err := runCommand(fmt.Sprintf("docker inspect %s --format {{.Args}}", cmd), conn)
if err != nil {
return nil, err
}
return out, nil
}
func expandPath(path string) (string, error) {
if len(path) == 0 || path[0] != '~' {
return path, nil
}
usr, err := user.Current()
if err != nil {
return "", err
}
return filepath.Join(usr.HomeDir, path[1:]), nil
}
func parsePublicKey(path string) (ssh.AuthMethod, error) {
key, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return nil, err
}
return ssh.PublicKeys(signer), nil
}
func runCommand(cmd string, conn *ssh.Client) ([]byte, error) {
sess, err := conn.NewSession()
if err != nil {
return nil, err
}
defer sess.Close()
out, err := sess.Output(cmd)
if err != nil {
return nil, err
}
return out, nil
}
// trimOutput removes trailing new line and brackets from output.
func trimOutput(b []byte) []byte {
b = bytes.TrimSpace(b)
b = bytes.TrimPrefix(b, []byte("["))
b = bytes.TrimSuffix(b, []byte("]"))
return b
}
// btos converts slice of bytes to slice of strings split by white space characters.
func btos(in []byte) []string {
var out []string
for _, b := range bytes.Fields(in) {
out = append(out, string(b))
}
return out
}