Skip to content

Commit

Permalink
Merge pull request #5 from iwilltry42/development
Browse files Browse the repository at this point in the history
Tested changes and improved error logs
  • Loading branch information
Thorsten Klein committed Apr 10, 2019
2 parents 9630e22 + 65c0bf9 commit 45eb460
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 149 deletions.
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@ SHELL := /bin/bash
TARGETS ?= darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 windows/amd64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-386.tar.gz linux-386.tar.gz.sha256 linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm64.tar.gz linux-arm64.tar.gz.sha256 windows-amd64.zip windows-amd64.zip.sha256

# get git tag
GIT_TAG := $(shell git describe --tags)
ifeq ($(GIT_TAG),)
GIT_TAG := $(shell git describe --always)
endif

# Go options
GO ?= go
PKG := $(shell go mod vendor)
TAGS :=
TESTS := .
TESTFLAGS :=
LDFLAGS := -w -s
LDFLAGS := -w -s -X github.com/iwilltry42/k3d-go/version.Version=${GIT_TAG}
GOFLAGS :=
BINDIR := $(CURDIR)/bin
BINARIES := k3d
BINARIES := k3d

export GO111MODULE=on

# go source files, ignore vendor directory
SRC = $(shell find . -type f -name '*.go' -not -path "./vendor/*")
SRC = $(shell find . -type f -name '*.go' -not -path "./*/*")

.PHONY: all build build-cross clean fmt simplify check

Expand Down
217 changes: 217 additions & 0 deletions cli/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package run

import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"path"
"strings"
"time"

"github.com/urfave/cli"
)

// CheckTools checks if the installed tools work correctly
func CheckTools(c *cli.Context) error {
log.Print("Checking docker...")
cmd := "docker"
args := []string{"version"}
if err := runCommand(true, cmd, args...); err != nil {
return fmt.Errorf("ERROR: checking docker failed\n%+v", err)
}
log.Println("SUCCESS: Checking docker succeeded")
return nil
}

// CreateCluster creates a new single-node cluster container and initializes the cluster directory
func CreateCluster(c *cli.Context) error {
if c.IsSet("timeout") && !c.IsSet("wait") {
return errors.New("Cannot use --timeout flag without --wait flag")
}
port := fmt.Sprintf("%s:%s", c.String("port"), c.String("port"))
image := fmt.Sprintf("rancher/k3s:%s", c.String("version"))
cmd := "docker"
args := []string{
"run",
"--name", c.String("name"),
"-e", "K3S_KUBECONFIG_OUTPUT=/output/kubeconfig.yaml",
"--publish", port,
"--privileged",
}
extraArgs := []string{}
if c.IsSet("volume") {
extraArgs = append(extraArgs, "--volume", c.String("volume"))
}
if len(extraArgs) > 0 {
args = append(args, extraArgs...)
}
args = append(args,
"-d",
image,
"server", // cmd
"--https-listen-port", c.String("port"), //args
)
log.Printf("Creating cluster [%s]", c.String("name"))
if err := runCommand(true, cmd, args...); err != nil {
return fmt.Errorf("ERROR: couldn't create cluster [%s]\n%+v", c.String("name"), err)
}

start := time.Now()
timeout := time.Duration(c.Int("timeout")) * time.Second
for c.IsSet("wait") {
if timeout != 0 && !time.Now().After(start.Add(timeout)) {
err := DeleteCluster(c)
if err != nil {
return err
}
return errors.New("Cluster creation exceeded specified timeout")
}
cmd := "docker"
args = []string{
"logs",
c.String("name"),
}
prog := exec.Command(cmd, args...)
output, err := prog.CombinedOutput()
if err != nil {
return err
}
if strings.Contains(string(output), "Running kubelet") {
break
}

time.Sleep(1 * time.Second)
}

createClusterDir(c.String("name"))
log.Printf("SUCCESS: created cluster [%s]", c.String("name"))
log.Printf(`You can now use the cluster with:
export KUBECONFIG="$(%s get-kubeconfig --name='%s')"
kubectl cluster-info`, os.Args[0], c.String("name"))
return nil
}

// DeleteCluster removes the cluster container and its cluster directory
func DeleteCluster(c *cli.Context) error {
cmd := "docker"
args := []string{"rm"}
clusters := []string{}

// operate on one or all clusters
if !c.Bool("all") {
clusters = append(clusters, c.String("name"))
} else {
clusterList, err := getClusterNames()
if err != nil {
return fmt.Errorf("ERROR: `--all` specified, but no clusters were found\n%+v", err)
}
clusters = append(clusters, clusterList...)
}

// remove clusters one by one instead of appending all names to the docker command
// this allows for more granular error handling and logging
for _, cluster := range clusters {
log.Printf("Removing cluster [%s]", cluster)
args = append(args, cluster)
if err := runCommand(true, cmd, args...); err != nil {
log.Printf("WARNING: couldn't delete cluster [%s], trying a force remove now.", cluster)
args = args[:len(args)-1] // pop last element from list (name of cluster)
args = append(args, "-f", cluster)
if err := runCommand(true, cmd, args...); err != nil {
log.Printf("FAILURE: couldn't delete cluster [%s] -> %+v", cluster, err)
}
args = args[:len(args)-1] // pop last element from list (-f flag)
}
deleteClusterDir(cluster)
log.Printf("SUCCESS: removed cluster [%s]", cluster)
args = args[:len(args)-1] // pop last element from list (name of last cluster)
}

return nil
}

