diff --git a/cli/commands.go b/cli/commands.go index d2c3906b..e3e1ae1a 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -1,5 +1,9 @@ package run +/* + * This file contains the "backend" functionality for the CLI commands (and flags) + */ + import ( "bytes" "context" @@ -19,7 +23,7 @@ import ( "github.com/urfave/cli" ) -// CheckTools checks if the installed tools work correctly +// CheckTools checks if the docker API server is responding func CheckTools(c *cli.Context) error { log.Print("Checking docker...") ctx := context.Background() @@ -67,7 +71,7 @@ func CreateCluster(c *cli.Context) error { k3sServerArgs = append(k3sServerArgs, c.StringSlice("server-arg")...) } - // let's go + // create the server log.Printf("Creating cluster [%s]", c.String("name")) dockerID, err := createServer( c.GlobalBool("verbose"), @@ -88,10 +92,13 @@ func CreateCluster(c *cli.Context) error { return err } - // wait for k3s to be up and running if we want it + // Wait for k3s to be up and running if wanted. + // We're simply scanning the container logs for a line that tells us that everything's up and running + // TODO: also wait for worker nodes start := time.Now() timeout := time.Duration(c.Int("timeout")) * time.Second for c.IsSet("wait") { + // not running after timeout exceeded? Rollback and delete everything. if timeout != 0 && !time.Now().After(start.Add(timeout)) { err := DeleteCluster(c) if err != nil { @@ -100,6 +107,7 @@ func CreateCluster(c *cli.Context) error { return errors.New("Cluster creation exceeded specified timeout") } + // scan container logs for a line that tells us that the required services are up and running out, err := docker.ContainerLogs(ctx, dockerID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true}) if err != nil { out.Close() @@ -116,9 +124,12 @@ func CreateCluster(c *cli.Context) error { time.Sleep(1 * time.Second) } + // create the directory where we will put the kubeconfig file by default (when running `k3d get-config`) + // TODO: this can probably be moved to `k3d get-config` or be removed in a different approach createClusterDir(c.String("name")) - // worker nodes + // spin up the worker nodes + // TODO: do this concurrently in different goroutines if c.Int("workers") > 0 { k3sWorkerArgs := []string{} env := []string{k3sClusterSecret} @@ -150,7 +161,7 @@ kubectl cluster-info`, os.Args[0], c.String("name")) return nil } -// DeleteCluster removes the cluster container and its cluster directory +// DeleteCluster removes the containers belonging to a cluster and its local directory func DeleteCluster(c *cli.Context) error { // operate on one or all clusters @@ -177,6 +188,7 @@ func DeleteCluster(c *cli.Context) error { for _, cluster := range clusters { log.Printf("Removing cluster [%s]", cluster.name) if len(cluster.workers) > 0 { + // TODO: this could be done in goroutines log.Printf("...Removing %d workers\n", len(cluster.workers)) for _, worker := range cluster.workers { if err := removeContainer(worker.ID); err != nil { diff --git a/cli/container.go b/cli/container.go index af77579b..61f7887d 100644 --- a/cli/container.go +++ b/cli/container.go @@ -1,5 +1,10 @@ package run +/* + * The functions in this file take care of spinning up the + * k3s server and worker containers as well as deleting them. + */ + import ( "context" "fmt" @@ -17,6 +22,7 @@ import ( "github.com/docker/docker/client" ) +// createServer creates and starts a k3s server container func createServer(verbose bool, image string, port string, args []string, env []string, name string, volumes []string) (string, error) { log.Printf("Creating server using %s...\n", image) ctx := context.Background() @@ -24,6 +30,8 @@ func createServer(verbose bool, image string, port string, args []string, env [] if err != nil { return "", fmt.Errorf("ERROR: couldn't create docker client\n%+v", err) } + + // pull the required docker image reader, err := docker.ImagePull(ctx, image, types.ImagePullOptions{}) if err != nil { return "", fmt.Errorf("ERROR: couldn't pull image %s\n%+v", image, err) @@ -40,6 +48,7 @@ func createServer(verbose bool, image string, port string, args []string, env [] } } + // configure container options (host/network configuration, labels, env vars, etc.) containerLabels := make(map[string]string) containerLabels["app"] = "k3d" containerLabels["component"] = "server" @@ -74,6 +83,7 @@ func createServer(verbose bool, image string, port string, args []string, env [] }, } + // create the container resp, err := docker.ContainerCreate(ctx, &container.Config{ Image: image, Cmd: append([]string{"server"}, args...), @@ -87,6 +97,7 @@ func createServer(verbose bool, image string, port string, args []string, env [] return "", fmt.Errorf("ERROR: couldn't create container %s\n%+v", containerName, err) } + // start the container if err := docker.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { return "", fmt.Errorf("ERROR: couldn't start container %s\n%+v", containerName, err) } @@ -95,6 +106,7 @@ func createServer(verbose bool, image string, port string, args []string, env [] } +// createWorker creates/starts a k3s agent node that connects to the server func createWorker(verbose bool, image string, args []string, env []string, name string, volumes []string, postfix string, serverPort string) (string, error) { ctx := context.Background() docker, err := client.NewEnvClient() @@ -102,6 +114,7 @@ func createWorker(verbose bool, image string, args []string, env []string, name return "", fmt.Errorf("ERROR: couldn't create docker client\n%+v", err) } + // pull the required docker image reader, err := docker.ImagePull(ctx, image, types.ImagePullOptions{}) if err != nil { return "", fmt.Errorf("ERROR: couldn't pull image %s\n%+v", image, err) @@ -113,6 +126,7 @@ func createWorker(verbose bool, image string, args []string, env []string, name } } + // configure container options (host/network configuration, labels, env vars, etc.) containerLabels := make(map[string]string) containerLabels["app"] = "k3d" containerLabels["component"] = "worker" @@ -143,6 +157,7 @@ func createWorker(verbose bool, image string, args []string, env []string, name }, } + // create the container resp, err := docker.ContainerCreate(ctx, &container.Config{ Image: image, Env: env, @@ -152,6 +167,7 @@ func createWorker(verbose bool, image string, args []string, env []string, name return "", fmt.Errorf("ERROR: couldn't create container %s\n%+v", containerName, err) } + // start the container if err := docker.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { return "", fmt.Errorf("ERROR: couldn't start container %s\n%+v", containerName, err) } @@ -159,13 +175,20 @@ func createWorker(verbose bool, image string, args []string, env []string, name return resp.ID, nil } +// removeContainer tries to rm a container, selected by Docker ID, and does a rm -f if it fails (e.g. if container is still running) func removeContainer(ID string) error { + // TODO: first check if container is running, then try to stop it with a timeout before trying to remove it + // if it does not terminate gracefully, try a force remove ctx := context.Background() docker, err := client.NewEnvClient() if err != nil { return fmt.Errorf("ERROR: couldn't create docker client\n%+v", err) } + + // first, try a soft remove if err := docker.ContainerRemove(ctx, ID, types.ContainerRemoveOptions{}); err != nil { + + // if soft remove didn't succeed, force remove the container log.Printf("WARNING: couldn't delete container [%s], trying a force remove now.", ID) if err := docker.ContainerRemove(ctx, ID, types.ContainerRemoveOptions{Force: true}); err != nil { return fmt.Errorf("FAILURE: couldn't delete container [%s] -> %+v", ID, err) diff --git a/cli/network.go b/cli/network.go index 148a0241..6141fb4d 100644 --- a/cli/network.go +++ b/cli/network.go @@ -11,6 +11,8 @@ import ( "github.com/docker/docker/client" ) +// createClusterNetwork creates a docker network for a cluster that will be used +// to let the server and worker containers communicate with each other easily. func createClusterNetwork(clusterName string) (string, error) { ctx := context.Background() docker, err := client.NewEnvClient() @@ -18,6 +20,7 @@ func createClusterNetwork(clusterName string) (string, error) { return "", fmt.Errorf("ERROR: couldn't create docker client\n%+v", err) } + // create the network with a set of labels and the cluster name as network name resp, err := docker.NetworkCreate(ctx, clusterName, types.NetworkCreate{ Labels: map[string]string{ "app": "k3d", @@ -31,6 +34,7 @@ func createClusterNetwork(clusterName string) (string, error) { return resp.ID, nil } +// deleteClusterNetwork deletes a docker network based on the name of a cluster it belongs to func deleteClusterNetwork(clusterName string) error { ctx := context.Background() docker, err := client.NewEnvClient() @@ -49,6 +53,7 @@ func deleteClusterNetwork(clusterName string) error { return fmt.Errorf("ERROR: couldn't find network for cluster %s\n%+v", clusterName, err) } + // there should be only one network that matches the name... but who knows? for _, network := range networks { if err := docker.NetworkRemove(ctx, network.ID); err != nil { log.Printf("WARNING: couldn't remove network for cluster %s\n%+v", clusterName, err) diff --git a/cli/util.go b/cli/util.go index 4033b4c8..66d0ba2b 100644 --- a/cli/util.go +++ b/cli/util.go @@ -16,6 +16,7 @@ const ( var src = rand.NewSource(time.Now().UnixNano()) // GenerateRandomString thanks to https://stackoverflow.com/a/31832326/6450189 +// GenerateRandomString is used to generate a random string that is used as a cluster secret func GenerateRandomString(n int) string { sb := strings.Builder{} diff --git a/main.go b/main.go index c72529d8..ec8b28de 100644 --- a/main.go +++ b/main.go @@ -41,10 +41,10 @@ func main() { Action: run.CheckTools, }, { - // create creates a new k3s cluster in a container + // create creates a new k3s cluster in docker containers Name: "create", Aliases: []string{"c"}, - Usage: "Create a single node k3s cluster in a container", + Usage: "Create a single- or multi-node k3s cluster in docker containers", Flags: []cli.Flag{ cli.StringFlag{ Name: "name, n", @@ -53,7 +53,7 @@ func main() { }, cli.StringFlag{ Name: "volume, v", - Usage: "Mount one or more volumes into the cluster node (Docker notation: `source:destination[,source:destination]`)", + Usage: "Mount one or more volumes into every node of the cluster (Docker notation: `source:destination[,source:destination]`)", }, cli.StringFlag{ Name: "version", @@ -63,7 +63,7 @@ func main() { cli.IntFlag{ Name: "port, p", Value: 6443, - Usage: "Set a port on which the ApiServer will listen", + Usage: "Map the Kubernetes ApiServer port to a local port", }, cli.IntFlag{ Name: "timeout, t", @@ -72,7 +72,7 @@ func main() { }, cli.BoolFlag{ Name: "wait, w", - Usage: "Wait for the cluster to come up", + Usage: "Wait for the cluster to come up before returning", }, cli.StringSliceFlag{ Name: "server-arg, x", @@ -103,7 +103,7 @@ func main() { }, cli.BoolFlag{ Name: "all, a", - Usage: "delete all existing clusters (this ignores the --name/-n flag)", + Usage: "Delete all existing clusters (this ignores the --name/-n flag)", }, }, Action: run.DeleteCluster, @@ -116,11 +116,11 @@ func main() { cli.StringFlag{ Name: "name, n", Value: "k3s_default", - Usage: "name of the cluster", + Usage: "Name of the cluster", }, cli.BoolFlag{ Name: "all, a", - Usage: "stop all running clusters (this ignores the --name/-n flag)", + Usage: "Stop all running clusters (this ignores the --name/-n flag)", }, }, Action: run.StopCluster, @@ -133,11 +133,11 @@ func main() { cli.StringFlag{ Name: "name, n", Value: "k3s_default", - Usage: "name of the cluster", + Usage: "Name of the cluster", }, cli.BoolFlag{ Name: "all, a", - Usage: "start all stopped clusters (this ignores the --name/-n flag)", + Usage: "Start all stopped clusters (this ignores the --name/-n flag)", }, }, Action: run.StartCluster, @@ -150,7 +150,7 @@ func main() { Flags: []cli.Flag{ cli.BoolFlag{ Name: "all, a", - Usage: "also show non-running clusters", + Usage: "Also show non-running clusters", }, }, Action: run.ListClusters, @@ -163,17 +163,18 @@ func main() { cli.StringFlag{ Name: "name, n", Value: "k3s_default", - Usage: "name of the cluster", + Usage: "Name of the cluster", }, cli.BoolFlag{ Name: "all, a", - Usage: "get kubeconfig for all clusters (this ignores the --name/-n flag)", + Usage: "Get kubeconfig for all clusters (this ignores the --name/-n flag)", }, }, Action: run.GetKubeConfig, }, } + // Global flags app.Flags = []cli.Flag{ cli.BoolFlag{ Name: "verbose",