diff --git a/installer/cmd/tectonic/main.go b/installer/cmd/tectonic/main.go index 6d89c8398f..4e41728f1f 100644 --- a/installer/cmd/tectonic/main.go +++ b/installer/cmd/tectonic/main.go @@ -1,29 +1,61 @@ package main import ( + "log" + "github.com/coreos/tectonic-installer/installer/pkg/workflow" "gopkg.in/alecthomas/kingpin.v2" ) var ( - dryRunFlag = kingpin.Flag("dry-run", "Just pretend, but don't do anything").Bool() - clusterInstallCommand = kingpin.Command("install", "Create a new Tectonic cluster") - clusterDeleteCommand = kingpin.Command("delete", "Delete an existing Tectonic cluster") - deleteClusterDir = clusterDeleteCommand.Arg("dir", "The name of the cluster to delete").String() - clusterConfigFlag = clusterInstallCommand.Flag("config", "Cluster specification file").Required().ExistingFile() + dryRunFlag = kingpin.Flag("dry-run", "Just pretend, but don't do anything").Bool() + clusterInstallCommand = kingpin.Command("install", "Create a new Tectonic cluster") + clusterFullInstallCommand = clusterInstallCommand.Command("full", "Create a new Tectonic cluster").Default() + clusterAssetsCommand = clusterInstallCommand.Command("assets", "Generate Tectonic assets.") + clusterBootstrapCommand = clusterInstallCommand.Command("bootstrap", "Create a single bootstrap node Tectonic cluster.") + clusterJoinCommand = clusterInstallCommand.Command("join", "Create master and worker nodes to join an exisiting Tectonic cluster.") + clusterDeleteCommand = kingpin.Command("delete", "Delete an existing Tectonic cluster") + deleteClusterDir = clusterDeleteCommand.Arg("dir", "The name of the cluster to delete").String() + clusterConfigFlag = clusterInstallCommand.Flag("config", "Cluster specification file").Required().ExistingFile() ) func main() { + // TODO: actually do proper error handling switch kingpin.Parse() { - case clusterInstallCommand.FullCommand(): + case clusterFullInstallCommand.FullCommand(): { w := workflow.NewInstallWorkflow(*clusterConfigFlag) - w.Execute() + if err := w.Execute(); err != nil { + log.Fatal(err) + } + } + case clusterAssetsCommand.FullCommand(): + { + w := workflow.NewAssetsWorkflow(*clusterConfigFlag) + if err := w.Execute(); err != nil { + log.Fatal(err) + } + } + case clusterBootstrapCommand.FullCommand(): + { + w := workflow.NewBootstrapWorkflow(*clusterConfigFlag) + if err := w.Execute(); err != nil { + log.Fatal(err) + } + } + case clusterJoinCommand.FullCommand(): + { + w := workflow.NewJoinWorkflow(*clusterConfigFlag) + if err := w.Execute(); err != nil { + log.Fatal(err) + } } case clusterDeleteCommand.FullCommand(): { w := workflow.NewDestroyWorkflow(*deleteClusterDir) - w.Execute() + if err := w.Execute(); err != nil { + log.Fatal(err) + } } } } diff --git a/installer/pkg/tectonic/buildstate.go b/installer/pkg/tectonic/buildstate.go index 20fa8d496b..87687dff9b 100644 --- a/installer/pkg/tectonic/buildstate.go +++ b/installer/pkg/tectonic/buildstate.go @@ -92,3 +92,11 @@ func writeFile(path, content string) error { return nil } + +// FindTemplatesForStep determines the location of top-level +// Terraform templates for a given step of build. +func FindTemplatesForStep(step ...string) string { + pwd, _ := os.Getwd() + step = append([]string{pwd, "steps"}, step...) + return filepath.Join(step...) +} diff --git a/installer/pkg/workflow/destroy.go b/installer/pkg/workflow/destroy.go index ec203d2ad8..c7ce9cf832 100644 --- a/installer/pkg/workflow/destroy.go +++ b/installer/pkg/workflow/destroy.go @@ -3,6 +3,8 @@ package workflow import ( "log" "os" + + "github.com/coreos/tectonic-installer/installer/pkg/tectonic" ) // NewDestroyWorkflow creates new instances of the 'destroy' workflow, @@ -16,12 +18,29 @@ func NewDestroyWorkflow(buildPath string) Workflow { } else if err != nil { log.Fatalf("%v encountered while validating build location.", err) } + + // TODO: get this dynamically once we move to cluster config + platform := "aws" + + if platform == "aws" { + return simpleWorkflow{ + metadata: metadata{ + statePath: buildPath, + }, + steps: []Step{ + terraformPrepareStep, + joiningDestroyStep, + bootstrapDestroyStep, + assetsDestroyStep, + }, + } + } return simpleWorkflow{ metadata: metadata{ statePath: buildPath, }, steps: []Step{ - tectonicPrepareStep, + terraformPrepareStep, terraformInitStep, terraformDestroyStep, }, @@ -32,8 +51,21 @@ func terraformDestroyStep(m *metadata) error { if m.statePath == "" { log.Fatalf("Invalid build location - cannot destroy.") } + log.Printf("Destroying cluster from %s...", m.statePath) + return tfDestroy(m.statePath, "state", tectonic.FindTemplatesForType(m.platform)) +} +func joiningDestroyStep(m *metadata) error { log.Printf("Destroying cluster from %s...", m.statePath) + return tfDestroy(m.statePath, "joining", tectonic.FindTemplatesForStep("joining")) +} - return terraformExec(m, "destroy", "-force") +func bootstrapDestroyStep(m *metadata) error { + log.Printf("Destroying cluster from %s...", m.statePath) + return tfDestroy(m.statePath, "bootstrap", tectonic.FindTemplatesForStep("bootstrap")) +} + +func assetsDestroyStep(m *metadata) error { + log.Printf("Destroying cluster from %s...", m.statePath) + return tfDestroy(m.statePath, "assets", tectonic.FindTemplatesForStep("assets")) } diff --git a/installer/pkg/workflow/install.go b/installer/pkg/workflow/install.go index bd73323c6b..19ed82f1c8 100644 --- a/installer/pkg/workflow/install.go +++ b/installer/pkg/workflow/install.go @@ -6,54 +6,117 @@ import ( "os" "path/filepath" - "github.com/coreos/tectonic-installer/installer/pkg/config" "github.com/coreos/tectonic-installer/installer/pkg/tectonic" ) const ( configFileName = "config.yaml" terraformVariablesFileName = "terraform.tfvars" + kubeConfig = "/generated/auth/kubeconfig" ) // NewInstallWorkflow creates new instances of the 'install' workflow, // responsible for running the actions necessary to install a new cluster. func NewInstallWorkflow(configFile string) Workflow { - config, err := config.ParseFile(configFile) - if err != nil { - log.Fatalf("%s is not a valid config file: %s", configFile, err) - } - cluster := config.Clusters[0] + // TODO: move to tectonicGenerateClusterConfig/tectonicGenerateTerraformVariables and get this dynamically + clusterName := "cluster-aws" + platform := "aws" + + if platform == "aws" { + return simpleWorkflow{ + metadata: metadata{ + clusterName: clusterName, + configFile: configFile, + }, + steps: []Step{ + terraformPrepareStep, + assetsStep, + bootstrapStep, + joiningStep, + }, + } + } return simpleWorkflow{ metadata: metadata{ - Cluster: cluster, - configFile: configFile, + clusterName: clusterName, + configFile: configFile, + platform: platform, }, steps: []Step{ - tectonicPrepareStep, - tectonicGenerateClusterConfig, - tectonicGenerateTerraformVariables, + terraformPrepareStep, terraformInitStep, terraformApplyStep, }, } } -func tectonicGenerateClusterConfig(m *metadata) error { - return tectonic.GenerateClusterConfig(m.Cluster, m.statePath) +//func tectonicGenerateClusterConfig(m *metadata) error { +// return tectonic.GenerateClusterConfig(m.Cluster, m.statePath) +//} +// +//func tectonicGenerateTerraformVariables(m *metadata) error { +// configFilePath := filepath.Join(m.statePath, terraformVariablesFileName) +// +// return tectonic.GenerateTerraformVars(m.Cluster, configFilePath) +//} + +// NewAssetsWorkflow creates new instances of the 'assets' workflow, +// responsible for running the actions necessary to generate cluster assets. +func NewAssetsWorkflow(configFile string) Workflow { + // TODO: move to tectonicGenerateClusterConfig/tectonicGenerateTerraformVariables and get this dynamically + clusterName := "cluster-aws" + return simpleWorkflow{ + metadata: metadata{ + clusterName: clusterName, + configFile: configFile, + }, + steps: []Step{ + terraformPrepareStep, + assetsStep, + }, + } } -func tectonicGenerateTerraformVariables(m *metadata) error { - configFilePath := filepath.Join(m.statePath, terraformVariablesFileName) +// NewBootstrapWorkflow creates new instances of the 'bootstrap' workflow, +// responsible for running the actions necessary to generate a single bootstrap machine cluster. +func NewBootstrapWorkflow(configFile string) Workflow { + // TODO: move to tectonicGenerateClusterConfig/tectonicGenerateTerraformVariables and get this dynamically + clusterName := "cluster-aws" + return simpleWorkflow{ + metadata: metadata{ + clusterName: clusterName, + configFile: configFile, + }, + steps: []Step{ + terraformPrepareStep, + bootstrapStep, + }, + } +} - return tectonic.GenerateTerraformVars(m.Cluster, configFilePath) +// NewJoinWorkflow creates new instances of the 'join' workflow, +// responsible for running the actions necessary to scale the machines of the cluster. +func NewJoinWorkflow(configFile string) Workflow { + // TODO: move to tectonicGenerateClusterConfig/tectonicGenerateTerraformVariables and get this dynamically + clusterName := "cluster-aws" + return simpleWorkflow{ + metadata: metadata{ + clusterName: clusterName, + configFile: configFile, + }, + steps: []Step{ + terraformPrepareStep, + joiningStep, + }, + } } -func tectonicPrepareStep(m *metadata) error { +func terraformPrepareStep(m *metadata) error { if m.statePath == "" { - m.statePath = tectonic.NewBuildLocation(m.Cluster.Name) + m.statePath = tectonic.NewBuildLocation(m.clusterName) } - varfile := filepath.Join(m.statePath, configFileName) + varfile := filepath.Join(m.statePath, m.configFile) if _, err := os.Stat(varfile); os.IsNotExist(err) { from, err := os.Open(m.configFile) if err != nil { @@ -73,14 +136,55 @@ func tectonicPrepareStep(m *metadata) error { return nil } +func terraformInitStep(m *metadata) error { + log.Printf("Initializing cluster ...") + return tfInit(m.statePath, tectonic.FindTemplatesForType(m.platform)) +} + func terraformApplyStep(m *metadata) error { log.Printf("Installation is running...") + return tfApply(m.statePath, "state", tectonic.FindTemplatesForType(m.platform)) +} - return terraformExec(m, "apply") +func assetsStep(m *metadata) error { + log.Printf("Installation is running...") + return runStep(m.statePath, "assets") } -func terraformInitStep(m *metadata) error { - log.Printf("Initializing cluster ...") +func bootstrapStep(m *metadata) error { + log.Printf("Installation is running...") + err := runStep(m.statePath, "bootstrap") + if err != nil { + return err + } + err = waitForNcg(m) + if err != nil { + return err + } + err = destroyCname(m) + if err != nil { + return err + } + return nil +} + +func joiningStep(m *metadata) error { + // TODO: import will fail after a first run, error is ignored for now + importAutoScalingGroup(m) + log.Printf("Installation is running...") + return runStep(m.statePath, "joining") +} - return terraformExec(m, "init") +func runStep(buildPath string, step string) error { + codePath := tectonic.FindTemplatesForStep(step) + err := tfInit(buildPath, codePath) + if err != nil { + return err + } + + err = tfApply(buildPath, step, codePath) + if err != nil { + return err + } + return nil } diff --git a/installer/pkg/workflow/terraform.go b/installer/pkg/workflow/terraform.go new file mode 100644 index 0000000000..480ed46830 --- /dev/null +++ b/installer/pkg/workflow/terraform.go @@ -0,0 +1,31 @@ +package workflow + +import ( + "os" + "os/exec" +) + +func runTfCommand(buildPath string, args ...string) error { + tfCommand := exec.Command("terraform", args...) + tfCommand.Dir = buildPath + tfCommand.Stdin = os.Stdin + tfCommand.Stdout = os.Stdout + tfCommand.Stderr = os.Stderr + err := tfCommand.Run() + if err != nil { + return err + } + return nil +} + +func tfInit(buildPath string, codePath string) error { + return runTfCommand(buildPath, "init", codePath) +} + +func tfDestroy(buildPath string, state string, codePath string) error { + return runTfCommand(buildPath, "destroy", "-force", "-state="+state+".tfstate", codePath) +} + +func tfApply(buildPath string, state string, codePath string) error { + return runTfCommand(buildPath, "apply", "-state="+state+".tfstate", codePath) +} diff --git a/installer/pkg/workflow/utils.go b/installer/pkg/workflow/utils.go index a7c6053d8c..11a99ea3e2 100644 --- a/installer/pkg/workflow/utils.go +++ b/installer/pkg/workflow/utils.go @@ -1,23 +1,57 @@ package workflow import ( - "os" - "os/exec" + "errors" + "log" + "time" "github.com/coreos/tectonic-installer/installer/pkg/tectonic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" ) -func terraformExec(m *metadata, command ...string) error { - command = append(command, tectonic.FindTemplatesForType(m.Cluster.Platform)) +func waitForNcg(m *metadata) error { + kubeconfigPath := m.statePath + kubeConfig + config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) + if err != nil { + return err + } - tf := exec.Command("terraform", command...) - tf.Dir = m.statePath - tf.Stdin = os.Stdin - tf.Stdout = os.Stdout - tf.Stderr = os.Stderr - if err := tf.Run(); err != nil { + client, err := kubernetes.NewForConfig(config) + if err != nil { return err } + retries := 180 + wait := 10 + for retries > 0 { + // client will error until api sever is up + ds, _ := client.DaemonSets("kube-system").Get("ncg") + log.Printf("Waiting for NCG to be running, this might take a while...") + if ds.Status.NumberReady >= 1 { + return nil + } + time.Sleep(time.Second * time.Duration(wait)) + retries-- + } + return errors.New("NCG is not running") +} + +func destroyCname(m *metadata) error { + return runTfCommand(m.statePath, "destroy", "-force", "-state=bootstrap.tfstate", "-target=aws_route53_record.tectonic_ncg", tectonic.FindTemplatesForStep("bootstrap")) +} + +func importAutoScalingGroup(m *metadata) error { + bp := m.statePath + var err error + err = runTfCommand(bp, "import", "-state=joining.tfstate", "-config="+tectonic.FindTemplatesForStep("joining"), "aws_autoscaling_group.masters", m.clusterName+"-masters") + if err != nil { + return err + } + err = runTfCommand(bp, "import", "-state=joining.tfstate", "-config="+tectonic.FindTemplatesForStep("joining"), "aws_autoscaling_group.workers", m.clusterName+"-workers") + if err != nil { + return err + } return nil + } diff --git a/installer/pkg/workflow/workflow.go b/installer/pkg/workflow/workflow.go index e38abd2962..4559aa17a9 100644 --- a/installer/pkg/workflow/workflow.go +++ b/installer/pkg/workflow/workflow.go @@ -1,11 +1,5 @@ package workflow -import ( - "log" - - "github.com/coreos/tectonic-installer/installer/pkg/config" -) - // Workflow is a high-level representation // of a set of actions performed in a predictable order. type Workflow interface { @@ -19,9 +13,11 @@ type Workflow interface { // Steps taked thier inputs from the metadata object and persist // results onto it for later consumption. type metadata struct { - config.Cluster - configFile string - statePath string + // TODO: use config and cluster structs + clusterName string + configFile string + statePath string + platform string } // Step is the entrypoint of a workflow step implementation. @@ -39,7 +35,7 @@ func (w simpleWorkflow) Execute() error { for _, step := range w.steps { err = step(&w.metadata) if err != nil { - log.Fatal(err) // TODO: actually do proper error handling + return err } } return nil diff --git a/installer/pkg/workflow/workflow_test.go b/installer/pkg/workflow/workflow_test.go new file mode 100644 index 0000000000..8a18ca4c27 --- /dev/null +++ b/installer/pkg/workflow/workflow_test.go @@ -0,0 +1,53 @@ +package workflow + +import ( + "errors" + "testing" +) + +func test1Step(m *metadata) error { + return nil +} + +func test2Step(m *metadata) error { + return nil +} + +func test3Step(m *metadata) error { + return errors.New("Step failed!") +} + +func TestWorkflowTypeExecute(t *testing.T) { + m := metadata{} + + testCases := []struct { + test string + steps []Step + m metadata + expectedError bool + }{ + { + test: "All steps succeed", + steps: []Step{test1Step, test2Step}, + m: m, + expectedError: false, + }, + { + test: "At least one step fails", + steps: []Step{test1Step, test2Step, test3Step}, + m: m, + expectedError: true, + }, + } + + for _, tc := range testCases { + wf := simpleWorkflow{ + metadata: tc.m, + steps: tc.steps, + } + err := wf.Execute() + if (err != nil) != tc.expectedError { + t.Errorf("Test case %s: WorkflowType.Execute() expected error: %v, got: %v", tc.test, tc.expectedError, (err != nil)) + } + } +}