// StopCluster stops a running cluster container (restartable)
func StopCluster(c *cli.Context) error {
cmd := "docker"
args := []string{"stop"}
clusters := []string{}

// operate on one or all clusters
if !c.Bool("all") {
clusters = append(clusters, c.String("name"))
} else {
clusterList, err := getClusterNames()
if err != nil {
return fmt.Errorf("ERROR: `--all` specified, but no clusters were found\n%+v", err)
}
clusters = append(clusters, clusterList...)
}

// stop clusters one by one instead of appending all names to the docker command
// this allows for more granular error handling and logging
for _, cluster := range clusters {
log.Printf("Stopping cluster [%s]", cluster)
args = append(args, cluster)
if err := runCommand(true, cmd, args...); err != nil {
log.Printf("FAILURE: couldn't stop cluster [%s] -> %+v", cluster, err)
}
log.Printf("SUCCESS: stopped cluster [%s]", cluster)
args = args[:len(args)-1] // pop last element from list (name of last cluster)
}

return nil
}

// StartCluster starts a stopped cluster container
func StartCluster(c *cli.Context) error {
cmd := "docker"
args := []string{"start"}
clusters := []string{}

// operate on one or all clusters
if !c.Bool("all") {
clusters = append(clusters, c.String("name"))
} else {
clusterList, err := getClusterNames()
if err != nil {
return fmt.Errorf("ERROR: `--all` specified, but no clusters were found\n%+v", err)
}
clusters = append(clusters, clusterList...)
}

// start clusters one by one instead of appending all names to the docker command
// this allows for more granular error handling and logging
for _, cluster := range clusters {
log.Printf("Starting cluster [%s]", cluster)
args = append(args, cluster)
if err := runCommand(true, cmd, args...); err != nil {
log.Printf("FAILURE: couldn't start cluster [%s] -> %+v", cluster, err)
}
log.Printf("SUCCESS: started cluster [%s]", cluster)
args = args[:len(args)-1] // pop last element from list (name of last cluster)
}

return nil
}

// ListClusters prints a list of created clusters
func ListClusters(c *cli.Context) error {
printClusters(c.Bool("all"))
return nil
}

// GetKubeConfig grabs the kubeconfig from the running cluster and prints the path to stdout
func GetKubeConfig(c *cli.Context) error {
sourcePath := fmt.Sprintf("%s:/output/kubeconfig.yaml", c.String("name"))
destPath, _ := getClusterDir(c.String("name"))
cmd := "docker"
args := []string{"cp", sourcePath, destPath}
if err := runCommand(false, cmd, args...); err != nil {
return fmt.Errorf("ERROR: Couldn't get kubeconfig for cluster [%s]\n%+v", c.String("name"), err)
}
fmt.Printf("%s\n", path.Join(destPath, "kubeconfig.yaml"))
return nil
}
51 changes: 40 additions & 11 deletions config.go → cli/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package run

import (
"context"
Expand All @@ -12,6 +12,12 @@ import (
"github.com/olekukonko/tablewriter"
)

type cluster struct {
name string
image string
status string
}

// 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 {
Expand Down Expand Up @@ -50,30 +56,30 @@ func getClusterDir(name string) (string, error) {

// printClusters prints the names of existing clusters
func printClusters(all bool) {
clusters, err := getClusters()
clusterNames, err := getClusterNames()
if err != nil {
log.Fatalf("ERROR: Couldn't list clusters -> %+v", err)
}
docker, err := dockerClient.NewEnvClient()
if err != nil {
log.Printf("WARNING: couldn't get docker info -> %+v", err)
if len(clusterNames) == 0 {
log.Printf("No clusters found!")
return
}

table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"NAME", "IMAGE", "STATUS"})

for _, cluster := range clusters {
containerInfo, _ := docker.ContainerInspect(context.Background(), cluster)
clusterData := []string{cluster, containerInfo.Config.Image, containerInfo.ContainerJSONBase.State.Status}
if containerInfo.ContainerJSONBase.State.Status == "running" || all {
for _, clusterName := range clusterNames {
cluster, _ := getCluster(clusterName)
clusterData := []string{cluster.name, cluster.image, cluster.status}
if cluster.status == "running" || all {
table.Append(clusterData)
}
}
table.Render()
}

// getClusters returns a list of cluster names which are folder names in the config directory
func getClusters() ([]string, error) {
// getClusterNames returns a list of cluster names which are folder names in the config directory
func getClusterNames() ([]string, error) {
homeDir, err := homedir.Dir()
if err != nil {
log.Printf("ERROR: Couldn't get user's home directory")
Expand All @@ -93,3 +99,26 @@ func getClusters() ([]string, error) {
}
return clusters, nil
}

// getCluster creates a cluster struct with populated information fields
func getCluster(name string) (cluster, error) {
cluster := cluster{
name: name,
image: "UNKNOWN",
status: "UNKNOWN",
}

docker, err := dockerClient.NewEnvClient()
if err != nil {
log.Printf("ERROR: couldn't create docker client -> %+v", err)
return cluster, err
}
containerInfo, err := docker.ContainerInspect(context.Background(), cluster.name)
if err != nil {
log.Printf("WARNING: couldn't get docker info for [%s] -> %+v", cluster.name, err)
} else {
cluster.image = containerInfo.Config.Image
cluster.status = containerInfo.ContainerJSONBase.State.Status
}
return cluster, nil
}
18 changes: 18 additions & 0 deletions cli/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package run

import (
"log"
"os"
"os/exec"
)

// runCommand accepts the name and args and runs the specified command
func runCommand(verbose bool, name string, args ...string) error {
if verbose {
log.Printf("Running command: %+v", append([]string{name}, args...))
}
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
Loading

0 comments on commit 45eb460

Please sign in to comment.