mirror of https://github.com/k3d-io/k3d
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
317 lines
9.3 KiB
317 lines
9.3 KiB
package run
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/client"
|
|
|
|
"github.com/mitchellh/go-homedir"
|
|
"github.com/olekukonko/tablewriter"
|
|
)
|
|
|
|
const (
|
|
defaultContainerNamePrefix = "k3d"
|
|
)
|
|
|
|
type cluster struct {
|
|
name string
|
|
image string
|
|
status string
|
|
serverPorts []string
|
|
server types.Container
|
|
workers []types.Container
|
|
}
|
|
|
|
// GetContainerName generates the container names
|
|
func GetContainerName(role, clusterName string, postfix int) string {
|
|
if postfix >= 0 {
|
|
return fmt.Sprintf("%s-%s-%s-%d", defaultContainerNamePrefix, clusterName, role, postfix)
|
|
}
|
|
return fmt.Sprintf("%s-%s-%s", defaultContainerNamePrefix, clusterName, role)
|
|
}
|
|
|
|
// GetAllContainerNames returns a list of all containernames that will be created
|
|
func GetAllContainerNames(clusterName string, serverCount, workerCount int) []string {
|
|
names := []string{}
|
|
for postfix := 0; postfix < serverCount; postfix++ {
|
|
names = append(names, GetContainerName("server", clusterName, postfix))
|
|
}
|
|
for postfix := 0; postfix < workerCount; postfix++ {
|
|
names = append(names, GetContainerName("worker", clusterName, postfix))
|
|
}
|
|
return names
|
|
}
|
|
|
|
// createDirIfNotExists checks for the existence of a directory and creates it along with all required parents if not.
|
|
// It returns an error if the directory (or parents) couldn't be created and nil if it worked fine or if the path already exists.
|
|
func createDirIfNotExists(path string) error {
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
return os.MkdirAll(path, os.ModePerm)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// createClusterDir creates a directory with the cluster name under $HOME/.config/k3d/<cluster_name>.
|
|
// The cluster directory will be used e.g. to store the kubeconfig file.
|
|
func createClusterDir(name string) {
|
|
clusterPath, _ := getClusterDir(name)
|
|
if err := createDirIfNotExists(clusterPath); err != nil {
|
|
log.Fatalf("ERROR: couldn't create cluster directory [%s] -> %+v", clusterPath, err)
|
|
}
|
|
}
|
|
|
|
// deleteClusterDir contrary to createClusterDir, this deletes the cluster directory under $HOME/.config/k3d/<cluster_name>
|
|
func deleteClusterDir(name string) {
|
|
clusterPath, _ := getClusterDir(name)
|
|
if err := os.RemoveAll(clusterPath); err != nil {
|
|
log.Printf("WARNING: couldn't delete cluster directory [%s]. You might want to delete it manually.", clusterPath)
|
|
}
|
|
}
|
|
|
|
// getClusterDir returns the path to the cluster directory which is $HOME/.config/k3d/<cluster_name>
|
|
func getClusterDir(name string) (string, error) {
|
|
homeDir, err := homedir.Dir()
|
|
if err != nil {
|
|
log.Printf("ERROR: Couldn't get user's home directory")
|
|
return "", err
|
|
}
|
|
return path.Join(homeDir, ".config", "k3d", name), nil
|
|
}
|
|
|
|
func getClusterKubeConfigPath(cluster string) (string, error) {
|
|
clusterDir, err := getClusterDir(cluster)
|
|
return path.Join(clusterDir, "kubeconfig.yaml"), err
|
|
}
|
|
|
|
func createKubeConfigFile(cluster string) error {
|
|
ctx := context.Background()
|
|
docker, err := client.NewEnvClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filters := filters.NewArgs()
|
|
filters.Add("label", "app=k3d")
|
|
filters.Add("label", fmt.Sprintf("cluster=%s", cluster))
|
|
filters.Add("label", "component=server")
|
|
server, err := docker.ContainerList(ctx, types.ContainerListOptions{
|
|
Filters: filters,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to get server container for cluster %s\n%+v", cluster, err)
|
|
}
|
|
|
|
if len(server) == 0 {
|
|
return fmt.Errorf("No server container for cluster %s", cluster)
|
|
}
|
|
|
|
// get kubeconfig file from container and read contents
|
|
reader, _, err := docker.CopyFromContainer(ctx, server[0].ID, "/output/kubeconfig.yaml")
|
|
if err != nil {
|
|
return fmt.Errorf("ERROR: couldn't copy kubeconfig.yaml from server container %s\n%+v", server[0].ID, err)
|
|
}
|
|
defer reader.Close()
|
|
|
|
readBytes, err := ioutil.ReadAll(reader)
|
|
if err != nil {
|
|
return fmt.Errorf("ERROR: couldn't read kubeconfig from container\n%+v", err)
|
|
}
|
|
|
|
// create destination kubeconfig file
|
|
destPath, err := getClusterKubeConfigPath(cluster)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
kubeconfigfile, err := os.Create(destPath)
|
|
if err != nil {
|
|
return fmt.Errorf("ERROR: couldn't create kubeconfig file %s\n%+v", destPath, err)
|
|
}
|
|
defer kubeconfigfile.Close()
|
|
|
|
// write to file, skipping the first 512 bytes which contain file metadata
|
|
// and trimming any NULL characters
|
|
trimBytes := bytes.Trim(readBytes[512:], "\x00")
|
|
|
|
// Fix up kubeconfig.yaml file.
|
|
//
|
|
// K3s generates the default kubeconfig.yaml with host name as 'localhost'.
|
|
// Change the host name to the name user specified via the --api-port argument.
|
|
//
|
|
// When user did not specify the host name and when we are running against a remote docker,
|
|
// set the host name to remote docker machine's IP address.
|
|
//
|
|
// Otherwise, the hostname remains as 'localhost'
|
|
apiHost := server[0].Labels["apihost"]
|
|
|
|
if apiHost != "" {
|
|
s := string(trimBytes)
|
|
s = strings.Replace(s, "localhost", apiHost, 1)
|
|
trimBytes = []byte(s)
|
|
}
|
|
_, err = kubeconfigfile.Write(trimBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("ERROR: couldn't write to kubeconfig.yaml\n%+v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getKubeConfig(cluster string) (string, error) {
|
|
kubeConfigPath, err := getClusterKubeConfigPath(cluster)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if clusters, err := getClusters(false, cluster); err != nil || len(clusters) != 1 {
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return "", fmt.Errorf("Cluster %s does not exist", cluster)
|
|
}
|
|
|
|
// If kubeconfi.yaml has not been created, generate it now
|
|
if _, err := os.Stat(kubeConfigPath); err != nil {
|
|
if os.IsNotExist(err) {
|
|
if err = createKubeConfigFile(cluster); err != nil {
|
|
return "", err
|
|
}
|
|
} else {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return kubeConfigPath, nil
|
|
}
|
|
|
|
// printClusters prints the names of existing clusters
|
|
func printClusters() {
|
|
clusters, err := getClusters(true, "")
|
|
if err != nil {
|
|
log.Fatalf("ERROR: Couldn't list clusters\n%+v", err)
|
|
}
|
|
if len(clusters) == 0 {
|
|
log.Printf("No clusters found!")
|
|
return
|
|
}
|
|
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.SetAlignment(tablewriter.ALIGN_CENTER)
|
|
table.SetHeader([]string{"NAME", "IMAGE", "STATUS", "WORKERS"})
|
|
|
|
for _, cluster := range clusters {
|
|
workersRunning := 0
|
|
for _, worker := range cluster.workers {
|
|
if worker.State == "running" {
|
|
workersRunning++
|
|
}
|
|
}
|
|
workerData := fmt.Sprintf("%d/%d", workersRunning, len(cluster.workers))
|
|
clusterData := []string{cluster.name, cluster.image, cluster.status, workerData}
|
|
table.Append(clusterData)
|
|
}
|
|
|
|
table.Render()
|
|
}
|
|
|
|
// Classify cluster state: Running, Stopped or Abnormal
|
|
func getClusterStatus(server types.Container, workers []types.Container) string {
|
|
// The cluster is in the abnromal state when server state and the worker
|
|
// states don't agree.
|
|
for _, w := range workers {
|
|
if w.State != server.State {
|
|
return "unhealthy"
|
|
}
|
|
}
|
|
|
|
switch server.State {
|
|
case "exited": // All containers in this state are most likely
|
|
// as the result of running the "k3d stop" command.
|
|
return "stopped"
|
|
}
|
|
|
|
return server.State
|
|
}
|
|
|
|
// getClusters uses the docker API to get existing clusters and compares that with the list of cluster directories
|
|
// When 'all' is true, 'cluster' contains all clusters found from the docker daemon
|
|
// When 'all' is false, 'cluster' contains up to one cluster whose name matches 'name'. 'cluster' can
|
|
// be empty if no matching cluster is found.
|
|
func getClusters(all bool, name string) (map[string]cluster, error) {
|
|
ctx := context.Background()
|
|
docker, err := client.NewEnvClient()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ERROR: couldn't create docker client\n%+v", err)
|
|
}
|
|
|
|
// Prepare docker label filters
|
|
filters := filters.NewArgs()
|
|
filters.Add("label", "app=k3d")
|
|
filters.Add("label", "component=server")
|
|
|
|
// get all servers created by k3d
|
|
k3dServers, err := docker.ContainerList(ctx, types.ContainerListOptions{
|
|
All: true,
|
|
Filters: filters,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("WARNING: couldn't list server containers\n%+v", err)
|
|
}
|
|
|
|
clusters := make(map[string]cluster)
|
|
|
|
// don't filter for servers but for workers now
|
|
filters.Del("label", "component=server")
|
|
filters.Add("label", "component=worker")
|
|
|
|
// for all servers created by k3d, get workers and cluster information
|
|
for _, server := range k3dServers {
|
|
clusterName := server.Labels["cluster"]
|
|
|
|
// Skip the cluster if we don't want all of them, and
|
|
// the cluster name does not match.
|
|
if all || name == clusterName {
|
|
|
|
// Add the cluster
|
|
filters.Add("label", fmt.Sprintf("cluster=%s", clusterName))
|
|
|
|
// get workers
|
|
workers, err := docker.ContainerList(ctx, types.ContainerListOptions{
|
|
All: true,
|
|
Filters: filters,
|
|
})
|
|
if err != nil {
|
|
log.Printf("WARNING: couldn't get worker containers for cluster %s\n%+v", clusterName, err)
|
|
}
|
|
|
|
// save cluster information
|
|
serverPorts := []string{}
|
|
for _, port := range server.Ports {
|
|
serverPorts = append(serverPorts, strconv.Itoa(int(port.PublicPort)))
|
|
}
|
|
clusters[clusterName] = cluster{
|
|
name: clusterName,
|
|
image: server.Image,
|
|
status: getClusterStatus(server, workers),
|
|
serverPorts: serverPorts,
|
|
server: server,
|
|
workers: workers,
|
|
}
|
|
// clear label filters before searching for next cluster
|
|
filters.Del("label", fmt.Sprintf("cluster=%s", clusterName))
|
|
}
|
|
}
|
|
|
|
return clusters, nil
|
|
}
|
|
|