Compare commits

..

4 Commits

Author SHA1 Message Date
Minghe
3627d5bb40 fix ci script (#414) 2019-12-18 11:20:32 +08:00
Minghe
1f7714c1e9 Refactor kubeconfig persist logic (#412)
* add Dir function to config

* clean up

* persist the kubeconf on the fly

Signed-off-by: Minghe Huang <h.minghe@gmail.com>

* fix test

Signed-off-by: Minghe Huang <h.minghe@gmail.com>

* simplicity the cloud config fetch

Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-18 10:58:34 +08:00
Minghe
d868ebf4a1 enable CI check (#410)
* enable CI check

Signed-off-by: Minghe Huang <h.minghe@gmail.com>

* lint

* clean up lint issue

* fix Makefile

* uncomment

* fix coverage

* check infrastructure during up command (#411)

* check infrastructure during up command

Signed-off-by: Minghe Huang <h.minghe@gmail.com>

* add unit test

Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-17 22:54:54 +08:00
Minghe
4640379b06 clean up no use deps (#408) 2019-12-16 18:02:45 +08:00
14 changed files with 279 additions and 102 deletions

View File

@@ -29,7 +29,7 @@ jobs:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: |
export KUBECONFIG="$(kind get kubeconfig-path)"
./scripts/coverage.sh
make unit-test
bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}
- name: build fx

View File

@@ -33,7 +33,7 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
export KUBECONFIG="$(kind get kubeconfig-path)"
DEBUG=true go test -v ./...
make unit-test
- name: build fx
run: |

View File

@@ -26,7 +26,7 @@ clean:
rm -rf ${DIST_DIR}
unit-test:
./scripts/coverage.sh
CI=true ./scripts/coverage.sh
cli-test-ci:
./scripts/test_cli.sh 'js'

View File

@@ -7,6 +7,7 @@ import (
"os"
"os/user"
"path"
"path/filepath"
dockerInfra "github.com/metrue/fx/infra/docker"
"github.com/metrue/fx/types"
@@ -22,6 +23,7 @@ type Configer interface {
UseCloud(name string) error
View() ([]byte, error)
AddCloud(name string, meta []byte) error
Dir() (string, error)
}
// Config config of fx
@@ -191,6 +193,15 @@ func (c *Config) writeDefaultConfig() error {
return c.UseCloud("default")
}
// Dir get directory of config
func (c *Config) Dir() (string, error) {
p, err := filepath.Abs(c.configFile)
if err != nil {
return "", err
}
return path.Dir(p), nil
}
var (
_ Configer = &Config{}
)

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"os/user"
"path/filepath"
"reflect"
"testing"
@@ -49,32 +50,39 @@ func TestConfig(t *testing.T) {
t.Fatalf("should get %s but got %s", "default", cloudMeta["name"])
}
// add k8s cloud
kCloud := k8sInfra.Cloud{
Type: types.CloudTypeK8S,
Config: "sample kubeconfg",
Token: "",
URL: "",
Nodes: map[string]k8sInfra.Noder{
"master-node": &k8sInfra.Node{
IP: "1.1.1.1",
User: "user-1",
Type: "k3s-master",
Name: "master-node",
},
"agent-node-1": &k8sInfra.Node{
IP: "1.1.1.1",
User: "user-1",
Type: "k3s-agent",
Name: "agent-node-1",
},
},
n1, err := k8sInfra.CreateNode(
"1.1.1.1",
"user-1",
"k3s-master",
"master-node",
)
if err != nil {
t.Fatal(err)
}
n2, err := k8sInfra.CreateNode(
"1.1.1.1",
"user-1",
"k3s-agent",
"agent-node-1",
)
if err != nil {
t.Fatal(err)
}
kName := "k8s-1"
kubeconf := "./tmp/" + kName + "config.yml"
defer func() {
if err := os.RemoveAll(kubeconf); err != nil {
t.Fatal(err)
}
}()
// add k8s cloud
kCloud := k8sInfra.NewCloud(kubeconf, n1, n2)
kMeta, err := kCloud.Dump()
if err != nil {
t.Fatal(err)
}
kName := "k8s-1"
if err := c.AddCloud(kName, kMeta); err != nil {
t.Fatal(err)
}
@@ -108,4 +116,16 @@ func TestConfig(t *testing.T) {
t.Fatal(err)
}
fmt.Println(string(body))
dir, err := c.Dir()
if err != nil {
t.Fatal(err)
}
here, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if dir != filepath.Join(here, "./tmp") {
t.Fatalf("should get %s but got %s", "./tmp", dir)
}
}

2
fx.go
View File

@@ -16,7 +16,7 @@ import (
"github.com/urfave/cli"
)
const version = "0.8.74"
const version = "0.8.75"
func init() {
go checkForUpdate()

2
go.mod
View File

@@ -40,8 +40,6 @@ require (
github.com/urfave/cli v1.22.2
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
google.golang.org/grpc v1.21.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.2.7
gotest.tools v2.2.0+incompatible // indirect

View File

@@ -2,6 +2,7 @@ package handlers
import (
"fmt"
"path/filepath"
"strings"
"github.com/metrue/fx/config"
@@ -11,7 +12,7 @@ import (
"github.com/metrue/fx/pkg/spinner"
)
func setupK8S(masterInfo string, agentsInfo string) ([]byte, error) {
func setupK8S(configDir string, name, masterInfo string, agentsInfo string) ([]byte, error) {
info := strings.Split(masterInfo, "@")
if len(info) != 2 {
return nil, fmt.Errorf("incorrect master info, should be <user>@<ip> format")
@@ -36,7 +37,8 @@ func setupK8S(masterInfo string, agentsInfo string) ([]byte, error) {
nodes = append(nodes, node)
}
}
cloud := k8sInfra.NewCloud(nodes...)
kubeconfigPath := filepath.Join(configDir, name+".kubeconfig")
cloud := k8sInfra.NewCloud(kubeconfigPath, nodes...)
if err := cloud.Provision(); err != nil {
return nil, err
}
@@ -91,7 +93,11 @@ func Setup(ctx context.Contexter) (err error) {
switch strings.ToLower(typ) {
case "k8s":
kubeconf, err := setupK8S(cli.String("master"), cli.String("agents"))
dir, err := fxConfig.Dir()
if err != nil {
return err
}
kubeconf, err := setupK8S(dir, name, cli.String("master"), cli.String("agents"))
if err != nil {
return err
}

View File

@@ -2,8 +2,11 @@ package docker
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/types"
@@ -68,18 +71,18 @@ func Load(meta []byte) (*Cloud, error) {
// Provision a host
func (c *Cloud) Provision() error {
if err := c.sshClient.RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{}); err != nil {
if err := c.sshClient.RunCommand(infra.Scripts["install_docker"].(string), ssh.CommandOptions{}); err != nil {
if err := c.runCmd(infra.Scripts["docker_version"].(string)); err != nil {
if err := c.runCmd(infra.Scripts["install_docker"].(string)); err != nil {
return err
}
if err := c.sshClient.RunCommand(infra.Scripts["start_dockerd"].(string), ssh.CommandOptions{}); err != nil {
if err := c.runCmd(infra.Scripts["start_dockerd"].(string)); err != nil {
return err
}
}
if err := c.sshClient.RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}); err != nil {
if err := c.sshClient.RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}); err != nil {
if err := c.runCmd(infra.Scripts["check_fx_agent"].(string)); err != nil {
if err := c.runCmd(infra.Scripts["start_fx_agent"].(string)); err != nil {
return err
}
}
@@ -103,11 +106,48 @@ func (c *Cloud) Dump() ([]byte, error) {
return json.Marshal(c)
}
// IsHealth check if cloud is in health
func (c *Cloud) IsHealth() (bool, error) {
if err := c.runCmd(infra.Scripts["check_fx_agent"].(string)); err != nil {
if err := c.runCmd(infra.Scripts["start_fx_agent"].(string)); err != nil {
return false, err
}
}
return true, nil
}
// NOTE only using for unit testing
func (c *Cloud) setsshClient(client ssh.Clienter) {
c.sshClient = client
}
// nolint:unparam
func (c *Cloud) runCmd(script string, options ...ssh.CommandOptions) error {
option := ssh.CommandOptions{}
if len(options) >= 1 {
option = options[0]
}
local := c.IP == "127.0.0.1" || c.IP == "localhost"
if local && os.Getenv("CI") == "" {
params := strings.Split(script, " ")
if len(params) == 0 {
return fmt.Errorf("invalid script: %s", script)
}
// nolint
cmd := exec.Command(params[0], params[1:]...)
cmd.Stdout = option.Stdout
cmd.Stderr = option.Stderr
err := cmd.Run()
if err != nil {
return err
}
return nil
}
return c.sshClient.RunCommand(script, option)
}
// NOTE the reason putting sshkey() and sshport here inside node.go is because
// ssh key and ssh port is related to node it self, we may extend this in future
func sshkey() (string, error) {

View File

@@ -12,7 +12,7 @@ import (
"github.com/mitchellh/go-homedir"
)
func TestCloud(t *testing.T) {
func TestCloudProvision(t *testing.T) {
t.Run("fx agent started", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -60,6 +60,61 @@ func TestCloud(t *testing.T) {
})
}
func TestCloudIsHealth(t *testing.T) {
t.Run("agent started", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cloud := New("127.0.0.1", "fx", "master")
sshClient := sshMocks.NewMockClienter(ctrl)
cloud.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
ok, err := cloud.IsHealth()
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatalf("cloud should be healthy")
}
})
t.Run("agent not started, and retart ok", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cloud := New("127.0.0.1", "fx", "master")
sshClient := sshMocks.NewMockClienter(ctrl)
cloud.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("fx agent not started"))
sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
ok, err := cloud.IsHealth()
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatalf("cloud should be healthy")
}
})
t.Run("agent not started, but restart failed", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cloud := New("127.0.0.1", "fx", "master")
sshClient := sshMocks.NewMockClienter(ctrl)
cloud.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("fx agent not started"))
sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("fx agent started failed"))
ok, err := cloud.IsHealth()
if err == nil {
t.Fatal("should got failed starting")
}
if ok {
t.Fatalf("cloud should not be healthy")
}
})
}
func TestGetSSHKeyFile(t *testing.T) {
t.Run("defaut", func(t *testing.T) {
defau, err := sshkey()

View File

@@ -12,6 +12,7 @@ type Clouder interface {
GetConfig() (string, error)
GetType() string
Dump() ([]byte, error)
IsHealth() (bool, error)
}
// Deployer deploy interface

View File

@@ -3,18 +3,22 @@ package k8s
import (
"encoding/json"
"fmt"
"io/ioutil"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils"
)
// Cloud define a cloud
type Cloud struct {
Config string `json:"config"`
URL string `json:"url"`
Token string `json:"token"`
Type string `json:"type"`
Nodes map[string]Noder `json:"nodes"`
// Define where is the location of kubeconf would be saved to
KubeConfig string `json:"config"`
Type string `json:"type"`
Nodes map[string]Noder `json:"nodes"`
token string
url string
}
// Load a cloud from config
@@ -39,14 +43,16 @@ func Load(meta []byte, messup ...func(n Noder) (Noder, error)) (*Cloud, error) {
}
// NewCloud new a cloud
func NewCloud(node ...Noder) *Cloud {
func NewCloud(kubeconf string, node ...Noder) *Cloud {
nodes := map[string]Noder{}
for _, n := range node {
nodes[n.GetName()] = n
}
return &Cloud{
Type: types.CloudTypeK8S,
Nodes: nodes,
KubeConfig: kubeconf,
Type: types.CloudTypeK8S,
Nodes: nodes,
}
}
@@ -64,7 +70,7 @@ func (c *Cloud) Provision() error {
// when it's k3s cluster
if master != nil {
c.URL = fmt.Sprintf("https://%s:6443", master.GetIP())
c.url = fmt.Sprintf("https://%s:6443", master.GetIP())
if err := master.Provision(map[string]string{}); err != nil {
return err
}
@@ -73,22 +79,19 @@ func (c *Cloud) Provision() error {
if err != nil {
return err
}
c.Token = tok
c.token = tok
config, err := master.GetConfig()
if err != nil {
return err
}
c.Config = config
}
// when it's a docker agent
if len(agents) == 1 && agents[0].GetType() == NodeTypeDocker {
config, err := agents[0].GetConfig()
if err != nil {
if err := utils.EnsureFile(c.KubeConfig); err != nil {
return err
}
if err := ioutil.WriteFile(c.KubeConfig, []byte(config), 0666); err != nil {
return err
}
c.Config = config
}
if len(agents) > 0 {
@@ -98,8 +101,8 @@ func (c *Cloud) Provision() error {
for _, agent := range agents {
go func(node Noder) {
errCh <- node.Provision(map[string]string{
"url": c.URL,
"token": c.Token,
"url": c.url,
"token": c.token,
})
}(agent)
}
@@ -118,8 +121,8 @@ func (c *Cloud) Provision() error {
func (c *Cloud) AddNode(n Noder, skipProvision bool) error {
if !skipProvision {
if err := n.Provision(map[string]string{
"url": c.URL,
"token": c.Token,
"url": c.url,
"token": c.token,
}); err != nil {
return err
}
@@ -173,16 +176,16 @@ func (c *Cloud) UnmarshalJSON(data []byte) error {
} else if k == "token" {
tok, ok := v.(string)
if ok {
c.Token = tok
c.token = tok
} else {
c.Token = ""
c.token = ""
}
} else if k == "config" {
config, ok := v.(string)
if ok {
c.Config = config
c.KubeConfig = config
} else {
c.Config = ""
c.KubeConfig = ""
}
} else if k == "type" {
typ, ok := v.(string)
@@ -194,9 +197,9 @@ func (c *Cloud) UnmarshalJSON(data []byte) error {
} else if k == "url" {
url, ok := v.(string)
if ok {
c.URL = url
c.url = url
} else {
c.URL = ""
c.url = ""
}
}
}
@@ -217,17 +220,17 @@ func (c *Cloud) MarshalJSON() ([]byte, error) {
}
body, err := json.Marshal(struct {
URL string `json:"url"`
Config string `json:"config"`
Type string `json:"type"`
Token string `json:"token"`
Nodes map[string]Node `json:"nodes"`
URL string `json:"url"`
KubeConfig string `json:"config"`
Type string `json:"type"`
Token string `json:"token"`
Nodes map[string]Node `json:"nodes"`
}{
URL: c.URL,
Config: c.Config,
Type: c.Type,
Token: c.Token,
Nodes: nodes,
KubeConfig: c.KubeConfig,
Type: c.Type,
Token: c.token,
URL: c.url,
Nodes: nodes,
})
if err != nil {
return nil, err
@@ -248,13 +251,18 @@ func (c *Cloud) Dump() ([]byte, error) {
// GetConfig get config
func (c *Cloud) GetConfig() (string, error) {
if c.Config != "" {
return c.Config, nil
if c.KubeConfig != "" {
return c.KubeConfig, nil
}
if err := c.Provision(); err != nil {
return "", err
}
return c.Config, nil
return c.KubeConfig, nil
}
// IsHealth check if cloud is in health
func (c *Cloud) IsHealth() (bool, error) {
return true, nil
}
var (

View File

@@ -3,6 +3,8 @@ package k8s
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/golang/mock/gomock"
@@ -25,6 +27,13 @@ func TestLoad(t *testing.T) {
})
t.Run("only master node", func(t *testing.T) {
kubeconfig := "./kubeconfig.yml"
defer func() {
if err := os.RemoveAll("./kubeconfig.yml"); err != nil {
t.Fatal(err)
}
}()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -36,18 +45,19 @@ func TestLoad(t *testing.T) {
name := "master"
ip := "127.0.0.1"
user := "testuser"
kubeconfContent := "sample-content"
master.EXPECT().GetName().Return(name)
master.EXPECT().GetType().Return(typ).Times(2)
master.EXPECT().GetIP().Return(ip).Times(2)
master.EXPECT().GetUser().Return(user)
master.EXPECT().GetConfig().Return("sample-config", nil)
master.EXPECT().GetConfig().Return(kubeconfContent, nil)
claud := &Cloud{
Config: "",
URL: "",
Token: "",
Type: "k8s",
Nodes: map[string]Noder{"master-node": master},
KubeConfig: kubeconfig,
Type: "k8s",
url: "",
token: "",
Nodes: map[string]Noder{"master-node": master},
}
meta, err := json.Marshal(claud)
@@ -67,9 +77,24 @@ func TestLoad(t *testing.T) {
if err := cloud.Provision(); err != nil {
t.Fatal(err)
}
content, err := ioutil.ReadFile(claud.KubeConfig)
if err != nil {
t.Fatal(err)
}
if string(content) != kubeconfContent {
t.Fatalf("should get %s but got %s", kubeconfContent, content)
}
})
t.Run("one master node and one agent", func(t *testing.T) {
kubeconfig := "./kubeconfig.yml"
defer func() {
if err := os.RemoveAll("./kubeconfig.yml"); err != nil {
t.Fatal(err)
}
}()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -85,10 +110,11 @@ func TestLoad(t *testing.T) {
name := "master"
ip := "127.0.0.1"
user := "testuser"
kubeconfContent := "sample-config"
master.EXPECT().GetName().Return(name)
master.EXPECT().GetType().Return(typ).Times(2)
master.EXPECT().GetIP().Return(ip).Times(3)
master.EXPECT().GetConfig().Return("sample-config", nil)
master.EXPECT().GetConfig().Return(kubeconfContent, nil)
master.EXPECT().GetUser().Return(user)
nodeType := NodeTypeAgent
@@ -96,18 +122,18 @@ func TestLoad(t *testing.T) {
nodeIP := "12.12.12.12"
nodeUser := "testuser"
node.EXPECT().GetName().Return(nodeName)
node.EXPECT().GetType().Return(nodeType).Times(3)
node.EXPECT().GetType().Return(nodeType).Times(2)
node.EXPECT().GetIP().Return(nodeIP)
node.EXPECT().GetUser().Return(nodeUser)
url := fmt.Sprintf("https://%s:6443", master.GetIP())
tok := "tok-1"
claud := &Cloud{
Config: "",
URL: url,
Token: tok,
Type: "k8s",
Nodes: map[string]Noder{"master-node": master, "agent-node": node},
KubeConfig: kubeconfig,
url: url,
token: tok,
Type: "k8s",
Nodes: map[string]Noder{"master-node": master, "agent-node": node},
}
meta, err := json.Marshal(claud)
if err != nil {
@@ -125,12 +151,19 @@ func TestLoad(t *testing.T) {
master.EXPECT().Provision(map[string]string{}).Return(nil)
master.EXPECT().GetToken().Return(tok, nil)
node.EXPECT().Provision(map[string]string{
"url": cloud.URL,
"token": cloud.Token,
"url": cloud.url,
"token": cloud.token,
}).Return(nil)
if err := cloud.Provision(); err != nil {
t.Fatal(err)
}
content, err := ioutil.ReadFile(claud.KubeConfig)
if err != nil {
t.Fatal(err)
}
if string(content) != kubeconfContent {
t.Fatalf("should get %s but got %s", kubeconfContent, content)
}
})
}

View File

@@ -39,6 +39,16 @@ func Provision(ctx context.Contexter) (err error) {
if err != nil {
return err
}
ok, err := cloud.IsHealth()
if err != nil {
return err
}
if !ok {
return fmt.Errorf("infrastrure is not health, please try to run create infrastructure use 'fx infra create ...' command")
}
ctx.Set("cloud", cloud)
conf, err := cloud.GetConfig()
@@ -46,13 +56,7 @@ func Provision(ctx context.Contexter) (err error) {
return err
}
var deployer infra.Deployer
if os.Getenv("KUBECONFIG") != "" {
deployer, err = k8sInfra.CreateDeployer(os.Getenv("KUBECONFIG"))
if err != nil {
return err
}
ctx.Set("cloud_type", types.CloudTypeK8S)
} else if cloud.GetType() == types.CloudTypeDocker {
if cloud.GetType() == types.CloudTypeDocker {
var meta map[string]string
if err := json.Unmarshal([]byte(conf), &meta); err != nil {
return err
@@ -69,11 +73,12 @@ func Provision(ctx context.Contexter) (err error) {
return err
}
} else if cloud.GetType() == types.CloudTypeK8S {
kubeconfig, err := fxConfig.GetKubeConfig()
if err != nil {
return err
ctx.Set("cloud_type", types.CloudTypeK8S)
if os.Getenv("KUBECONFIG") != "" {
conf = os.Getenv("KUBECONFIG")
}
deployer, err = k8sInfra.CreateDeployer(kubeconfig)
deployer, err = k8sInfra.CreateDeployer(conf)
if err != nil {
return err
}