diff --git a/README.md b/README.md index b231c09875..1851e52ffc 100644 --- a/README.md +++ b/README.md @@ -55,24 +55,24 @@ These instructions can be used for AWS: export PATH=$(pwd)/installer:$PATH ``` -4. Edit Tectonic configuration file including the $CLUSTER_NAME +4. Edit Tectonic configuration file including ```shell $EDITOR examples/tectonic.aws.yaml ``` 5. Init Tectonic CLI ```shell - tectonic init --config=examples/tectonic.aws.yaml + tectonic init --config=examples/tectonic.aws.yaml --workspace=example ``` 6. Install Tectonic cluster ```shell - tectonic install --dir=$CLUSTER_NAME + tectonic install --workspace=example ``` 7. Teardown Tectonic cluster ```shell - tectonic destroy --dir=$CLUSTER_NAME + tectonic destroy --workspace=example ``` #### Tests diff --git a/installer/cmd/tectonic/main.go b/installer/cmd/tectonic/main.go index d693c62746..4ab4b97f3e 100644 --- a/installer/cmd/tectonic/main.go +++ b/installer/cmd/tectonic/main.go @@ -10,18 +10,19 @@ import ( ) var ( - clusterInitCommand = kingpin.Command("init", "Initialize a new Tectonic cluster") - clusterInitConfigFlag = clusterInitCommand.Flag("config", "Cluster specification file").Required().ExistingFile() + clusterInitCommand = kingpin.Command("init", "Initialize a new Tectonic cluster") + clusterInitConfigFlag = clusterInitCommand.Flag("config", "Cluster specification file").Required().ExistingFile() + clusterInitWorkspaceNameFlag = clusterInitCommand.Flag("workspace", "Workspace folder name").Required().String() clusterInstallCommand = kingpin.Command("install", "Create a new Tectonic cluster") clusterInstallAssetsCommand = clusterInstallCommand.Command("assets", "Generate Tectonic assets.") clusterInstallBootstrapCommand = clusterInstallCommand.Command("bootstrap", "Create a single bootstrap node Tectonic cluster.") clusterInstallFullCommand = clusterInstallCommand.Command("full", "Create a new Tectonic cluster").Default() clusterInstallJoinCommand = clusterInstallCommand.Command("join", "Create master and worker nodes to join an exisiting Tectonic cluster.") - clusterInstallDirFlag = clusterInstallCommand.Flag("dir", "Cluster directory").Default(".").ExistingDir() + clusterInstallWorkspaceFlag = clusterInstallCommand.Flag("workspace", "Workspace directory").Default(".").ExistingDir() - clusterDestroyCommand = kingpin.Command("destroy", "Destroy an existing Tectonic cluster") - clusterDestroyDirFlag = clusterDestroyCommand.Flag("dir", "Cluster directory").Default(".").ExistingDir() + clusterDestroyCommand = kingpin.Command("destroy", "Destroy an existing Tectonic cluster") + clusterDestroyWorkspaceFlag = clusterDestroyCommand.Flag("workspace", "Workspace directory").Default(".").ExistingDir() convertCommand = kingpin.Command("convert", "Convert a tfvars.json to a Tectonic config.yaml") convertConfigFlag = convertCommand.Flag("config", "tfvars.json file").Required().ExistingFile() @@ -30,33 +31,48 @@ var ( ) func main() { - var w workflow.Workflow + var c *workflow.Cluster + var err error + + newCluster := func(clusterInstallDirFlag string) *workflow.Cluster { + l, err := log.ParseLevel(*logLevel) + if err != nil { + // By definition we should never enter this condition since kingpin should be guarding against incorrect values. + log.Fatalf("invalid log-level: %v", err) + } + log.SetLevel(l) + + c, err = workflow.NewCluster(clusterInstallDirFlag) + if err != nil { + log.Fatal(err) + os.Exit(1) + } + return c + } switch kingpin.Parse() { case clusterInitCommand.FullCommand(): - w = workflow.NewInitWorkflow(*clusterInitConfigFlag) + err = workflow.InitWorkspace(*clusterInitConfigFlag, *clusterInitWorkspaceNameFlag) case clusterInstallFullCommand.FullCommand(): - w = workflow.NewInstallFullWorkflow(*clusterInstallDirFlag) + c = newCluster(*clusterInstallWorkspaceFlag) + err = c.Install() case clusterInstallAssetsCommand.FullCommand(): - w = workflow.NewInstallAssetsWorkflow(*clusterInstallDirFlag) + c = newCluster(*clusterInstallWorkspaceFlag) + err = c.Assets() case clusterInstallBootstrapCommand.FullCommand(): - w = workflow.NewInstallBootstrapWorkflow(*clusterInstallDirFlag) + c = newCluster(*clusterInstallWorkspaceFlag) + err = c.Bootstrap() case clusterInstallJoinCommand.FullCommand(): - w = workflow.NewInstallJoinWorkflow(*clusterInstallDirFlag) + c = newCluster(*clusterInstallWorkspaceFlag) + err = c.Scale() case clusterDestroyCommand.FullCommand(): - w = workflow.NewDestroyWorkflow(*clusterDestroyDirFlag) + c = newCluster(*clusterInstallWorkspaceFlag) + err = c.Destroy() case convertCommand.FullCommand(): - w = workflow.NewConvertWorkflow(*convertConfigFlag) + err = workflow.TF2YAML(*convertConfigFlag) } - l, err := log.ParseLevel(*logLevel) if err != nil { - // By definition we should never enter this condition since kingpin should be guarding against incorrect values. - log.Fatalf("invalid log-level: %v", err) - } - log.SetLevel(l) - - if err := w.Execute(); err != nil { log.Fatal(err) os.Exit(1) } diff --git a/installer/pkg/config-generator/generator.go b/installer/pkg/config-generator/generator.go index 05d8fe9e27..515d3cb5da 100644 --- a/installer/pkg/config-generator/generator.go +++ b/installer/pkg/config-generator/generator.go @@ -7,6 +7,8 @@ import ( "errors" "fmt" "net" + "os" + "path/filepath" "strings" "github.com/apparentlymart/go-cidr/cidr" @@ -33,6 +35,14 @@ const ( ingressConfigIngressKind = "NodePort" certificatesStrategy = "userProvidedCA" identityAPIService = "tectonic-identity-api.tectonic-system.svc.cluster.local" + + generatedPath = "generated" + kcoConfigFileName = "kco-config.yaml" + tncoConfigFileName = "tnco-config.yaml" + kubeSystemPath = "generated/manifests" + kubeSystemFileName = "cluster-config.yaml" + tectonicSystemPath = "generated/tectonic" + tectonicSystemFileName = "cluster-config.yaml" ) // ConfigGenerator defines the cluster config generation for a cluster. @@ -290,6 +300,62 @@ func marshalYAML(obj interface{}) (string, error) { return string(data), nil } +// GenerateClusterConfigMaps returns, if successful, the cluster kubernetes configMaps +func (c ConfigGenerator) GenerateClusterConfigMaps(workspace string) error { + clusterGeneratedPath := filepath.Join(workspace, generatedPath) + if err := os.MkdirAll(clusterGeneratedPath, os.ModeDir|0755); err != nil { + return fmt.Errorf("Failed to create cluster generated directory at %s", clusterGeneratedPath) + } + + kcoConfig, err := c.CoreConfig() + if err != nil { + return err + } + + kcoConfigFilePath := filepath.Join(clusterGeneratedPath, kcoConfigFileName) + if err := writeFile(kcoConfigFilePath, kcoConfig); err != nil { + return err + } + + tncoConfig, err := c.TncoConfig() + if err != nil { + return err + } + + tncoConfigFilePath := filepath.Join(clusterGeneratedPath, tncoConfigFileName) + if err := writeFile(tncoConfigFilePath, tncoConfig); err != nil { + return err + } + + kubeSystem, err := c.KubeSystem() + if err != nil { + return err + } + + kubePath := filepath.Join(workspace, kubeSystemPath) + if err := os.MkdirAll(kubePath, os.ModeDir|0755); err != nil { + return fmt.Errorf("Failed to create manifests directory at %s", kubePath) + } + + kubeSystemConfigFilePath := filepath.Join(kubePath, kubeSystemFileName) + if err := writeFile(kubeSystemConfigFilePath, kubeSystem); err != nil { + return err + } + + tectonicSystem, err := c.TectonicSystem() + if err != nil { + return err + } + + tectonicPath := filepath.Join(workspace, tectonicSystemPath) + if err := os.MkdirAll(tectonicPath, os.ModeDir|0755); err != nil { + return fmt.Errorf("Failed to create tectonic directory at %s", tectonicPath) + } + + tectonicSystemConfigFilePath := filepath.Join(tectonicPath, tectonicSystemFileName) + return writeFile(tectonicSystemConfigFilePath, tectonicSystem) +} + func (c ConfigGenerator) getEtcdServersURLs() string { if len(c.Cluster.Etcd.External.Servers) > 0 { return strings.Join(c.Cluster.Etcd.External.Servers, ",") diff --git a/installer/pkg/workflow/BUILD.bazel b/installer/pkg/workflow/BUILD.bazel index 96f1c4786c..fe94ce5421 100644 --- a/installer/pkg/workflow/BUILD.bazel +++ b/installer/pkg/workflow/BUILD.bazel @@ -4,8 +4,7 @@ go_test( name = "go_default_test", size = "small", srcs = [ - "init_test.go", - "workflow_test.go", + "cluster_test.go", ], data = glob(["fixtures/**"]), embed = [":go_default_library"], @@ -16,13 +15,10 @@ go_library( name = "go_default_library", srcs = [ "convert.go", - "destroy.go", "executor.go", - "init.go", - "install.go", "terraform.go", "utils.go", - "workflow.go", + "cluster.go", ], importpath = "github.com/coreos/tectonic-installer/installer/pkg/workflow", visibility = ["//visibility:public"], diff --git a/installer/pkg/workflow/cluster.go b/installer/pkg/workflow/cluster.go new file mode 100644 index 0000000000..5d07ea4c45 --- /dev/null +++ b/installer/pkg/workflow/cluster.go @@ -0,0 +1,311 @@ +package workflow + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + log "github.com/Sirupsen/logrus" + "github.com/coreos/tectonic-installer/installer/pkg/config" + configgenerator "github.com/coreos/tectonic-installer/installer/pkg/config-generator" + yaml "gopkg.in/yaml.v2" +) + +const ( + stepsBaseDir = "steps" + assetsStep = "assets" + topologyStep = "topology" + tncDNSStep = "tnc_dns" + bootstrapOn = "-var=tectonic_aws_bootstrap=true" + bootstrapOff = "-var=tectonic_aws_bootstrap=false" + bootstrapStep = "bootstrap" + etcdStep = "etcd" + joinMastersStep = "joining_masters" + joinWorkersStep = "joining_workers" + configFileName = "config.yaml" + internalFileName = "internal.yaml" + terraformVariablesFileName = "terraform.tfvars" +) + +// Cluster models the cluster info within a workspace +// and enables running actions over the given cluster +type Cluster struct { + workspace string + config config.Cluster + platform string +} + +// InitWorkspace generates, if successful, a workspace folder with the config.yaml +func InitWorkspace(sourceConfigFilePath, workspaceName string) error { + if sourceConfigFilePath == "" { + errors.New("no cluster sourceConfigFilePath given for instantiating new cluster") + } + if workspaceName == "" { + errors.New("no cluster sourceConfigFilePath given for instantiating new cluster") + } + + // generate workspace folder + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("Failed to get current directory because: %s", err) + } + workspace := filepath.Join(dir, workspaceName) + + if stat, err := os.Stat(workspace); err == nil && stat.IsDir() { + return fmt.Errorf("workspace directory already exists at %s", workspace) + } + + if err := os.MkdirAll(workspace, os.ModeDir|0755); err != nil { + return fmt.Errorf("failed to create workspace directory at %s", workspace) + } + + // put config file under the workspace folder + configFilePath := filepath.Join(workspace, configFileName) + if err := copyFile(sourceConfigFilePath, configFilePath); err != nil { + return fmt.Errorf("failed to create cluster config at %s: %v", workspace, err) + } + + // generate the internal config file under the workspace folder + return buildInternalConfig(workspace) +} + +// NewCluster creates a cluster struct from a workspace +// It ensures cluster.config and the tfvars file are always up to date with config.yaml +func NewCluster(workspace string) (*Cluster, error) { + if workspace == "" { + errors.New("no workspace dir given for new cluster") + } + + config, err := readClusterConfig(workspace) + if err != nil { + return nil, fmt.Errorf("failed to read cluster config when refreshing it: %v", err) + } + + c := Cluster{ + config: *config, + platform: strings.ToLower(config.Platform), + workspace: workspace, + } + + if err := c.generateTerraformVariables(); err != nil { + return nil, fmt.Errorf("failed to generate terraform variables when creating new cluster: %v", err) + } + return &c, nil +} + +// Assets generates, if successful, the cluster assets +func (c Cluster) Assets() error { + if err := c.generateClusterConfigMaps(); err != nil { + return err + } + if err := c.runInstallStep(assetsStep); err != nil { + return err + } + return c.generateIgnConfig() +} + +// Bootstrap runs, if successful, the steps to bootstrap a single node cluster +func (c Cluster) Bootstrap() error { + if err := c.runInstallStep(topologyStep); err != nil { + return err + } + if err := c.createTNCCNAME(); err != nil { + return err + } + if err := c.runInstallStep(bootstrapStep); err != nil { + return err + } + if err := c.createTNCARecord(); err != nil { + return err + } + return c.runInstallStep(etcdStep) +} + +// Scale runs, if successful, the steps to scale a cluster +func (c Cluster) Scale() error { + if err := c.importAutoScalingGroup(); err != nil { + return err + } + if err := c.runInstallStep(joinMastersStep); err != nil { + return err + } + return c.runInstallStep(joinWorkersStep) +} + +// Install runs, if successful, the steps to install a cluster +func (c Cluster) Install() error { + if err := c.Assets(); err != nil { + return err + } + if err := c.Bootstrap(); err != nil { + return err + } + return c.Scale() +} + +// Destroy runs, if successful, the steps to destroy a cluster +func (c Cluster) Destroy() error { + if err := c.runDestroyStep(joinMastersStep); err != nil { + return err + } + if err := c.runDestroyStep(joinWorkersStep); err != nil { + return err + } + if err := c.runDestroyStep(etcdStep); err != nil { + return err + } + if err := c.runDestroyStep(bootstrapStep); err != nil { + return err + } + if err := c.runDestroyStep(tncDNSStep, []string{bootstrapOff}...); err != nil { + return err + } + if err := c.runDestroyStep(topologyStep); err != nil { + return err + } + return c.runDestroyStep(assetsStep) +} + +func (c Cluster) runInstallStep(step string, extraArgs ...string) error { + templateDir, err := findStepTemplates(step, c.platform) + if err != nil { + return err + } + if err := tfInit(c.workspace, templateDir); err != nil { + return err + } + return tfApply(c.workspace, step, templateDir, extraArgs...) +} + +func (c Cluster) runDestroyStep(step string, extraArgs ...string) error { + if !hasStateFile(c.workspace, step) { + log.Warningf("there is no statefile, therefore nothing to destroy for the step %s within %s", step, c.workspace) + return nil + } + templateDir, err := findStepTemplates(step, c.config.Platform) + if err != nil { + return err + } + + return tfDestroy(c.workspace, step, templateDir, extraArgs...) +} + +func (c Cluster) generateClusterConfigMaps() error { + configGenerator := configgenerator.New(c.config) + return configGenerator.GenerateClusterConfigMaps(c.workspace) +} + +func (c Cluster) generateIgnConfig() error { + configGenerator := configgenerator.New(c.config) + return configGenerator.GenerateIgnConfig(c.workspace) +} + +func (c Cluster) generateTerraformVariables() error { + vars, err := c.config.TFVars() + if err != nil { + return err + } + + terraformVariablesFilePath := filepath.Join(c.workspace, terraformVariablesFileName) + return writeFile(terraformVariablesFilePath, vars) +} + +func (c Cluster) createTNCCNAME() error { + if !c.clusterIsBootstrapped() { + return c.runInstallStep(tncDNSStep, []string{bootstrapOn}...) + } + return nil +} + +func (c Cluster) clusterIsBootstrapped() bool { + return hasStateFile(c.workspace, topologyStep) && + hasStateFile(c.workspace, bootstrapStep) && + hasStateFile(c.workspace, tncDNSStep) +} + +func (c Cluster) createTNCARecord() error { + return c.runInstallStep(tncDNSStep, []string{bootstrapOff}...) +} + +func (c Cluster) importAutoScalingGroup() error { + templatesPath, err := findStepTemplates(joinMastersStep, c.platform) + if err != nil { + return err + } + return terraformExec( + c.workspace, + "import", + fmt.Sprintf("-state=%s.tfstate", joinMastersStep), + fmt.Sprintf("-config=%s", templatesPath), + "aws_autoscaling_group.masters", + fmt.Sprintf("%s-masters", c.config.Name)) +} + +// readClusterConfig builds a config.Cluster from a workspace +// it's not allowed to modify cluster.Config because we only +// want that to happen atomically with generateTerraformVariables +func readClusterConfig(workspace string) (*config.Cluster, error) { + if workspace == "" { + errors.New("no workspace dir given for reading config") + } + configFilePath := filepath.Join(workspace, configFileName) + internalFilePath := filepath.Join(workspace, internalFileName) + + clusterConfig, err := parseClusterConfig(configFilePath, internalFilePath) + if err != nil { + return nil, fmt.Errorf("failed to parse cluster config when reading it: %v", err) + } + + if errs := clusterConfig.Validate(); len(errs) != 0 { + log.Errorf("Found %d errors in the cluster definition:", len(errs)) + for i, err := range errs { + log.Errorf("error %d: %v", i+1, err) + } + return nil, fmt.Errorf("found %d cluster definition errors", len(errs)) + } + + return clusterConfig, nil +} + +func parseClusterConfig(configFilePath string, internalFilePath string) (*config.Cluster, error) { + cfg, err := config.ParseConfigFile(configFilePath) + if err != nil { + return nil, fmt.Errorf("%s is not a valid config file: %s", configFilePath, err) + } + + if internalFilePath != "" { + internal, err := config.ParseInternalFile(internalFilePath) + if err != nil { + return nil, fmt.Errorf("%s is not a valid internal file: %s", internalFilePath, err) + } + cfg.Internal = *internal + } + + return cfg, nil +} + +func buildInternalConfig(workspace string) error { + if workspace == "" { + return errors.New("no workspace dir given for building internal config") + } + + // fill the internal struct + clusterID, err := configgenerator.GenerateClusterID(16) + if err != nil { + return err + } + internalCfg := config.Internal{ + ClusterID: clusterID, + } + + // store the content + yamlContent, err := yaml.Marshal(internalCfg) + internalFileContent := []byte("# Do not touch, auto-generated\n") + internalFileContent = append(internalFileContent, yamlContent...) + if err != nil { + return err + } + return writeFile(filepath.Join(workspace, internalFileName), string(internalFileContent)) +} diff --git a/installer/pkg/workflow/init_test.go b/installer/pkg/workflow/cluster_test.go similarity index 63% rename from installer/pkg/workflow/init_test.go rename to installer/pkg/workflow/cluster_test.go index 06b12a1bee..49c8d249b1 100644 --- a/installer/pkg/workflow/init_test.go +++ b/installer/pkg/workflow/cluster_test.go @@ -1,8 +1,6 @@ package workflow import ( - "errors" - "fmt" "io/ioutil" "os" "path/filepath" @@ -12,21 +10,10 @@ import ( "github.com/coreos/tectonic-installer/installer/pkg/config" ) -func initTestCluster(file string) (*config.Cluster, error) { - testConfig, err := config.ParseConfigFile(file) - if err != nil { - return nil, fmt.Errorf("failed to parse test config: %v", err) - } - if len(testConfig.Validate()) != 0 { - return nil, errors.New("failed to validate test conifg") - } - return testConfig, nil -} - -func TestGenerateTerraformVariablesStep(t *testing.T) { - expectedTfVarsFilePath := "./fixtures/terraform.tfvars" - clusterDir := "." - gotTfVarsFilePath := filepath.Join(clusterDir, terraformVariablesFileName) +func TestNewCluster(t *testing.T) { + expectedTfVarsFilePath := "./fixtures/expected.terraform.tfvars" + workspace := "./fixtures" + gotTfVarsFilePath := filepath.Join(workspace, terraformVariablesFileName) // clean up defer func() { @@ -35,17 +22,11 @@ func TestGenerateTerraformVariablesStep(t *testing.T) { } }() - cluster, err := initTestCluster("./fixtures/aws.basic.yaml") + _, err := NewCluster(workspace) if err != nil { t.Errorf("failed to init cluster: %v", err) } - m := &metadata{ - cluster: *cluster, - clusterDir: clusterDir, - } - - generateTerraformVariablesStep(m) gotData, err := ioutil.ReadFile(gotTfVarsFilePath) if err != nil { t.Errorf("failed to load generated tf vars file: %v", err) @@ -63,7 +44,7 @@ func TestGenerateTerraformVariablesStep(t *testing.T) { } } -func TestBuildInternalStep(t *testing.T) { +func TestBuildInternalConfig(t *testing.T) { testClusterDir := "." internalFilePath := filepath.Join(testClusterDir, internalFileName) @@ -74,28 +55,15 @@ func TestBuildInternalStep(t *testing.T) { } }() - metaNoClusterDir := &metadata{ - cluster: config.Cluster{ - Name: "test", - }, - } - - meta := &metadata{ - clusterDir: testClusterDir, - cluster: config.Cluster{ - Name: "test", - }, - } - errorTestCases := []struct { test string got string expected string }{ { - test: "no clusterDir exists", - got: buildInternalStep(metaNoClusterDir).Error(), - expected: "no clusterDir path set in metadata", + test: "no workspace exists", + got: buildInternalConfig("").Error(), + expected: "no workspace dir given for building internal config", }, } @@ -105,7 +73,7 @@ func TestBuildInternalStep(t *testing.T) { } } - if err := buildInternalStep(meta); err != nil { + if err := buildInternalConfig(testClusterDir); err != nil { t.Errorf("failed to run buildInternalStep, %v", err) } diff --git a/installer/pkg/workflow/convert.go b/installer/pkg/workflow/convert.go index e578525886..17545a7c03 100644 --- a/installer/pkg/workflow/convert.go +++ b/installer/pkg/workflow/convert.go @@ -8,31 +8,30 @@ import ( "github.com/coreos/tectonic-installer/installer/pkg/config" ) -// NewConvertWorkflow creates new instances of the 'convert' workflow, -// responsible for converting an old cluster config. -func NewConvertWorkflow(configFilePath string) Workflow { - return Workflow{ - metadata: metadata{configFilePath: configFilePath}, - steps: []Step{ - readTFVarsConfigStep, - printYAMLConfigStep, - }, +func TF2YAML(configFilePath string) error { + config, err := readTFVarsConfig(configFilePath) + if err != nil { + return err } + return printYAMLConfig(*config) } -func readTFVarsConfigStep(m *metadata) error { - data, err := ioutil.ReadFile(m.configFilePath) +func readTFVarsConfig(configFilePath string) (*config.Cluster, error) { + data, err := ioutil.ReadFile(configFilePath) if err != nil { - return err + return nil, err } - m.cluster = config.Cluster{} + config := &config.Cluster{} - return json.Unmarshal([]byte(data), &m.cluster) + if err := json.Unmarshal([]byte(data), config); err != nil { + return nil, err + } + return config, nil } -func printYAMLConfigStep(m *metadata) error { - yaml, err := m.cluster.YAML() +func printYAMLConfig(config config.Cluster) error { + yaml, err := config.YAML() if err != nil { return err } diff --git a/installer/pkg/workflow/destroy.go b/installer/pkg/workflow/destroy.go deleted file mode 100644 index c71ad84fc4..0000000000 --- a/installer/pkg/workflow/destroy.go +++ /dev/null @@ -1,61 +0,0 @@ -package workflow - -// NewDestroyWorkflow creates new instances of the 'destroy' workflow, -// responsible for running the actions required to remove resources -// of an existing cluster and clean up any remaining artefacts. -func NewDestroyWorkflow(clusterDir string) Workflow { - return Workflow{ - metadata: metadata{clusterDir: clusterDir}, - steps: []Step{ - readClusterConfigStep, - destroyJoinMastersStep, - destroyJoinWorkersStep, - destroyEtcdStep, - destroyBootstrapStep, - destroyTNCDNSStep, - destroyTopologyStep, - destroyAssetsStep, - }, - } -} - -func destroyAssetsStep(m *metadata) error { - return runDestroyStep(m, assetsStep) -} - -func destroyEtcdStep(m *metadata) error { - return runDestroyStep(m, etcdStep) -} - -func destroyBootstrapStep(m *metadata) error { - return runDestroyStep(m, bootstrapStep) -} - -func destroyTNCDNSStep(m *metadata) error { - return destroyTNCDNS(m) -} - -func destroyTopologyStep(m *metadata) error { - return runDestroyStep(m, topologyStep) -} - -func destroyJoinWorkersStep(m *metadata) error { - return runDestroyStep(m, joinWorkersStep) -} - -func destroyJoinMastersStep(m *metadata) error { - return runDestroyStep(m, joinMastersStep) -} - -func runDestroyStep(m *metadata, step string, extraArgs ...string) error { - if !hasStateFile(m.clusterDir, step) { - // there is no statefile, therefore nothing to destroy for this step - return nil - } - templateDir, err := findStepTemplates(step, m.cluster.Platform) - if err != nil { - return err - } - - return tfDestroy(m.clusterDir, step, templateDir, extraArgs...) -} diff --git a/installer/pkg/workflow/fixtures/aws.basic.yaml b/installer/pkg/workflow/fixtures/config.yaml similarity index 100% rename from installer/pkg/workflow/fixtures/aws.basic.yaml rename to installer/pkg/workflow/fixtures/config.yaml diff --git a/installer/pkg/workflow/fixtures/terraform.tfvars b/installer/pkg/workflow/fixtures/expected.terraform.tfvars similarity index 95% rename from installer/pkg/workflow/fixtures/terraform.tfvars rename to installer/pkg/workflow/fixtures/expected.terraform.tfvars index 05260513cf..b9ca0311b0 100644 --- a/installer/pkg/workflow/fixtures/terraform.tfvars +++ b/installer/pkg/workflow/fixtures/expected.terraform.tfvars @@ -5,6 +5,7 @@ "tectonic_container_linux_channel": "beta", "tectonic_container_linux_version": "latest", "tectonic_etcd_count": 3, + "tectonic_cluster_id": "212ebd0d-bb69-7f4a-883d-40f2856bae25", "tectonic_master_count": 2, "tectonic_cluster_name": "aws-basic", "tectonic_networking": "canal", diff --git a/installer/pkg/workflow/fixtures/internal.yaml b/installer/pkg/workflow/fixtures/internal.yaml new file mode 100644 index 0000000000..f738447535 --- /dev/null +++ b/installer/pkg/workflow/fixtures/internal.yaml @@ -0,0 +1,3 @@ +# Do not touch, auto-generated +clusterId: 212ebd0d-bb69-7f4a-883d-40f2856bae25 + diff --git a/installer/pkg/workflow/init.go b/installer/pkg/workflow/init.go deleted file mode 100644 index c37e6b3529..0000000000 --- a/installer/pkg/workflow/init.go +++ /dev/null @@ -1,88 +0,0 @@ -package workflow - -import ( - "errors" - "fmt" - "os" - "path/filepath" - - yaml "gopkg.in/yaml.v2" - - configgenerator "github.com/coreos/tectonic-installer/installer/pkg/config-generator" -) - -const ( - generatedPath = "generated" - kcoConfigFileName = "kco-config.yaml" - tncoConfigFileName = "tnco-config.yaml" - kubeSystemPath = "generated/manifests" - kubeSystemFileName = "cluster-config.yaml" - tectonicSystemPath = "generated/tectonic" - tectonicSystemFileName = "cluster-config.yaml" - terraformVariablesFileName = "terraform.tfvars" -) - -// NewInitWorkflow creates new instances of the 'init' workflow, -// responsible for initializing a new cluster. -func NewInitWorkflow(configFilePath string) Workflow { - return Workflow{ - metadata: metadata{configFilePath: configFilePath}, - steps: []Step{ - readClusterConfigStep, - prepareWorspaceStep, - buildInternalStep, - generateTerraformVariablesStep, - }, - } -} - -func buildInternalStep(m *metadata) error { - if m.clusterDir == "" { - return errors.New("no clusterDir path set in metadata") - } - - // fill the internal struct - clusterID, err := configgenerator.GenerateClusterID(16) - if err != nil { - return err - } - m.cluster.Internal.ClusterID = clusterID - - // store the content - yamlContent, err := yaml.Marshal(m.cluster.Internal) - internalFileContent := []byte("# Do not touch, auto-generated\n") - internalFileContent = append(internalFileContent, yamlContent...) - if err != nil { - return err - } - return writeFile(filepath.Join(m.clusterDir, internalFileName), string(internalFileContent)) -} - -func generateTerraformVariablesStep(m *metadata) error { - vars, err := m.cluster.TFVars() - if err != nil { - return err - } - - terraformVariablesFilePath := filepath.Join(m.clusterDir, terraformVariablesFileName) - return writeFile(terraformVariablesFilePath, vars) -} - -func prepareWorspaceStep(m *metadata) error { - dir, err := os.Getwd() - if err != nil { - return fmt.Errorf("Failed to get current directory because: %s", err) - } - - m.clusterDir = filepath.Join(dir, m.cluster.Name) - if stat, err := os.Stat(m.clusterDir); err == nil && stat.IsDir() { - return fmt.Errorf("cluster directory already exists at %s", m.clusterDir) - } - - if err := os.MkdirAll(m.clusterDir, os.ModeDir|0755); err != nil { - return fmt.Errorf("Failed to create cluster directory at %s", m.clusterDir) - } - - configFilePath := filepath.Join(m.clusterDir, configFileName) - return copyFile(m.configFilePath, configFilePath) -} diff --git a/installer/pkg/workflow/install.go b/installer/pkg/workflow/install.go deleted file mode 100644 index 4470d178d4..0000000000 --- a/installer/pkg/workflow/install.go +++ /dev/null @@ -1,120 +0,0 @@ -package workflow - -import "github.com/coreos/tectonic-installer/installer/pkg/config-generator" - -// NewInstallFullWorkflow creates new instances of the 'install' workflow, -// responsible for running the actions necessary to install a new cluster. -func NewInstallFullWorkflow(clusterDir string) Workflow { - return Workflow{ - metadata: metadata{clusterDir: clusterDir}, - steps: []Step{ - readClusterConfigStep, - generateClusterConfigMaps, - installAssetsStep, - generateIgnConfigStep, - installTopologyStep, - installTNCCNAMEStep, - installBootstrapStep, - installTNCARecordStep, - installEtcdStep, - installJoinMastersStep, - installJoinWorkersStep, - }, - } -} - -// NewInstallAssetsWorkflow creates new instances of the 'assets' workflow, -// responsible for running the actions necessary to generate cluster assets. -func NewInstallAssetsWorkflow(clusterDir string) Workflow { - return Workflow{ - metadata: metadata{clusterDir: clusterDir}, - steps: []Step{ - readClusterConfigStep, - generateClusterConfigMaps, - installAssetsStep, - generateIgnConfigStep, - }, - } -} - -// NewInstallBootstrapWorkflow creates new instances of the 'bootstrap' workflow, -// responsible for running the actions necessary to generate a single bootstrap machine cluster. -func NewInstallBootstrapWorkflow(clusterDir string) Workflow { - return Workflow{ - metadata: metadata{clusterDir: clusterDir}, - steps: []Step{ - readClusterConfigStep, - installTopologyStep, - installTNCCNAMEStep, - installBootstrapStep, - installTNCARecordStep, - installEtcdStep, - }, - } -} - -// NewInstallJoinWorkflow creates new instances of the 'join' workflow, -// responsible for running the actions necessary to scale the machines of the cluster. -func NewInstallJoinWorkflow(clusterDir string) Workflow { - return Workflow{ - metadata: metadata{clusterDir: clusterDir}, - steps: []Step{ - readClusterConfigStep, - installJoinMastersStep, - installJoinWorkersStep, - }, - } -} - -func installAssetsStep(m *metadata) error { - return runInstallStep(m, assetsStep) -} - -func installTopologyStep(m *metadata) error { - return runInstallStep(m, topologyStep) -} - -func installBootstrapStep(m *metadata) error { - return runInstallStep(m, bootstrapStep) -} - -func installTNCCNAMEStep(m *metadata) error { - if !clusterIsBootstrapped(m.clusterDir) { - return createTNCCNAME(m) - } - return nil -} - -func installTNCARecordStep(m *metadata) error { - return createTNCARecord(m) -} - -func installEtcdStep(m *metadata) error { - return runInstallStep(m, etcdStep) -} - -func installJoinMastersStep(m *metadata) error { - // TODO: import will fail after a first run, error is ignored for now - importAutoScalingGroup(m) - return runInstallStep(m, joinMastersStep) -} - -func installJoinWorkersStep(m *metadata) error { - return runInstallStep(m, joinWorkersStep) -} - -func runInstallStep(m *metadata, step string, extraArgs ...string) error { - templateDir, err := findStepTemplates(step, m.cluster.Platform) - if err != nil { - return err - } - if err := tfInit(m.clusterDir, templateDir); err != nil { - return err - } - return tfApply(m.clusterDir, step, templateDir, extraArgs...) -} - -func generateIgnConfigStep(m *metadata) error { - c := configgenerator.New(m.cluster) - return c.GenerateIgnConfig(m.clusterDir) -} diff --git a/installer/pkg/workflow/terraform.go b/installer/pkg/workflow/terraform.go index 9b3004c27e..bc823be072 100644 --- a/installer/pkg/workflow/terraform.go +++ b/installer/pkg/workflow/terraform.go @@ -6,21 +6,21 @@ import ( "path/filepath" ) -func terraformExec(clusterDir string, args ...string) error { +func terraformExec(stateDir string, args ...string) error { // Create an executor ex, err := newExecutor() if err != nil { return fmt.Errorf("Could not create Terraform executor: %s", err) } - err = ex.execute(clusterDir, args...) + err = ex.execute(stateDir, args...) if err != nil { return fmt.Errorf("Failed to run Terraform: %s", err) } return nil } -func tfApply(clusterDir string, state string, templateDir string, extraArgs ...string) error { +func tfApply(stateDir string, state string, templateDir string, extraArgs ...string) error { defaultArgs := []string{ "apply", "-auto-approve", @@ -28,7 +28,7 @@ func tfApply(clusterDir string, state string, templateDir string, extraArgs ...s } extraArgs = append(extraArgs, templateDir) args := append(defaultArgs, extraArgs...) - return terraformExec(clusterDir, args...) + return terraformExec(stateDir, args...) } func tfDestroy(clusterDir, state, templateDir string, extraArgs ...string) error { @@ -51,3 +51,30 @@ func hasStateFile(stateDir string, stateName string) bool { _, err := os.Stat(stepStateFile) return !os.IsNotExist(err) } + +// returns the directory containing templates for a given step. If platform is +// specified, it looks for a subdirectory with platform first, falling back if +// there are no platform-specific templates for that step +func findStepTemplates(stepName, platform string) (string, error) { + base, err := baseLocation() + if err != nil { + return "", fmt.Errorf("error looking up step %s templates: %v", stepName, err) + } + for _, path := range []string{ + filepath.Join(base, stepsBaseDir, stepName, platform), + filepath.Join(base, stepsBaseDir, stepName)} { + + stat, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + continue + } + return "", fmt.Errorf("invalid path for '%s' templates: %s", base, err) + } + if !stat.IsDir() { + return "", fmt.Errorf("invalid path for '%s' templates", base) + } + return path, nil + } + return "", os.ErrNotExist +} diff --git a/installer/pkg/workflow/utils.go b/installer/pkg/workflow/utils.go index 0a7b6c9593..77a5c020c4 100644 --- a/installer/pkg/workflow/utils.go +++ b/installer/pkg/workflow/utils.go @@ -6,30 +6,10 @@ import ( "io" "os" "path" - "path/filepath" - - "github.com/coreos/tectonic-installer/installer/pkg/config" - configgenerator "github.com/coreos/tectonic-installer/installer/pkg/config-generator" - - log "github.com/Sirupsen/logrus" ) const ( - stepsBaseDir = "steps" - assetsStep = "assets" - topologyStep = "topology" - tncDNSStep = "tnc_dns" - bootstrapOn = "-var=tectonic_aws_bootstrap=true" - bootstrapOff = "-var=tectonic_aws_bootstrap=false" - bootstrapStep = "bootstrap" - etcdStep = "etcd" - joinMastersStep = "joining_masters" - joinWorkersStep = "joining_workers" - configFileName = "config.yaml" - internalFileName = "internal.yaml" - kubeConfigPath = "generated/auth/kubeconfig" - binaryPrefix = "installer" - tncDaemonSet = "tectonic-node-controller" + binaryPrefix = "tectonic-installer" ) func copyFile(fromFilePath, toFilePath string) error { @@ -49,150 +29,6 @@ func copyFile(fromFilePath, toFilePath string) error { return err } -// returns the directory containing templates for a given step. If platform is -// specified, it looks for a subdirectory with platform first, falling back if -// there are no platform-specific templates for that step -func findStepTemplates(stepName, platform string) (string, error) { - base, err := baseLocation() - if err != nil { - return "", fmt.Errorf("error looking up step %s templates: %v", stepName, err) - } - for _, path := range []string{ - filepath.Join(base, stepsBaseDir, stepName, platform), - filepath.Join(base, stepsBaseDir, stepName)} { - - stat, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - continue - } - return "", fmt.Errorf("invalid path for '%s' templates: %s", base, err) - } - if !stat.IsDir() { - return "", fmt.Errorf("invalid path for '%s' templates", base) - } - return path, nil - } - return "", os.ErrNotExist -} - -func generateClusterConfigMaps(m *metadata) error { - clusterGeneratedPath := filepath.Join(m.clusterDir, generatedPath) - if err := os.MkdirAll(clusterGeneratedPath, os.ModeDir|0755); err != nil { - return fmt.Errorf("Failed to create cluster generated directory at %s", clusterGeneratedPath) - } - - configGenerator := configgenerator.New(m.cluster) - - kcoConfig, err := configGenerator.CoreConfig() - if err != nil { - return err - } - - kcoConfigFilePath := filepath.Join(clusterGeneratedPath, kcoConfigFileName) - if err := writeFile(kcoConfigFilePath, kcoConfig); err != nil { - return err - } - - tncoConfig, err := configGenerator.TncoConfig() - if err != nil { - return err - } - - tncoConfigFilePath := filepath.Join(clusterGeneratedPath, tncoConfigFileName) - if err := writeFile(tncoConfigFilePath, tncoConfig); err != nil { - return err - } - - kubeSystem, err := configGenerator.KubeSystem() - if err != nil { - return err - } - - kubePath := filepath.Join(m.clusterDir, kubeSystemPath) - if err := os.MkdirAll(kubePath, os.ModeDir|0755); err != nil { - return fmt.Errorf("Failed to create manifests directory at %s", kubePath) - } - - kubeSystemConfigFilePath := filepath.Join(kubePath, kubeSystemFileName) - if err := writeFile(kubeSystemConfigFilePath, kubeSystem); err != nil { - return err - } - - tectonicSystem, err := configGenerator.TectonicSystem() - if err != nil { - return err - } - - tectonicPath := filepath.Join(m.clusterDir, tectonicSystemPath) - if err := os.MkdirAll(tectonicPath, os.ModeDir|0755); err != nil { - return fmt.Errorf("Failed to create tectonic directory at %s", tectonicPath) - } - - tectonicSystemConfigFilePath := filepath.Join(tectonicPath, tectonicSystemFileName) - return writeFile(tectonicSystemConfigFilePath, tectonicSystem) -} - -func importAutoScalingGroup(m *metadata) error { - templatesPath, err := findStepTemplates(joinMastersStep, m.cluster.Platform) - if err != nil { - return err - } - return terraformExec( - m.clusterDir, - "import", - fmt.Sprintf("-state=%s.tfstate", joinMastersStep), - fmt.Sprintf("-config=%s", templatesPath), - "aws_autoscaling_group.masters", - fmt.Sprintf("%s-masters", m.cluster.Name)) -} - -func readClusterConfig(configFilePath string, internalFilePath string) (*config.Cluster, error) { - cfg, err := config.ParseConfigFile(configFilePath) - if err != nil { - return nil, fmt.Errorf("%s is not a valid config file: %s", configFilePath, err) - } - - if internalFilePath != "" { - internal, err := config.ParseInternalFile(internalFilePath) - if err != nil { - return nil, fmt.Errorf("%s is not a valid internal file: %s", internalFilePath, err) - } - cfg.Internal = *internal - } - - return cfg, nil -} - -func readClusterConfigStep(m *metadata) error { - var configFilePath string - var internalFilePath string - - if m.configFilePath != "" { - configFilePath = m.configFilePath - } else { - configFilePath = filepath.Join(m.clusterDir, configFileName) - internalFilePath = filepath.Join(m.clusterDir, internalFileName) - } - - cluster, err := readClusterConfig(configFilePath, internalFilePath) - if err != nil { - return err - } - - if errs := cluster.Validate(); len(errs) != 0 { - log.Errorf("Found %d errors in the cluster definition:", len(errs)) - for i, err := range errs { - log.Errorf("error %d: %v", i+1, err) - } - return fmt.Errorf("found %d cluster definition errors", len(errs)) - } - - m.cluster = *cluster - - return nil -} - func writeFile(path, content string) error { f, err := os.Create(path) if err != nil { @@ -220,21 +56,3 @@ func baseLocation() (string, error) { } return path.Dir(ex), nil } - -func clusterIsBootstrapped(stateDir string) bool { - return hasStateFile(stateDir, topologyStep) && - hasStateFile(stateDir, bootstrapStep) && - hasStateFile(stateDir, tncDNSStep) -} - -func createTNCCNAME(m *metadata) error { - return runInstallStep(m, tncDNSStep, []string{bootstrapOn}...) -} - -func createTNCARecord(m *metadata) error { - return runInstallStep(m, tncDNSStep, []string{bootstrapOff}...) -} - -func destroyTNCDNS(m *metadata) error { - return runDestroyStep(m, tncDNSStep, []string{bootstrapOff}...) -} diff --git a/installer/pkg/workflow/workflow.go b/installer/pkg/workflow/workflow.go deleted file mode 100644 index 849b1d92c5..0000000000 --- a/installer/pkg/workflow/workflow.go +++ /dev/null @@ -1,38 +0,0 @@ -package workflow - -import "github.com/coreos/tectonic-installer/installer/pkg/config" - -// metadata is the state store of the current workflow execution. -// It is meant to carry state for one step to another. -// When creating a new workflow, initial state from external parameters -// is also injected by when initializing the metadata object. -// Steps taked thier inputs from the metadata object and persist -// results onto it for later consumption. -type metadata struct { - cluster config.Cluster - configFilePath string - clusterDir string -} - -// Step is the entrypoint of a workflow step implementation. -// To add a new step, put your logic in a function that matches this signature. -// Next, add a refrence to this new function in a Workflow's steps list. -type Step func(*metadata) error - -// Workflow is a high-level representation -// of a set of actions performed in a predictable order. -type Workflow struct { - metadata metadata - steps []Step -} - -// Execute runs all steps in order. -func (w Workflow) Execute() error { - for _, step := range w.steps { - if err := step(&w.metadata); err != nil { - return err - } - } - - return nil -} diff --git a/installer/pkg/workflow/workflow_test.go b/installer/pkg/workflow/workflow_test.go deleted file mode 100644 index 71a35eaadf..0000000000 --- a/installer/pkg/workflow/workflow_test.go +++ /dev/null @@ -1,53 +0,0 @@ -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 := Workflow{ - 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)) - } - } -} diff --git a/tests/rspec/lib/aws_cluster.rb b/tests/rspec/lib/aws_cluster.rb index ea3551aba3..3f930bca33 100644 --- a/tests/rspec/lib/aws_cluster.rb +++ b/tests/rspec/lib/aws_cluster.rb @@ -166,7 +166,9 @@ def init env = env_variables env['TF_INIT_OPTIONS'] = '-no-color' - run_tectonic_cli(env, 'init', '--config=config.yaml') + run_tectonic_cli(env, 'init', "--config=config.yaml --workspace=#{@name}") + # The config within the build folder is the source of truth after init + @config_file = ConfigFile.new(File.expand_path("#{@name}/config.yaml")) end rescue Timeout::Error forensic(false) @@ -180,7 +182,7 @@ def apply env['TF_APPLY_OPTIONS'] = '-no-color' env['TF_INIT_OPTIONS'] = '-no-color' - run_tectonic_cli(env, 'install', "--dir=#{@name}") + run_tectonic_cli(env, 'install', "--workspace=#{@name}") end end rescue Timeout::Error @@ -195,7 +197,7 @@ def destroy env = env_variables env['TF_DESTROY_OPTIONS'] = '-no-color' env['TF_INIT_OPTIONS'] = '-no-color' - run_tectonic_cli(env, 'destroy', "--dir=#{@name}") + run_tectonic_cli(env, 'destroy', "--workspace=#{@name}") end end