diff --git a/.gitignore b/.gitignore index 84749f1..6a411c9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ _book tk8 dist bin + +kubespray +testcase \ No newline at end of file diff --git a/cmd/aws.go b/cmd/aws.go index 1ad19a9..8b74465 100644 --- a/cmd/aws.go +++ b/cmd/aws.go @@ -17,6 +17,8 @@ package cmd import ( "os" + "github.com/kubernauts/tk8/internal/cluster/oshelper" + "github.com/kubernauts/tk8/internal/cluster" "github.com/spf13/cobra" ) @@ -33,12 +35,14 @@ Kindly ensure that terraform is installed also.`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { + aws := cluster.NewAWS("DefineWorkspace", cluster.DistOSMap, oshelper.NewOSHelper()) + if install { cluster.AWSInstall() } if create { - cluster.AWSCreate() + aws.Create() } if destroy { diff --git a/configs/templates/config.yaml b/configs/templates/config.yaml new file mode 100644 index 0000000..cf4a5eb --- /dev/null +++ b/configs/templates/config.yaml @@ -0,0 +1,23 @@ +aws: + clustername: kubernauts + os: coreos + master: 1 + worker: 1 + etcd: 1 + aws_access_key_id: + aws_secret_access_key: + aws_ssh_keypair: + aws_default_region: + aws_vpc_cidr_block : "10.250.192.0/18" + aws_cidr_subnets_private : '["10.250.192.0/20","10.250.208.0/20"]' + aws_cidr_subnets_public : '["10.250.224.0/20","10.250.240.0/20"]' + aws_bastion_size : "t2.medium" + aws_kube_master_num : 1 + aws_kube_master_size : "t2.medium" + aws_etcd_num : 1 + aws_etcd_size : "t2.medium" + aws_kube_worker_num : 2 + aws_kube_worker_size : "t2.medium" + aws_elb_api_port : 6443 + k8s_secure_api_port : 6443 + kube_insecure_apiserver_address : "0.0.0.0" diff --git a/configs/templates/kubespray-aws-create-infra.tf b/configs/templates/kubespray-aws-create-infra.tf new file mode 100644 index 0000000..d0486ac --- /dev/null +++ b/configs/templates/kubespray-aws-create-infra.tf @@ -0,0 +1,191 @@ +terraform { + required_version = ">= 0.8.7" +} + +provider "aws" { + access_key = "${var.AWS_ACCESS_KEY_ID}" + secret_key = "${var.AWS_SECRET_ACCESS_KEY}" + region = "${var.AWS_DEFAULT_REGION}" +} + +data "aws_availability_zones" "available" {} + +/* +* Calling modules who create the initial AWS VPC / AWS ELB +* and AWS IAM Roles for Kubernetes Deployment +*/ + +module "aws-vpc" { + source = "modules/vpc" + + aws_cluster_name = "${var.aws_cluster_name}" + aws_vpc_cidr_block = "${var.aws_vpc_cidr_block}" + aws_avail_zones="${slice(data.aws_availability_zones.available.names,0,2)}" + aws_cidr_subnets_private="${var.aws_cidr_subnets_private}" + aws_cidr_subnets_public="${var.aws_cidr_subnets_public}" + default_tags="${var.default_tags}" + +} + + +module "aws-elb" { + source = "modules/elb" + + aws_cluster_name="${var.aws_cluster_name}" + aws_vpc_id="${module.aws-vpc.aws_vpc_id}" + aws_avail_zones="${slice(data.aws_availability_zones.available.names,0,2)}" + aws_subnet_ids_public="${module.aws-vpc.aws_subnet_ids_public}" + aws_elb_api_port = "${var.aws_elb_api_port}" + k8s_secure_api_port = "${var.k8s_secure_api_port}" + default_tags="${var.default_tags}" + +} + +module "aws-iam" { + source = "modules/iam" + + aws_cluster_name="${var.aws_cluster_name}" +} + +/* +* Create Bastion Instances in AWS +* +*/ + +resource "aws_instance" "bastion-server" { + ami = "{{.OS}}" + instance_type = "${var.aws_bastion_size}" + count = "${length(var.aws_cidr_subnets_public)}" + associate_public_ip_address = true + availability_zone = "${element(slice(data.aws_availability_zones.available.names,0,2),count.index)}" + subnet_id = "${element(module.aws-vpc.aws_subnet_ids_public,count.index)}" + + + vpc_security_group_ids = [ "${module.aws-vpc.aws_security_group}" ] + + key_name = "${var.AWS_SSH_KEY_NAME}" + + tags = "${merge(var.default_tags, map( + "Name", "kubernetes-${var.aws_cluster_name}-bastion-${count.index}", + "Cluster", "${var.aws_cluster_name}", + "Role", "bastion-${var.aws_cluster_name}-${count.index}" + ))}" +} + + +/* +* Create K8s Master and worker nodes and etcd instances +* +*/ + +resource "aws_instance" "k8s-master" { + ami = "{{.OS}}" + instance_type = "${var.aws_kube_master_size}" + + count = "${var.aws_kube_master_num}" + + + availability_zone = "${element(slice(data.aws_availability_zones.available.names,0,2),count.index)}" + subnet_id = "${element(module.aws-vpc.aws_subnet_ids_private,count.index)}" + + + vpc_security_group_ids = [ "${module.aws-vpc.aws_security_group}" ] + + + iam_instance_profile = "${module.aws-iam.kube-master-profile}" + key_name = "${var.AWS_SSH_KEY_NAME}" + + + tags = "${merge(var.default_tags, map( + "Name", "kubernetes-${var.aws_cluster_name}-master${count.index}", + "kubernetes.io/cluster/${var.aws_cluster_name}", "member", + "Role", "master" + ))}" +} + +resource "aws_elb_attachment" "attach_master_nodes" { + count = "${var.aws_kube_master_num}" + elb = "${module.aws-elb.aws_elb_api_id}" + instance = "${element(aws_instance.k8s-master.*.id,count.index)}" +} + + +resource "aws_instance" "k8s-etcd" { + ami = "{{.OS}}" + instance_type = "${var.aws_etcd_size}" + + count = "${var.aws_etcd_num}" + + + availability_zone = "${element(slice(data.aws_availability_zones.available.names,0,2),count.index)}" + subnet_id = "${element(module.aws-vpc.aws_subnet_ids_private,count.index)}" + + + vpc_security_group_ids = [ "${module.aws-vpc.aws_security_group}" ] + + key_name = "${var.AWS_SSH_KEY_NAME}" + + tags = "${merge(var.default_tags, map( + "Name", "kubernetes-${var.aws_cluster_name}-etcd${count.index}", + "kubernetes.io/cluster/${var.aws_cluster_name}", "member", + "Role", "etcd" + ))}" + +} + + +resource "aws_instance" "k8s-worker" { + ami = "{{.OS}}" + instance_type = "${var.aws_kube_worker_size}" + + count = "${var.aws_kube_worker_num}" + + availability_zone = "${element(slice(data.aws_availability_zones.available.names,0,2),count.index)}" + subnet_id = "${element(module.aws-vpc.aws_subnet_ids_private,count.index)}" + + vpc_security_group_ids = [ "${module.aws-vpc.aws_security_group}" ] + + iam_instance_profile = "${module.aws-iam.kube-worker-profile}" + key_name = "${var.AWS_SSH_KEY_NAME}" + + + tags = "${merge(var.default_tags, map( + "Name", "kubernetes-${var.aws_cluster_name}-worker${count.index}", + "kubernetes.io/cluster/${var.aws_cluster_name}", "member", + "Role", "worker" + ))}" + +} + + + +/* +* Create Kubespray Inventory File +* +*/ +data "template_file" "inventory" { + template = "${file("${path.module}/templates/inventory.tpl")}" + + vars { + public_ip_address_bastion = "${join("\n",formatlist("bastion ansible_host=%s" , aws_instance.bastion-server.*.public_ip))}" + connection_strings_master = "${join("\n",formatlist("%s ansible_host=%s",aws_instance.k8s-master.*.tags.Name, aws_instance.k8s-master.*.private_ip))}" + connection_strings_node = "${join("\n", formatlist("%s ansible_host=%s", aws_instance.k8s-worker.*.tags.Name, aws_instance.k8s-worker.*.private_ip))}" + connection_strings_etcd = "${join("\n",formatlist("%s ansible_host=%s", aws_instance.k8s-etcd.*.tags.Name, aws_instance.k8s-etcd.*.private_ip))}" + list_master = "${join("\n",aws_instance.k8s-master.*.tags.Name)}" + list_node = "${join("\n",aws_instance.k8s-worker.*.tags.Name)}" + list_etcd = "${join("\n",aws_instance.k8s-etcd.*.tags.Name)}" + elb_api_fqdn = "apiserver_loadbalancer_domain_name=\"${module.aws-elb.aws_elb_api_fqdn}\"" + } + +} + +resource "null_resource" "inventories" { + provisioner "local-exec" { + command = "echo '${data.template_file.inventory.rendered}' > ../../../inventory/hosts" + } + + triggers { + template = "${data.template_file.inventory.rendered}" + } + +} \ No newline at end of file diff --git a/configs/templates/kubespray-aws-credentials.tfvars b/configs/templates/kubespray-aws-credentials.tfvars new file mode 100644 index 0000000..7ebce7a --- /dev/null +++ b/configs/templates/kubespray-aws-credentials.tfvars @@ -0,0 +1,4 @@ +AWS_ACCESS_KEY_ID = "{{.AwsAccessKeyID}}" +AWS_SECRET_ACCESS_KEY = "{{.AwsSecretKey}}" +AWS_SSH_KEY_NAME = "{{.AwsAccessSSHKey}}" +AWS_DEFAULT_REGION = "{{.AwsDefaultRegion}}" \ No newline at end of file diff --git a/configs/templates/kubespray-aws-output.tf b/configs/templates/kubespray-aws-output.tf new file mode 100644 index 0000000..5125030 --- /dev/null +++ b/configs/templates/kubespray-aws-output.tf @@ -0,0 +1,28 @@ +output "bastion_ip" { + value = "${join("\n", aws_instance.bastion-server.*.public_ip)}" +} + +output "masters" { + value = "${join("\n", aws_instance.k8s-master.*.private_ip)}" +} + +output "workers" { + value = "${join("\n", aws_instance.k8s-worker.*.private_ip)}" +} + +output "etcd" { + value = "${join("\n", aws_instance.k8s-etcd.*.private_ip)}" +} + + +output "aws_elb_api_fqdn" { + value = "${module.aws-elb.aws_elb_api_fqdn}:${var.aws_elb_api_port}" +} + +output "inventory" { + value = "${data.template_file.inventory.rendered}" +} + +output "default_tags" { + value = "${var.default_tags}" +} diff --git a/configs/templates/kubespray-aws-terraform.tfvars b/configs/templates/kubespray-aws-terraform.tfvars new file mode 100644 index 0000000..2f51335 --- /dev/null +++ b/configs/templates/kubespray-aws-terraform.tfvars @@ -0,0 +1,21 @@ +aws_cluster_name = "{{.AwsClusterName}}" +aws_vpc_cidr_block = "{{.AwsVpcCidrBlock}}" +aws_cidr_subnets_private = "{{.AwsCidrSubnetsPrivate}}" +aws_cidr_subnets_public = "{{.AwsCidrSubnetsPublic}}" + +aws_bastion_size = "{{.AwsBastionSize}}" +aws_kube_master_num = "{{.AwsKubeMasterNum}}" +aws_kube_master_size = "{{.AwsKubeMasterSize}}" +aws_etcd_num = "{{.AwsEtcdNum}}" + +aws_etcd_size = "{{.AwsEtcdSize}}" +aws_kube_worker_num = "{{.AwsKubeWorkerNum}}" +aws_kube_worker_size = "{{.AwsKubeWorkerSize}}" +aws_elb_api_port = "{{.AwsElbAPIPort}}" +k8s_secure_api_port = "{{.K8sSecureAPIPort}}" +kube_insecure_apiserver_address = "{{.KubeInsecureApiserverAddress}}" + +default_tags = { + Env = "devtest" + Product = "kubernetes" +} \ No newline at end of file diff --git a/configs/templates/kubespray-aws-variables.tf b/configs/templates/kubespray-aws-variables.tf new file mode 100644 index 0000000..c939200 --- /dev/null +++ b/configs/templates/kubespray-aws-variables.tf @@ -0,0 +1,105 @@ +variable "AWS_ACCESS_KEY_ID" { + description = "AWS Access Key" +} + +variable "AWS_SECRET_ACCESS_KEY" { + description = "AWS Secret Key" +} + +variable "AWS_SSH_KEY_NAME" { + description = "Name of the SSH keypair to use in AWS." +} + +variable "AWS_DEFAULT_REGION" { + description = "AWS Region" +} + +//General Cluster Settings + +variable "aws_cluster_name" { + description = "Name of AWS Cluster" +} + +data "aws_ami" "distro" { + most_recent = true + + filter { + name = "name" + values = ["{{.OS}}"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + owners = ["{{.AmiOwner}}"] #CoreOS +} + +//AWS VPC Variables + +variable "aws_vpc_cidr_block" { + description = "CIDR Block for VPC" +} + +variable "aws_cidr_subnets_private" { + description = "CIDR Blocks for private subnets in Availability Zones" + type = "list" +} + +variable "aws_cidr_subnets_public" { + description = "CIDR Blocks for public subnets in Availability Zones" + type = "list" +} + +//AWS EC2 Settings + +variable "aws_bastion_size" { + description = "EC2 Instance Size of Bastion Host" +} + +/* +* AWS EC2 Settings +* The number should be divisable by the number of used +* AWS Availability Zones without an remainder. +*/ +variable "aws_kube_master_num" { + description = "Number of Kubernetes Master Nodes" +} + +variable "aws_kube_master_size" { + description = "Instance size of Kube Master Nodes" +} + +variable "aws_etcd_num" { + description = "Number of etcd Nodes" +} + +variable "aws_etcd_size" { + description = "Instance size of etcd Nodes" +} + +variable "aws_kube_worker_num" { + description = "Number of Kubernetes Worker Nodes" +} + +variable "aws_kube_worker_size" { + description = "Instance size of Kubernetes Worker Nodes" +} + +/* +* AWS ELB Settings +* +*/ +variable "aws_elb_api_port" { + description = "Port for AWS ELB" +} + +variable "k8s_secure_api_port" { + description = "Secure Port of K8S API Server" +} + +variable "default_tags" { + description = "Default tags for all resources" + type = "map" +} diff --git a/internal/cluster/aws.go b/internal/cluster/aws.go index fbf0795..3a056c9 100644 --- a/internal/cluster/aws.go +++ b/internal/cluster/aws.go @@ -1,232 +1,221 @@ -// Copyright © 2018 NAME HERE -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package cluster import ( "bufio" "fmt" + "html/template" + "io" + "io/ioutil" "log" "net" "os" "os/exec" - "strconv" "strings" + "path/filepath" + + "github.com/kubernauts/tk8/internal/cluster/oshelper" "github.com/spf13/viper" ) -var ec2IP string - -func distSelect() (string, string) { - var sshUser, osLabel string - - centos := map[string]string{ - "user": "centos", - "ami_owner": "688023202711", - "os": "dcos-centos7", - } - - ubuntu := map[string]string{ - "user": "ubuntu", - "ami_owner": "099720109477", - "os": "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64", - } - - //Read Configuration File - viper.SetConfigName("config") - - viper.AddConfigPath(".") - verr := viper.ReadInConfig() // Find and read the config file - if verr != nil { // Handle errors reading the config file - panic(fmt.Errorf("fatal error config file: %s", verr)) - } - - awsAmiID := viper.GetString("aws.ami_id") - awsInstanceOS := viper.GetString("aws.os") - sshUser = viper.GetString("aws.ssh_user") +// AWS is the main structer of the platform controller +type AWS struct { + Dists map[string]DistOS + Ec2IP string + Namespace string + OSHelper oshelper.OSHelper +} - // Think of a better way to do this - if awsInstanceOS != "" { - fmt.Println(awsInstanceOS) - switch awsInstanceOS { - case "centos": - exec.Command("sh", "-c", "sed -i \"\" -e 's/dcos-centos7/"+centos["os"]+"/g' ./kubespray/contrib/terraform/aws/variables.tf").Run() - exec.Command("sh", "-c", "sed -i \"\" -e 's/688023202711/"+centos["ami_owner"]+"/g' ./kubespray/contrib/terraform/aws/variables.tf").Run() - sshUser = centos["user"] - osLabel = "centos" - case "ubuntu": - exec.Command("sh", "-c", "sed -i \"\" -e 's#dcos-centos7#"+ubuntu["os"]+"#g' ./kubespray/contrib/terraform/aws/variables.tf").Run() - exec.Command("sh", "-c", "sed -i \"\" -e 's/688023202711/"+ubuntu["ami_owner"]+"/g' ./kubespray/contrib/terraform/aws/variables.tf").Run() - sshUser = ubuntu["user"] - osLabel = "ubuntu" - // Will only work with 'https://github.com/kubernetes-incubator/kubespray' - default: - sshUser = "core" - osLabel = "coreos" - return sshUser, osLabel - } - } else if awsAmiID != "" && sshUser != "" { - err := exec.Command("sh", "-c", "sed -i \"\" -e 's/${data.aws_ami.distro.id}/"+awsAmiID+"/g' ./kubespray/contrib/terraform/aws/create-infrastructure.tf").Run() - if err != nil { - log.Fatal("Cannot replace AMI ID in Infrastructure template", err) - } - osLabel = "Custom-AMI" - } else if awsAmiID != "" && sshUser == "" { - log.Fatal("SSH Username is required when using custom AMI") - return "", "" - } else { - log.Fatal("Provide either of AMI ID or OS in the config file.") - return "", "" - } +type AwsCredentials struct { + AwsAccessKeyID string + AwsSecretKey string + AwsAccessSSHKey string + AwsDefaultRegion string +} - return sshUser, osLabel +// GetAbsPath return the absoltue path of a file somewhere in the folder structure +func (aws *AWS) GetAbsPath(filePath string) string { + absPath, _ := filepath.Abs("../../" + filePath) + return absPath } -func AWSCreate() { - // check if terraform is installed - terr, err := exec.LookPath("terraform") +// CreateFileFromTemplate create config files from templates +func (aws *AWS) CreateFileFromTemplate(templateName string, targetFileName string, awsInstanceOS string, data interface{}) bool { + + _ = os.Mkdir(aws.GetAbsPath(".")+"/"+aws.Namespace, os.ModePerm) + file, err := os.Create(aws.GetAbsPath(".") + "/" + aws.Namespace + "/" + targetFileName) if err != nil { - log.Fatal("Terraform command not found, kindly check") + aws.OSHelper.FatalLog("Cannot create file", err) + return false } - fmt.Printf("Found terraform at %s\n", terr) - rr, err := exec.Command("terraform", "version").Output() + defer file.Close() + template := template.Must(template.ParseFiles(aws.GetAbsPath(templateName))) if err != nil { - log.Fatal(err) + aws.OSHelper.FatalLog(templateName, "for", awsInstanceOS, "could not parsed") + return false } - fmt.Printf(string(rr)) - - // Check if credentials file exist, if it exists skip asking to input the AWS values - if _, err := os.Stat("./kubespray/contrib/terraform/aws/credentials.tfvars"); err == nil { - fmt.Println("Credentials file already exists, creation skipped") - } else { - - //Read Configuration File - viper.SetConfigName("config") - - viper.AddConfigPath(".") - viper.AddConfigPath("/tk8") - verr := viper.ReadInConfig() // Find and read the config file - if verr != nil { // Handle errors reading the config file - panic(fmt.Errorf("fatal error config file: %s", verr)) - } - - awsAccessKeyID := viper.GetString("aws.aws_access_key_id") + if data == nil { + template.Execute(file, aws.Dists[awsInstanceOS]) + return true + } + template.Execute(file, data) + return true +} - awsSecretKey := viper.GetString("aws.aws_secret_access_key") +// GetConfig configs from viper +func (aws *AWS) GetConfig() (string, string, string) { + aws.readViperConfigFile("config") + awsAmiID := viper.GetString("aws.ami_id") + awsInstanceOS := viper.GetString("aws.os") + sshUser := viper.GetString("aws.ssh_user") + return awsAmiID, awsInstanceOS, sshUser +} - awsAccessSSHKey := viper.GetString("aws.aws_ssh_keypair") +// DistSelect choose the Dist and return sshUser and osLabel +func (aws *AWS) DistSelect() (string, string) { - awsDefaultRegion := viper.GetString("aws.aws_default_region") + awsAmiID, awsInstanceOS, sshUser := aws.GetConfig() - file, err := os.Create("./kubespray/contrib/terraform/aws/credentials.tfvars") - if err != nil { - log.Fatal("Cannot create file", err) + if awsAmiID != "" && sshUser != "" { + awsInstanceOS = "custom" + aws.Dists[awsInstanceOS] = DistOS{ + User: sshUser, + AmiOwner: "", + OS: awsAmiID, } - defer file.Close() + } - fmt.Fprintf(file, "AWS_ACCESS_KEY_ID = %s\n", strconv.Quote(awsAccessKeyID)) - fmt.Fprintf(file, "AWS_SECRET_ACCESS_KEY = %s\n", strconv.Quote(awsSecretKey)) - fmt.Fprintf(file, "AWS_SSH_KEY_NAME = %s\n", strconv.Quote(awsAccessSSHKey)) - fmt.Fprintf(file, "AWS_DEFAULT_REGION = %s\n", strconv.Quote(awsDefaultRegion)) + // TODO: clean up debug + aws.OSHelper.Log(awsInstanceOS) + if awsInstanceOS == "" && awsAmiID == "" { + log.Fatal("Provide either of AMI ID or OS in the config file.") + return "", "" + } + if awsAmiID != "" && sshUser == "" { + log.Fatal("SSH Username is required when using custom AMI") + return "", "" } - // Remove tftvars file - err = os.Remove("./kubespray/contrib/terraform/aws/terraform.tfvars") - if err != nil { - fmt.Println(err) + // TODO change to parallel creation + // prepare config + if !aws.CreateFileFromTemplate("/configs/templates/kubespray-aws-variables.tf", "variables.tf", awsInstanceOS, nil) { + return "", "" + } + if !aws.CreateFileFromTemplate("/configs/templates/kubespray-aws-create-infra.tf", "create-infrastructure.tf", awsInstanceOS, nil) { + return "", "" } + return sshUser, awsInstanceOS +} +func (aws *AWS) readViperConfigFile(configName string) { //Read Configuration File - viper.SetConfigName("config") - + viper.SetConfigName(configName) viper.AddConfigPath(".") + viper.AddConfigPath("/tk8") + viper.AddConfigPath("./../..") verr := viper.ReadInConfig() // Find and read the config file if verr != nil { // Handle errors reading the config file panic(fmt.Errorf("fatal error config file: %s", verr)) } +} + +// GetCredentials get the aws credentials from config file +func (aws *AWS) GetCredentials() AwsCredentials { + aws.readViperConfigFile("config") + return AwsCredentials{ + AwsAccessKeyID: viper.GetString("aws.aws_access_key_id"), + AwsSecretKey: viper.GetString("aws.aws_secret_access_key"), + AwsAccessSSHKey: viper.GetString("aws.aws_ssh_keypair"), + AwsDefaultRegion: viper.GetString("aws.aws_default_region"), + } +} + +// GetClusterConfig get the configuration from config file +func (aws *AWS) GetClusterConfig() ClusterConfig { + aws.readViperConfigFile("config") + return ClusterConfig{ + AwsClusterName: viper.GetString("aws.clustername"), + AwsVpcCidrBlock: viper.GetString("aws.aws_vpc_cidr_block"), + AwsCidrSubnetsPrivate: viper.GetString("aws.aws_cidr_subnets_private"), + AwsCidrSubnetsPublic: viper.GetString("aws.aws_cidr_subnets_public"), + AwsBastionSize: viper.GetString("aws.aws_bastion_size"), + AwsKubeMasterNum: viper.GetString("aws.aws_kube_master_num"), + AwsKubeMasterSize: viper.GetString("aws.aws_kube_master_size"), + AwsEtcdNum: viper.GetString("aws.aws_etcd_num"), + AwsEtcdSize: viper.GetString("aws.aws_etcd_size"), + AwsKubeWorkerNum: viper.GetString("aws.aws_kube_worker_num"), + AwsKubeWorkerSize: viper.GetString("aws.aws_kube_worker_size"), + AwsElbAPIPort: viper.GetString("aws.aws_elb_api_port"), + K8sSecureAPIPort: viper.GetString("aws.k8s_secure_api_port"), + KubeInsecureApiserverAddress: viper.GetString("aws."), + } +} + +func (aws *AWS) prepareBuilderFiles() { - awsClusterName := viper.GetString("aws.clustername") - awsVpcCidrBlock := viper.GetString("aws.aws_vpc_cidr_block") - awsCidrSubnetsPrivate := viper.GetString("aws.aws_cidr_subnets_private") - awsCidrSubnetsPublic := viper.GetString("aws.aws_cidr_subnets_public") - awsBastionSize := viper.GetString("aws.aws_bastion_size") - awsKubeMasterNum := viper.GetString("aws.aws_kube_master_num") - awsKubeMasterSize := viper.GetString("aws.aws_kube_master_size") - awsEtcdNum := viper.GetString("aws.aws_etcd_num") - awsEtcdSize := viper.GetString("aws.aws_etcd_size") - awsKubeWorkerNum := viper.GetString("aws.aws_kube_worker_num") - awsKubeWorkerSize := viper.GetString("aws.aws_kube_worker_size") - awsElbAPIPort := viper.GetString("aws.aws_elb_api_port") - k8sSecureAPIPort := viper.GetString("aws.k8s_secure_api_port") - kubeInsecureApiserverAddress := viper.GetString("aws.") - - tfile, err := os.Create("./kubespray/contrib/terraform/aws/terraform.tfvars") + _ = os.Mkdir(aws.GetAbsPath(".")+"/"+aws.Namespace, os.ModePerm) + + files, err := ioutil.ReadDir("./kubespray/contrib/terraform/aws/") if err != nil { - log.Fatal("Cannot create file", err) + log.Fatal(err) } - defer tfile.Close() - fmt.Fprintf(tfile, "aws_cluster_name = %s\n", strconv.Quote(awsClusterName)) - fmt.Fprintf(tfile, "aws_vpc_cidr_block = %s\n", strconv.Quote(awsVpcCidrBlock)) - fmt.Fprintf(tfile, "aws_cidr_subnets_private = %s\n", awsCidrSubnetsPrivate) - fmt.Fprintf(tfile, "aws_cidr_subnets_public = %s\n", awsCidrSubnetsPublic) + for _, f := range files { + inFileReader, _ := os.Open("./kubespray/contrib/terraform/aws/" + f.Name()) + defer inFileReader.Close() + outWriter, _ := os.Open(aws.GetAbsPath(".") + "/" + aws.Namespace + "/" + f.Name()) + defer outWriter.Close() + io.Copy(outWriter, inFileReader) + } +} - fmt.Fprintf(tfile, "aws_bastion_size = %s\n", strconv.Quote(awsBastionSize)) - fmt.Fprintf(tfile, "aws_kube_master_num = %s\n", awsKubeMasterNum) - fmt.Fprintf(tfile, "aws_kube_master_size = %s\n", strconv.Quote(awsKubeMasterSize)) - fmt.Fprintf(tfile, "aws_etcd_num = %s\n", awsEtcdNum) +// Create a aws kubernetes cluster with terraform +func (aws *AWS) Create() { + if !aws.OSHelper.CheckDependency("terraform") { + return + } - fmt.Fprintf(tfile, "aws_etcd_size = %s\n", strconv.Quote(awsEtcdSize)) - fmt.Fprintf(tfile, "aws_kube_worker_num = %s\n", awsKubeWorkerNum) - fmt.Fprintf(tfile, "aws_kube_worker_size = %s\n", strconv.Quote(awsKubeWorkerSize)) - fmt.Fprintf(tfile, "aws_elb_api_port = %s\n", awsElbAPIPort) - fmt.Fprintf(tfile, "k8s_secure_api_port = %s\n", k8sSecureAPIPort) - fmt.Fprintf(tfile, "kube_insecure_apiserver_address = %s\n", strconv.Quote(kubeInsecureApiserverAddress)) + _, err := aws.OSHelper.Shell("terraform", "version") + if err != nil { + return // cancel process + } - fmt.Fprintf(tfile, "default_tags = {\n") - fmt.Fprintf(tfile, "# Env = 'devtest'\n") - fmt.Fprintf(tfile, "# Product = 'kubernetes'\n") - fmt.Fprintf(tfile, "}") + aws.prepareBuilderFiles() - distSelect() + if _, err := aws.OSHelper.FileInfo("/configs/templates/credentials.tfvars"); err != nil { + if !aws.CreateFileFromTemplate("/configs/templates/kubespray-aws-credentials.tfvars", "credentials.tfvars", aws.Namespace, aws.GetCredentials()) { + return // cancel process + } + } + if !aws.CreateFileFromTemplate("/configs/templates/kubespray-aws-terraform.tfvars", "terraform.tfvars", aws.Namespace, aws.GetClusterConfig()) { + return // cancel process + } - terrInit := exec.Command("terraform", "init") - terrInit.Dir = "./kubespray/contrib/terraform/aws/" + //TODO: extract to a builder same like oshelper + terrInit, _ := aws.OSHelper.Shell("terraform", "init") + terrInit.Dir = "./" + aws.Namespace + "/" out, _ := terrInit.StdoutPipe() terrInit.Start() scanInit := bufio.NewScanner(out) for scanInit.Scan() { m := scanInit.Text() - fmt.Println(m) - //log.Printf(m) + aws.OSHelper.Log(m) } terrInit.Wait() - terrSet := exec.Command("terraform", "apply", "-var-file=credentials.tfvars", "-auto-approve") - terrSet.Dir = "./kubespray/contrib/terraform/aws/" - stdout, err := terrSet.StdoutPipe() + terrSet, _ := aws.OSHelper.Shell("terraform", "apply", "-var-file=credentials.tfvars", "-auto-approve") + + terrSet.Dir = "./" + aws.Namespace + "/" + stdout, _ := terrSet.StdoutPipe() terrSet.Stderr = terrSet.Stdout terrSet.Start() scanner := bufio.NewScanner(stdout) for scanner.Scan() { m := scanner.Text() - fmt.Println(m) - //log.Printf(m) + aws.OSHelper.Log(m) } terrSet.Wait() @@ -234,6 +223,13 @@ func AWSCreate() { } +// NewAWS is the AWS Constructor +func NewAWS(namespace string, distOS map[string]DistOS, oshelper oshelper.OSHelper) AWS { + + aws := AWS{Namespace: namespace, Dists: distOS, OSHelper: oshelper} + return aws +} + func AWSInstall() { // check if ansible is installed terr, err := exec.LookPath("ansible") @@ -262,6 +258,8 @@ func AWSInstall() { fmt.Println("Configuration folder already exists") } else { //os.MkdirAll("./kubespray/inventory/awscluster/group_vars", 0755) + + //TODO: os.Rename to move files exec.Command("cp", "-rfp", "./kubespray/inventory/sample/", "./kubespray/inventory/awscluster/").Run() exec.Command("cp", "./kubespray/inventory/hosts", "./kubespray/inventory/awscluster/hosts").Run() @@ -413,3 +411,72 @@ func AWSDestroy() { os.Exit(0) } + +// depricated +var ec2IP string + +// depricated +func distSelect() (string, string) { + var sshUser, osLabel string + + centos := map[string]string{ + "user": "centos", + "ami_owner": "688023202711", + "os": "dcos-centos7", + } + + ubuntu := map[string]string{ + "user": "ubuntu", + "ami_owner": "099720109477", + "os": "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64", + } + + //Read Configuration File + viper.SetConfigName("config") + + viper.AddConfigPath(".") + verr := viper.ReadInConfig() // Find and read the config file + if verr != nil { // Handle errors reading the config file + panic(fmt.Errorf("fatal error config file: %s", verr)) + } + + awsAmiID := viper.GetString("aws.ami_id") + awsInstanceOS := viper.GetString("aws.os") + sshUser = viper.GetString("aws.ssh_user") + + // Think of a better way to do this + if awsInstanceOS != "" { + fmt.Println(awsInstanceOS) + switch awsInstanceOS { + case "centos": + exec.Command("sh", "-c", "sed -i \"\" -e 's/dcos-centos7/"+centos["os"]+"/g' ./kubespray/contrib/terraform/aws/variables.tf").Run() + exec.Command("sh", "-c", "sed -i \"\" -e 's/688023202711/"+centos["ami_owner"]+"/g' ./kubespray/contrib/terraform/aws/variables.tf").Run() + sshUser = centos["user"] + osLabel = "centos" + case "ubuntu": + exec.Command("sh", "-c", "sed -i \"\" -e 's#dcos-centos7#"+ubuntu["os"]+"#g' ./kubespray/contrib/terraform/aws/variables.tf").Run() + exec.Command("sh", "-c", "sed -i \"\" -e 's/688023202711/"+ubuntu["ami_owner"]+"/g' ./kubespray/contrib/terraform/aws/variables.tf").Run() + sshUser = ubuntu["user"] + osLabel = "ubuntu" + // Will only work with 'https://github.com/kubernetes-incubator/kubespray' + default: + sshUser = "core" + osLabel = "coreos" + return sshUser, osLabel + } + } else if awsAmiID != "" && sshUser != "" { + err := exec.Command("sh", "-c", "sed -i \"\" -e 's/${data.aws_ami.distro.id}/"+awsAmiID+"/g' ./kubespray/contrib/terraform/aws/create-infrastructure.tf").Run() + if err != nil { + log.Fatal("Cannot replace AMI ID in Infrastructure template", err) + } + osLabel = "Custom-AMI" + } else if awsAmiID != "" && sshUser == "" { + log.Fatal("SSH Username is required when using custom AMI") + return "", "" + } else { + log.Fatal("Provide either of AMI ID or OS in the config file.") + return "", "" + } + + return sshUser, osLabel +} diff --git a/internal/cluster/aws_test.go b/internal/cluster/aws_test.go new file mode 100644 index 0000000..edf0199 --- /dev/null +++ b/internal/cluster/aws_test.go @@ -0,0 +1,336 @@ +package cluster + +import ( + "reflect" + "testing" + + "github.com/kubernauts/tk8/internal/cluster/oshelper" +) + +func TestAWS_CreateFileFromTemplate(t *testing.T) { + type fields struct { + Dists map[string]DistOS + Ec2IP string + Namespace string + OSHelper oshelper.OSHelper + } + type args struct { + templateName string + targetFileName string + awsInstanceOS string + data interface{} + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + "test_variables", + fields{ + map[string]DistOS{ + "centos": DistOS{ + User: "centos", + AmiOwner: "688023202711", + OS: "dcos-centos7-*", + }, + }, + "", + "testcase", + oshelper.NewOSHelper(), + }, + args{ + "configs/templates/kubespray-aws-variables.tf", "variables.tf", "centos", nil, + }, + true, + }, + { + "test_infra", + fields{ + map[string]DistOS{ + "centos": DistOS{ + User: "centos", + AmiOwner: "688023202711", + OS: "dcos-centos7-*", + }, + }, + "", + "testcase", + oshelper.NewOSHelper(), + }, + args{ + "configs/templates/kubespray-aws-create-infra.tf", "create-infrastructer.tf", "centos", nil, + }, + true, + }, + { + "test_terraform", + fields{ + map[string]DistOS{ + "centos": DistOS{ + User: "centos", + AmiOwner: "688023202711", + OS: "dcos-centos7-*", + }, + }, + "", + "testcase", + oshelper.NewOSHelper(), + }, + args{ + "configs/templates/kubespray-aws-terraform.tfvars", "terraform.tfvars", "centos", nil, + }, + true, + }, + { + "test_credentials", + fields{ + map[string]DistOS{ + "centos": DistOS{ + User: "centos", + AmiOwner: "688023202711", + OS: "dcos-centos7-*", + }, + }, + "", + "testcase", + oshelper.NewOSHelper(), + }, + args{ + "configs/templates/kubespray-aws-credentials.tfvars", "credentials.tfvars", "centos", nil, + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + aws := &AWS{ + Dists: tt.fields.Dists, + Ec2IP: tt.fields.Ec2IP, + Namespace: tt.fields.Namespace, + OSHelper: tt.fields.OSHelper, + } + switch tt.name { + case "test_terraform": + tt.args.data = aws.GetClusterConfig() + case "test_credentials": + tt.args.data = aws.GetCredentials() + } + + if got := aws.CreateFileFromTemplate(tt.args.templateName, tt.args.targetFileName, tt.args.awsInstanceOS, tt.args.data); got != tt.want { + t.Errorf("AWS.CreateFileFromTemplate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAWS_GetAbsPath(t *testing.T) { + type fields struct { + Dists map[string]DistOS + Ec2IP string + Namespace string + OSHelper oshelper.OSHelper + } + type args struct { + filePath string + } + tests := []struct { + name string + fields fields + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + aws := &AWS{ + Dists: tt.fields.Dists, + Ec2IP: tt.fields.Ec2IP, + Namespace: tt.fields.Namespace, + OSHelper: tt.fields.OSHelper, + } + if got := aws.GetAbsPath(tt.args.filePath); got != tt.want { + t.Errorf("AWS.GetAbsPath() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAWS_GetConfig(t *testing.T) { + type fields struct { + Dists map[string]DistOS + Ec2IP string + Namespace string + OSHelper oshelper.OSHelper + } + tests := []struct { + name string + fields fields + want string + want1 string + want2 string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + aws := &AWS{ + Dists: tt.fields.Dists, + Ec2IP: tt.fields.Ec2IP, + Namespace: tt.fields.Namespace, + OSHelper: tt.fields.OSHelper, + } + got, got1, got2 := aws.GetConfig() + if got != tt.want { + t.Errorf("AWS.GetConfig() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("AWS.GetConfig() got1 = %v, want %v", got1, tt.want1) + } + if got2 != tt.want2 { + t.Errorf("AWS.GetConfig() got2 = %v, want %v", got2, tt.want2) + } + }) + } +} + +func TestAWS_DistSelect(t *testing.T) { + type fields struct { + Dists map[string]DistOS + Ec2IP string + Namespace string + OSHelper oshelper.OSHelper + } + tests := []struct { + name string + fields fields + want string + want1 string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + aws := &AWS{ + Dists: tt.fields.Dists, + Ec2IP: tt.fields.Ec2IP, + Namespace: tt.fields.Namespace, + OSHelper: tt.fields.OSHelper, + } + got, got1 := aws.DistSelect() + if got != tt.want { + t.Errorf("AWS.DistSelect() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("AWS.DistSelect() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func Test_distSelect(t *testing.T) { + tests := []struct { + name string + want string + want1 string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := distSelect() + if got != tt.want { + t.Errorf("distSelect() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("distSelect() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestAWS_GetCredentials(t *testing.T) { + type fields struct { + Dists map[string]DistOS + Ec2IP string + Namespace string + OSHelper oshelper.OSHelper + } + tests := []struct { + name string + fields fields + want AwsCredentials + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + aws := &AWS{ + Dists: tt.fields.Dists, + Ec2IP: tt.fields.Ec2IP, + Namespace: tt.fields.Namespace, + OSHelper: tt.fields.OSHelper, + } + if got := aws.GetCredentials(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("AWS.GetCredentials() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAWS_GetClusterConfig(t *testing.T) { + type fields struct { + Dists map[string]DistOS + Ec2IP string + Namespace string + OSHelper oshelper.OSHelper + } + tests := []struct { + name string + fields fields + want ClusterConfig + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + aws := &AWS{ + Dists: tt.fields.Dists, + Ec2IP: tt.fields.Ec2IP, + Namespace: tt.fields.Namespace, + OSHelper: tt.fields.OSHelper, + } + if got := aws.GetClusterConfig(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("AWS.GetClusterConfig() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAWS_Create(t *testing.T) { + type fields struct { + Dists map[string]DistOS + Ec2IP string + Namespace string + OSHelper oshelper.OSHelper + } + tests := []struct { + name string + fields fields + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + aws := &AWS{ + Dists: tt.fields.Dists, + Ec2IP: tt.fields.Ec2IP, + Namespace: tt.fields.Namespace, + OSHelper: tt.fields.OSHelper, + } + aws.Create() + }) + } +} diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go new file mode 100644 index 0000000..68dd34b --- /dev/null +++ b/internal/cluster/cluster.go @@ -0,0 +1,47 @@ +package cluster + +/* + DistOs struct holds the main dist OS information + It is possible easly extend the list of OS + Append new DistOS to cluster.DistOSMap and use the key(string) in the config +*/ +type DistOS struct { + User string + AmiOwner string + OS string +} + +var DistOSMap = map[string]DistOS{ + "centos": DistOS{ + User: "centos", + AmiOwner: "688023202711", + OS: "dcos-centos7-*", + }, + "ubuntu": DistOS{ + User: "ubuntu", + AmiOwner: "099720109477", + OS: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-*", + }, + "coreos": DistOS{ + User: "core", + AmiOwner: "688023202711", + OS: "CoreOS-stable-*", + }, +} + +type ClusterConfig struct { + AwsClusterName string + AwsVpcCidrBlock string + AwsCidrSubnetsPrivate string + AwsCidrSubnetsPublic string + AwsBastionSize string + AwsKubeMasterNum string + AwsKubeMasterSize string + AwsEtcdNum string + AwsEtcdSize string + AwsKubeWorkerNum string + AwsKubeWorkerSize string + AwsElbAPIPort string + K8sSecureAPIPort string + KubeInsecureApiserverAddress string +} diff --git a/internal/cluster/oshelper/helper.go b/internal/cluster/oshelper/helper.go new file mode 100644 index 0000000..d240d43 --- /dev/null +++ b/internal/cluster/oshelper/helper.go @@ -0,0 +1,18 @@ +package oshelper + +import "os/exec" + +// OSHelper to support differnet OS +type OSHelper interface { + CheckDependency(dependecy string) bool + FatalLog(...interface{}) + Log(...interface{}) + Shell(cmd string, v ...string) (*exec.Cmd, error) + FileInfo(file string) (string, error) +} + +// NewOSHelper is the Constructor for the OSHelper +func NewOSHelper() OSHelper { + // TODO check os and return the Helper + return UnixHelper{} +} diff --git a/internal/cluster/oshelper/unixhelper.go b/internal/cluster/oshelper/unixhelper.go new file mode 100644 index 0000000..659eec6 --- /dev/null +++ b/internal/cluster/oshelper/unixhelper.go @@ -0,0 +1,53 @@ +package oshelper + +import ( + "encoding/json" + "log" + "os" + "os/exec" +) + +// UnixHelper provied some methods for unix +type UnixHelper struct { +} + +// FatalLog for logging Fatal errors +func (h UnixHelper) FatalLog(v ...interface{}) { + log.Fatal(v) +} + +// Log for logging output +func (h UnixHelper) Log(v ...interface{}) { + log.Print(v) +} + +// FileInfo to get information of a file +func (h UnixHelper) FileInfo(file string) (string, error) { + fileInfo, err := os.Stat(file) + fileInfoJSON, _ := json.Marshal(fileInfo) + return string(fileInfoJSON), err +} + +// Shell to execute operstion on the os shell +func (h UnixHelper) Shell(cmd string, v ...string) (*exec.Cmd, error) { + rr := exec.Command(cmd, v...) + logString, err := rr.Output() + if err != nil { + h.FatalLog(err) + return rr, err + } + h.Log(string(logString)) + return rr, err +} + +// CheckDependency checked if the dependency is installed +func (h UnixHelper) CheckDependency(dependecy string) bool { + _, err := exec.LookPath(dependecy) + if err != nil { + h.FatalLog(dependecy, " command not found, kindly check") + return false + } + + h.Log("Found", dependecy) + return true +}