From c44a6a29708e6d1241943e97fae8cc4750389c2f Mon Sep 17 00:00:00 2001 From: ilzheev Date: Sat, 22 Aug 2020 10:05:24 +0400 Subject: [PATCH 01/19] configuration --- config/config.go | 36 ++++++++++++++++++++++++++++++++++++ main.go | 1 + 2 files changed, 37 insertions(+) create mode 100644 config/config.go create mode 100644 main.go diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..1bb51fb --- /dev/null +++ b/config/config.go @@ -0,0 +1,36 @@ +package config + +import ( + "io/ioutil" + + "github.com/go-yaml/yaml" + "github.com/jinzhu/configor" + "github.com/mcuadros/go-defaults" +) + +// App config struct +type Config struct { + Endpoint string `default:"" json:"endpoint" form:"endpoint" query:"endpoint"` + Username string `default:"" json:"username" form:"username" query:"username"` + Password string `default:"" json:"password" form:"password" query:"password"` +} + +// Create config from configFile +func NewConfig(configFile string) (*Config, error) { + + config := new(Config) + defaults.SetDefaults(config) + + configBytes, err := ioutil.ReadFile(configFile) + if err == nil { + err = yaml.Unmarshal(configBytes, &config) + if err != nil { + return nil, err + } + } + + if err := configor.Load(config); err != nil { + return nil, err + } + return config, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/main.go @@ -0,0 +1 @@ +package main From acb238d38291eeceb8fc6f5192e49179777a3336 Mon Sep 17 00:00:00 2001 From: ilzheev Date: Sat, 22 Aug 2020 10:31:14 +0400 Subject: [PATCH 02/19] example config --- config.example.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 config.example.yaml diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..9a0213a --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,3 @@ +endpoint: https://test.factom.com +username: test +password: test \ No newline at end of file From f262ba9e70b51387478141998dfd1c6790fd38fc Mon Sep 17 00:00:00 2001 From: ilzheev Date: Sat, 22 Aug 2020 10:35:04 +0400 Subject: [PATCH 03/19] config required params --- config/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index 1bb51fb..cfa5963 100644 --- a/config/config.go +++ b/config/config.go @@ -10,9 +10,9 @@ import ( // App config struct type Config struct { - Endpoint string `default:"" json:"endpoint" form:"endpoint" query:"endpoint"` - Username string `default:"" json:"username" form:"username" query:"username"` - Password string `default:"" json:"password" form:"password" query:"password"` + Endpoint string `default:"" json:"endpoint" form:"endpoint" query:"endpoint" required:"true"` + Username string `default:"" json:"username" form:"username" query:"username" required:"true"` + Password string `default:"" json:"password" form:"password" query:"password" required:"true"` } // Create config from configFile From cbe70c005b4a66d6b2f2374aac311020d9aed338 Mon Sep 17 00:00:00 2001 From: ilzheev Date: Sat, 12 Sep 2020 10:34:29 +0400 Subject: [PATCH 04/19] alpha --- go.mod | 11 +++ go.sum | 26 ++++++ main.go | 135 +++++++++++++++++++++++++++ portainer/portainer.go | 177 ++++++++++++++++++++++++++++++++++++ portainer/portainer_test.go | 69 ++++++++++++++ 5 files changed, 418 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 portainer/portainer.go create mode 100644 portainer/portainer_test.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4445e33 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/DeFacto-Team/factom-network-restart + +go 1.14 + +require ( + github.com/go-yaml/yaml v2.1.0+incompatible + github.com/jinzhu/configor v1.2.0 + github.com/mcuadros/go-defaults v1.2.0 + github.com/sirupsen/logrus v1.6.0 + github.com/stretchr/testify v1.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b90e0ae --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/jinzhu/configor v1.2.0 h1:u78Jsrxw2+3sGbGMgpY64ObKU4xWCNmNRJIjGVqxYQA= +github.com/jinzhu/configor v1.2.0/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc= +github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index 06ab7d0..d21da1c 100644 --- a/main.go +++ b/main.go @@ -1 +1,136 @@ package main + +import ( + "flag" + "fmt" + "os/user" + "strings" + "sync" + + "github.com/DeFacto-Team/factom-network-restart/config" + "github.com/DeFacto-Team/factom-network-restart/portainer" + log "github.com/sirupsen/logrus" +) + +func main() { + + var err error + var live bool + var conf *config.Config + + log.SetLevel(5) + + usr, err := user.Current() + if err != nil { + log.Error(err) + } + + // default config location + configFile := usr.HomeDir + "/.factom-restart/config.yaml" + + // check if custom config location passed as flag + flag.StringVar(&configFile, "c", configFile, "config.yaml path") + + // parse dry run flag + flag.BoolVar(&live, "live", live, "live bool") + + flag.Parse() + + log.Info("Using config: ", configFile) + + // load config + if conf, err = config.NewConfig(configFile); err != nil { + log.Fatal(err) + } + + log.Info("Starting restart system") + log.Info("Portainer endpoint: ", conf.Endpoint) + + if live { + log.Warn("LIVE mode: network will be restarted!") + } else { + log.Info("DRY RUN mode: simulating restart") + } + + // initialize portainer + p := portainer.NewPortainer(conf.Username, conf.Password, conf.Endpoint) + + // get all endpoints from portainer + endpoints, err := p.GetSwarmEndpoints() + if err != nil { + log.Fatal(err) + } + + // get factomd containers for each endpoint + for i := range endpoints { + if endpoints[i].PublicURL != "" { + fmt.Printf("------------------------------\n") + log.Debug("Trying connecting to ", endpoints[i].Name, " (", endpoints[i].PublicURL, "), ID=", endpoints[i].ID) + endpoints[i].Containers, err = p.GetDockerContainers(endpoints[i].ID) + if err != nil { + log.Error(err) + } + for j := range endpoints[i].Containers { + version := strings.Split(endpoints[i].Containers[j].Image, ":") + if endpoints[i].Containers[j].State == "running" { + log.Debug("factomd container is ", endpoints[i].Containers[j].State, "\n", endpoints[i].Containers[j].ID, "\n", version[1]) + } else { + log.Warn("factomd container is ", endpoints[i].Containers[j].State, "\n", endpoints[i].Containers[j].ID, "\n", version[1]) + } + } + } + } + + // RESTART START + fmt.Printf("------------------------------\n") + if live { + log.Info("RESTARTING NOW") + } else { + log.Info("SIMULATING RESTART (DRY-RUN)") + } + + var wg sync.WaitGroup + + // Restart each factomd container + for i := range endpoints { + if endpoints[i].PublicURL != "" { + + if len(endpoints[i].Containers) > 0 { + // for all online hosts with containers, restart factomd container(s) + for j := range endpoints[i].Containers { + if live { + // if live mode, async restarting + wg.Add(1) + go restart(&wg, p, endpoints[i].Name, endpoints[i].ID, endpoints[i].Containers[j].ID) + } else { + // if dry-run mode, just print endpoint and containerId + log.Info("OK ", endpoints[i].Name, " (", endpoints[i].Containers[j].ID, ")") + } + } + } else { + // log skipped endpoints with no containers + log.Warn("SKIP ", endpoints[i].Name) + } + + } + } + + wg.Wait() + +} + +func restart(wg *sync.WaitGroup, p portainer.Portainer, name string, endpointID int, containerID string) { + + err := p.RestartDockerContainer(endpointID, containerID) + if err != nil { + // if restart request failed, print error + log.Error("ERROR ", name, " (", containerID, ")") + log.Error(err) + } else { + // no error = success restart, print OK + log.Info("OK ", name, " (", containerID, ")") + } + + defer wg.Done() + +} diff --git a/portainer/portainer.go b/portainer/portainer.go new file mode 100644 index 0000000..df5b67b --- /dev/null +++ b/portainer/portainer.go @@ -0,0 +1,177 @@ +package portainer + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "time" + + log "github.com/sirupsen/logrus" +) + +// HTTP request timeout +const Timeout = 5 * time.Second + +type Context struct { + Endpoint string + Token string +} + +// AuthRequest is authorization form struct for Portainer API +type AuthRequest struct { + Username string + Password string +} + +// AuthResponse is response format for authorization in Portainer API +type AuthResponse struct { + JWT string + Err string +} + +// SwarmEndpoint is response format for GET /endpoints call in Portainer API +type SwarmEndpoint struct { + ID int + Name string + PublicURL string + Containers []DockerContainer +} + +// DockerContainer is response format for GET /endpoints/{endpointId}/docker/containers call in Portainer API +type DockerContainer struct { + ID string + Image string + State string +} + +type Portainer interface { + GetSwarmEndpoints() ([]SwarmEndpoint, error) + GetDockerContainers(endpointID int) ([]DockerContainer, error) + RestartDockerContainer(endpointID int, containerID string) error + GetToken() string +} + +// NewPortainer initializes connection to Portainer API and obtain JWT access token +func NewPortainer(username string, password string, endpoint string) Portainer { + + url := endpoint + "/api/auth" + + login := AuthRequest{Username: username, Password: password} + data, err := json.Marshal(login) + + if err != nil { + log.Fatal(err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: Timeout} + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + token := &AuthResponse{} + + err = json.Unmarshal(body, token) + if err != nil { + log.Fatal(err) + } + if token.Err != "" { + log.Fatal(token.Err) + } + + log.Info("Successfully logged in as " + username) + + return &Context{Token: token.JWT, Endpoint: endpoint} +} + +// GetSwarmEndpoints makes request to Portainer API and returns []SwarmEndpoint +func (c *Context) GetSwarmEndpoints() ([]SwarmEndpoint, error) { + + var endpoints []SwarmEndpoint + + resp, err := c.makeRequest("GET", "/api/endpoints", nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(resp, &endpoints) + if err != nil { + return nil, err + } + + return endpoints, nil + +} + +// GetDockerContainers makes request to Portainer API and returns []DockerContainer with label "name=factomd" for requested endpointId +func (c *Context) GetDockerContainers(endpointID int) ([]DockerContainer, error) { + + var containers []DockerContainer + + resp, err := c.makeRequest("GET", "/api/endpoints/"+strconv.Itoa(endpointID)+"/docker/containers/json?all=1&filters={\"label\":[\"name=factomd\"]}", nil) + if err != nil || len(resp) == 0 { + return nil, fmt.Errorf("Can not connect to remote host") + } + + err = json.Unmarshal(resp, &containers) + if err != nil { + return nil, err + } + + if len(containers) == 0 { + return nil, fmt.Errorf("No containers with label 'name=factomd'") + } + + return containers, nil + +} + +// RestartDockerContainer makes restart request to Portainer API for requested endpointId and containerId +func (c *Context) RestartDockerContainer(endpointID int, containerID string) error { + + // nothing returned = success + _, err := c.makeRequest("POST", "/api/endpoints/"+strconv.Itoa(endpointID)+"/docker/containers/"+containerID+"/restart?t=5", nil) + if err != nil { + return err + } + + return nil + +} + +// Low level function making requests to Portainer API +func (c *Context) makeRequest(method string, path string, data []byte) ([]byte, error) { + + url := c.Endpoint + path + + req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.Token) + + client := &http.Client{Timeout: Timeout} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ioutil.ReadAll(resp.Body) + +} + +// Token() returns context token +func (c *Context) GetToken() string { + return c.Token +} diff --git a/portainer/portainer_test.go b/portainer/portainer_test.go new file mode 100644 index 0000000..c5a0685 --- /dev/null +++ b/portainer/portainer_test.go @@ -0,0 +1,69 @@ +package portainer + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewPortainer(t *testing.T) { + + assert := assert.New(t) + + // expected success response + success := &AuthResponse{JWT: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImNlN2EyY2U0LWMxYWYtNGYyMy1iNjM4LTczNjUyMDQ3ZDQyNSIsImlhdCI6MTU5OTg5MDc4MiwiZXhwIjoxNTk5ODk0MzgyfQ.kdFpPMkUYlTJa_K4h2bWTmCZYfLW9LTqXhlCZulcPxc"} + resp, _ := json.Marshal(success) + + // start test server + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write(resp) + })) + + defer srv.Close() + + // start portainer client + p := NewPortainer("test", "test", srv.URL) + + assert.Equal(p.GetToken(), success.JWT) + +} + +func TestGetSwarmEndpoints(t *testing.T) { + + assert := assert.New(t) + + // expected success response + success := []SwarmEndpoint{{ID: 1, Name: "Test1", PublicURL: "1.1.1.1"}, {ID: 2, Name: "Test2", PublicURL: "2.2.2.2"}} + resp, _ := json.Marshal(success) + + // start test auth server + authResponse := &AuthResponse{JWT: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImNlN2EyY2U0LWMxYWYtNGYyMy1iNjM4LTczNjUyMDQ3ZDQyNSIsImlhdCI6MTU5OTg5MDc4MiwiZXhwIjoxNTk5ODk0MzgyfQ.kdFpPMkUYlTJa_K4h2bWTmCZYfLW9LTqXhlCZulcPxc"} + authResponseByte, _ := json.Marshal(authResponse) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write(authResponseByte) + })) + + defer srv.Close() + + // start portainer client + p := NewPortainer("test", "test", srv.URL) + + // update test server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write(resp) + })) + + defer srv.Close() + + endpoints, err := p.GetSwarmEndpoints() + + assert.Equal(err, assert.Nil) + assert.Equal(endpoints, success) + +} From dba2a254757cd4dd5c4d35bc9febd4f82d64b475 Mon Sep 17 00:00:00 2001 From: ilzheev Date: Sat, 12 Sep 2020 12:28:27 +0400 Subject: [PATCH 05/19] portainer tests --- portainer/portainer.go | 6 ++- portainer/portainer_test.go | 90 +++++++++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/portainer/portainer.go b/portainer/portainer.go index df5b67b..fa26968 100644 --- a/portainer/portainer.go +++ b/portainer/portainer.go @@ -85,11 +85,15 @@ func NewPortainer(username string, password string, endpoint string) Portainer { err = json.Unmarshal(body, token) if err != nil { - log.Fatal(err) + log.Error(err) } + if token.Err != "" { log.Fatal(token.Err) } + if token.JWT == "" { + log.Fatal("Portainer server doesn't return JWT token") + } log.Info("Successfully logged in as " + username) diff --git a/portainer/portainer_test.go b/portainer/portainer_test.go index c5a0685..d42b4b4 100644 --- a/portainer/portainer_test.go +++ b/portainer/portainer_test.go @@ -13,7 +13,7 @@ func TestNewPortainer(t *testing.T) { assert := assert.New(t) - // expected success response + // expected response success := &AuthResponse{JWT: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImNlN2EyY2U0LWMxYWYtNGYyMy1iNjM4LTczNjUyMDQ3ZDQyNSIsImlhdCI6MTU5OTg5MDc4MiwiZXhwIjoxNTk5ODk0MzgyfQ.kdFpPMkUYlTJa_K4h2bWTmCZYfLW9LTqXhlCZulcPxc"} resp, _ := json.Marshal(success) @@ -34,18 +34,24 @@ func TestNewPortainer(t *testing.T) { func TestGetSwarmEndpoints(t *testing.T) { + var resp []byte + assert := assert.New(t) - // expected success response - success := []SwarmEndpoint{{ID: 1, Name: "Test1", PublicURL: "1.1.1.1"}, {ID: 2, Name: "Test2", PublicURL: "2.2.2.2"}} - resp, _ := json.Marshal(success) + // expected responses + endpointsResp := []SwarmEndpoint{{ID: 1, Name: "Test1", PublicURL: "1.1.1.1"}, {ID: 2, Name: "Test2", PublicURL: "2.2.2.2"}} + authResp := &AuthResponse{JWT: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImNlN2EyY2U0LWMxYWYtNGYyMy1iNjM4LTczNjUyMDQ3ZDQyNSIsImlhdCI6MTU5OTg5MDc4MiwiZXhwIjoxNTk5ODk0MzgyfQ.kdFpPMkUYlTJa_K4h2bWTmCZYfLW9LTqXhlCZulcPxc"} - // start test auth server - authResponse := &AuthResponse{JWT: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImNlN2EyY2U0LWMxYWYtNGYyMy1iNjM4LTczNjUyMDQ3ZDQyNSIsImlhdCI6MTU5OTg5MDc4MiwiZXhwIjoxNTk5ODk0MzgyfQ.kdFpPMkUYlTJa_K4h2bWTmCZYfLW9LTqXhlCZulcPxc"} - authResponseByte, _ := json.Marshal(authResponse) + // setup test server srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/api/endpoints": + resp, _ = json.Marshal(endpointsResp) + case "/api/auth": + resp, _ = json.Marshal(authResp) + } w.WriteHeader(200) - w.Write(authResponseByte) + w.Write(resp) })) defer srv.Close() @@ -53,17 +59,75 @@ func TestGetSwarmEndpoints(t *testing.T) { // start portainer client p := NewPortainer("test", "test", srv.URL) - // update test server - srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + endpoints, err := p.GetSwarmEndpoints() + + assert.Equal(err, nil) + assert.Equal(endpoints, endpointsResp) + +} + +func TestGetDockerContainers(t *testing.T) { + + var resp []byte + + assert := assert.New(t) + + // expected responses + containersResp := []DockerContainer{{ID: "c55e61d8a89b363c450bef7163cc46ea44479ed2245c2c3fde5e3c818e7dd0ef", Image: "v6.6.0", State: "running"}} + authResp := &AuthResponse{JWT: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImNlN2EyY2U0LWMxYWYtNGYyMy1iNjM4LTczNjUyMDQ3ZDQyNSIsImlhdCI6MTU5OTg5MDc4MiwiZXhwIjoxNTk5ODk0MzgyfQ.kdFpPMkUYlTJa_K4h2bWTmCZYfLW9LTqXhlCZulcPxc"} + + // setup test server + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/api/endpoints/1/docker/containers/json?all=1&filters={\"label\":[\"name=factomd\"]}": + resp, _ = json.Marshal(containersResp) + case "/api/auth": + resp, _ = json.Marshal(authResp) + } w.WriteHeader(200) w.Write(resp) })) defer srv.Close() - endpoints, err := p.GetSwarmEndpoints() + // start portainer client + p := NewPortainer("test", "test", srv.URL) + + containers, err := p.GetDockerContainers(1) + + assert.Equal(err, nil) + assert.Equal(containers, containersResp) + +} + +func TestRestartDockerContainer(t *testing.T) { + + var resp []byte + + assert := assert.New(t) + + // expected responses + authResp := &AuthResponse{JWT: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImNlN2EyY2U0LWMxYWYtNGYyMy1iNjM4LTczNjUyMDQ3ZDQyNSIsImlhdCI6MTU5OTg5MDc4MiwiZXhwIjoxNTk5ODk0MzgyfQ.kdFpPMkUYlTJa_K4h2bWTmCZYfLW9LTqXhlCZulcPxc"} + + // setup test server + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/api/endpoints/1/docker/containers/c55e61d8a89b363c450bef7163cc46ea44479ed2245c2c3fde5e3c818e7dd0ef/restart?t=5": + resp = make([]byte, 0) + case "/api/auth": + resp, _ = json.Marshal(authResp) + } + w.WriteHeader(200) + w.Write(resp) + })) + + defer srv.Close() + + // start portainer client + p := NewPortainer("test", "test", srv.URL) + + err := p.RestartDockerContainer(1, "c55e61d8a89b363c450bef7163cc46ea44479ed2245c2c3fde5e3c818e7dd0ef") - assert.Equal(err, assert.Nil) - assert.Equal(endpoints, success) + assert.Equal(err, nil) } From 33c41b56040ad6817e6f7d76751abd48eadc2730 Mon Sep 17 00:00:00 2001 From: Anton Ilzheev Date: Sat, 12 Sep 2020 12:40:02 +0400 Subject: [PATCH 06/19] Update README.md --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a6f3391..1dc53c2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,62 @@ # Factom Network Restart System -Restart system for Factom network +The app connects to remote Portainer server via Portainer API and asynchronously restarts all containers with label `name=factomd`. + +## Configuration +Config file with Portainer credentials should be placed in `~/.factom-restart/config.yaml` or provided as a flag `-c /path/to/config.yaml` while running the application. + +## Usage +### Dry-run +Run in dry-run mode (scans Portainer endpoints and containers, no restart happens): +```bash +go run main.go +``` +Output: +```bash +INFO[0000] Using config: /Users/anton/.factom-restart/config.yaml +INFO[0000] Starting restart system +INFO[0000] Portainer endpoint: https://test.com +INFO[0000] DRY RUN mode: simulating restart +INFO[0001] Successfully logged in as anton +------------------------------ +DEBU[0001] Trying connecting to X (xxx.xxx.xxx.xxx), ID=2 +ERRO[0001] Can not connect to remote host +------------------------------ +DEBU[0003] Trying connecting to Y (yyy.yyy.yyy.yyy), ID=29 +DEBU[0003] factomd container is running +307f871a97e1dd91851c6298cf3a183f1709d908810b0059ce9094622bb78126 +v6.6.0-alpine +… +------------------------------ +INFO[0033] SIMULATING RESTART (DRY-RUN) +WARN[0033] SKIP X +INFO[0033] OK Y (307f871a97e1dd91851c6298cf3a183f1709d908810b0059ce9094622bb78126) +… +``` + +### Live mode +Run in live mode (restarts the network): +```bash +go run main.go --live +``` +Output: +```bash +INFO[0000] Using config: /Users/anton/.factom-restart/config.yaml +INFO[0000] Starting restart system +INFO[0000] Portainer endpoint: https://test.com +WARN[0000] LIVE mode: network will be restarted! +INFO[0001] Successfully logged in as anton +------------------------------ +DEBU[0001] Trying connecting to X (xxx.xxx.xxx.xxx), ID=2 +ERRO[0001] Can not connect to remote host +------------------------------ +DEBU[0003] Trying connecting to Y (yyy.yyy.yyy.yyy), ID=29 +DEBU[0003] factomd container is running +307f871a97e1dd91851c6298cf3a183f1709d908810b0059ce9094622bb78126 +v6.6.0-alpine +… +------------------------------ +INFO[0033] RESTARTING NOW +WARN[0033] SKIP X +INFO[0033] OK Y (307f871a97e1dd91851c6298cf3a183f1709d908810b0059ce9094622bb78126) +… +``` From 2af2864e0e9340aa2011784db1f3d08bb177e85c Mon Sep 17 00:00:00 2001 From: ilzheev Date: Mon, 14 Sep 2020 15:01:52 +0400 Subject: [PATCH 07/19] =?UTF-8?q?fix=20=E2=80=94live=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index d21da1c..9f2b3ff 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ func main() { flag.StringVar(&configFile, "c", configFile, "config.yaml path") // parse dry run flag - flag.BoolVar(&live, "live", live, "live bool") + flag.BoolVar(&live, "live", false, "live bool") flag.Parse() From 89a8bb0864090344bdd9b4460a6bd9785c47f045 Mon Sep 17 00:00:00 2001 From: ilzheev Date: Mon, 14 Sep 2020 18:54:39 +0400 Subject: [PATCH 08/19] joining config and portainer into main package --- config/config.go => config.go | 2 +- main.go | 8 +++----- portainer/portainer.go => portainer.go | 2 +- portainer/portainer_test.go => portainer_test.go | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) rename config/config.go => config.go (98%) rename portainer/portainer.go => portainer.go (99%) rename portainer/portainer_test.go => portainer_test.go (99%) diff --git a/config/config.go b/config.go similarity index 98% rename from config/config.go rename to config.go index cfa5963..64f76d3 100644 --- a/config/config.go +++ b/config.go @@ -1,4 +1,4 @@ -package config +package main import ( "io/ioutil" diff --git a/main.go b/main.go index 9f2b3ff..538898f 100644 --- a/main.go +++ b/main.go @@ -7,8 +7,6 @@ import ( "strings" "sync" - "github.com/DeFacto-Team/factom-network-restart/config" - "github.com/DeFacto-Team/factom-network-restart/portainer" log "github.com/sirupsen/logrus" ) @@ -16,7 +14,7 @@ func main() { var err error var live bool - var conf *config.Config + var conf *Config log.SetLevel(5) @@ -39,7 +37,7 @@ func main() { log.Info("Using config: ", configFile) // load config - if conf, err = config.NewConfig(configFile); err != nil { + if conf, err = NewConfig(configFile); err != nil { log.Fatal(err) } @@ -53,7 +51,7 @@ func main() { } // initialize portainer - p := portainer.NewPortainer(conf.Username, conf.Password, conf.Endpoint) + p := NewPortainer(conf.Username, conf.Password, conf.Endpoint) // get all endpoints from portainer endpoints, err := p.GetSwarmEndpoints() diff --git a/portainer/portainer.go b/portainer.go similarity index 99% rename from portainer/portainer.go rename to portainer.go index fa26968..af35112 100644 --- a/portainer/portainer.go +++ b/portainer.go @@ -1,4 +1,4 @@ -package portainer +package main import ( "bytes" diff --git a/portainer/portainer_test.go b/portainer_test.go similarity index 99% rename from portainer/portainer_test.go rename to portainer_test.go index d42b4b4..f9bd8c3 100644 --- a/portainer/portainer_test.go +++ b/portainer_test.go @@ -1,4 +1,4 @@ -package portainer +package main import ( "encoding/json" From b551aac066893185f5b03fd515519229f2df0f2f Mon Sep 17 00:00:00 2001 From: ilzheev Date: Mon, 14 Sep 2020 18:56:43 +0400 Subject: [PATCH 09/19] typo fix --- portainer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portainer.go b/portainer.go index af35112..1160cb3 100644 --- a/portainer.go +++ b/portainer.go @@ -92,7 +92,7 @@ func NewPortainer(username string, password string, endpoint string) Portainer { log.Fatal(token.Err) } if token.JWT == "" { - log.Fatal("Portainer server doesn't return JWT token") + log.Fatal("Portainer server didn't return JWT token") } log.Info("Successfully logged in as " + username) From 94928b00b692f8bcc76bb193447507f9262c259c Mon Sep 17 00:00:00 2001 From: ilzheev Date: Tue, 15 Sep 2020 12:42:40 +0400 Subject: [PATCH 10/19] go func() and range refactoring --- main.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/main.go b/main.go index 538898f..2edf46f 100644 --- a/main.go +++ b/main.go @@ -90,24 +90,27 @@ func main() { var wg sync.WaitGroup // Restart each factomd container - for i := range endpoints { - if endpoints[i].PublicURL != "" { + for _, e := range endpoints { + if e.PublicURL != "" { - if len(endpoints[i].Containers) > 0 { + if len(e.Containers) > 0 { // for all online hosts with containers, restart factomd container(s) - for j := range endpoints[i].Containers { + for _, c := range e.Containers { if live { // if live mode, async restarting wg.Add(1) - go restart(&wg, p, endpoints[i].Name, endpoints[i].ID, endpoints[i].Containers[j].ID) + go func(name string, endpointID int, containerID string) { + restart(p, name, endpointID, containerID) + wg.Done() + }(e.Name, e.ID, c.ID) } else { // if dry-run mode, just print endpoint and containerId - log.Info("OK ", endpoints[i].Name, " (", endpoints[i].Containers[j].ID, ")") + log.Info("OK ", e.Name, " (", c.ID, ")") } } } else { // log skipped endpoints with no containers - log.Warn("SKIP ", endpoints[i].Name) + log.Warn("SKIP ", e.Name) } } @@ -117,7 +120,7 @@ func main() { } -func restart(wg *sync.WaitGroup, p portainer.Portainer, name string, endpointID int, containerID string) { +func restart(p Portainer, name string, endpointID int, containerID string) { err := p.RestartDockerContainer(endpointID, containerID) if err != nil { @@ -129,6 +132,4 @@ func restart(wg *sync.WaitGroup, p portainer.Portainer, name string, endpointID log.Info("OK ", name, " (", containerID, ")") } - defer wg.Done() - } From 5da956aa958077ff8b98e188bf3261d1b23be311 Mon Sep 17 00:00:00 2001 From: ilzheev Date: Tue, 15 Sep 2020 12:48:26 +0400 Subject: [PATCH 11/19] return http error --- portainer.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/portainer.go b/portainer.go index 1160cb3..cf0a810 100644 --- a/portainer.go +++ b/portainer.go @@ -125,8 +125,11 @@ func (c *Context) GetDockerContainers(endpointID int) ([]DockerContainer, error) var containers []DockerContainer resp, err := c.makeRequest("GET", "/api/endpoints/"+strconv.Itoa(endpointID)+"/docker/containers/json?all=1&filters={\"label\":[\"name=factomd\"]}", nil) - if err != nil || len(resp) == 0 { - return nil, fmt.Errorf("Can not connect to remote host") + if err != nil { + return nil, err + } + if len(resp) == 0 { + return nil, fmt.Errorf("Empty response received from Portainer API") } err = json.Unmarshal(resp, &containers) From 8c94fbb984f39e91edc4530b57182d11e7f5b4dd Mon Sep 17 00:00:00 2001 From: WhoSoup Date: Tue, 15 Sep 2020 10:57:02 +0200 Subject: [PATCH 12/19] remove interface --- main.go | 2 +- portainer.go | 37 +++++++++++++++---------------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/main.go b/main.go index 2edf46f..fe17f9c 100644 --- a/main.go +++ b/main.go @@ -120,7 +120,7 @@ func main() { } -func restart(p Portainer, name string, endpointID int, containerID string) { +func restart(p *Portainer, name string, endpointID int, containerID string) { err := p.RestartDockerContainer(endpointID, containerID) if err != nil { diff --git a/portainer.go b/portainer.go index cf0a810..59be4a1 100644 --- a/portainer.go +++ b/portainer.go @@ -15,7 +15,7 @@ import ( // HTTP request timeout const Timeout = 5 * time.Second -type Context struct { +type Portainer struct { Endpoint string Token string } @@ -47,15 +47,8 @@ type DockerContainer struct { State string } -type Portainer interface { - GetSwarmEndpoints() ([]SwarmEndpoint, error) - GetDockerContainers(endpointID int) ([]DockerContainer, error) - RestartDockerContainer(endpointID int, containerID string) error - GetToken() string -} - // NewPortainer initializes connection to Portainer API and obtain JWT access token -func NewPortainer(username string, password string, endpoint string) Portainer { +func NewPortainer(username string, password string, endpoint string) *Portainer { url := endpoint + "/api/auth" @@ -97,15 +90,15 @@ func NewPortainer(username string, password string, endpoint string) Portainer { log.Info("Successfully logged in as " + username) - return &Context{Token: token.JWT, Endpoint: endpoint} + return &Portainer{Token: token.JWT, Endpoint: endpoint} } // GetSwarmEndpoints makes request to Portainer API and returns []SwarmEndpoint -func (c *Context) GetSwarmEndpoints() ([]SwarmEndpoint, error) { +func (p *Portainer) GetSwarmEndpoints() ([]SwarmEndpoint, error) { var endpoints []SwarmEndpoint - resp, err := c.makeRequest("GET", "/api/endpoints", nil) + resp, err := p.makeRequest("GET", "/api/endpoints", nil) if err != nil { return nil, err } @@ -120,11 +113,11 @@ func (c *Context) GetSwarmEndpoints() ([]SwarmEndpoint, error) { } // GetDockerContainers makes request to Portainer API and returns []DockerContainer with label "name=factomd" for requested endpointId -func (c *Context) GetDockerContainers(endpointID int) ([]DockerContainer, error) { +func (p *Portainer) GetDockerContainers(endpointID int) ([]DockerContainer, error) { var containers []DockerContainer - resp, err := c.makeRequest("GET", "/api/endpoints/"+strconv.Itoa(endpointID)+"/docker/containers/json?all=1&filters={\"label\":[\"name=factomd\"]}", nil) + resp, err := p.makeRequest("GET", "/api/endpoints/"+strconv.Itoa(endpointID)+"/docker/containers/json?all=1&filters={\"label\":[\"name=factomd\"]}", nil) if err != nil { return nil, err } @@ -146,10 +139,10 @@ func (c *Context) GetDockerContainers(endpointID int) ([]DockerContainer, error) } // RestartDockerContainer makes restart request to Portainer API for requested endpointId and containerId -func (c *Context) RestartDockerContainer(endpointID int, containerID string) error { +func (p *Portainer) RestartDockerContainer(endpointID int, containerID string) error { // nothing returned = success - _, err := c.makeRequest("POST", "/api/endpoints/"+strconv.Itoa(endpointID)+"/docker/containers/"+containerID+"/restart?t=5", nil) + _, err := p.makeRequest("POST", "/api/endpoints/"+strconv.Itoa(endpointID)+"/docker/containers/"+containerID+"/restart?t=5", nil) if err != nil { return err } @@ -159,13 +152,13 @@ func (c *Context) RestartDockerContainer(endpointID int, containerID string) err } // Low level function making requests to Portainer API -func (c *Context) makeRequest(method string, path string, data []byte) ([]byte, error) { +func (p *Portainer) makeRequest(method string, path string, data []byte) ([]byte, error) { - url := c.Endpoint + path + url := p.Endpoint + path req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+c.Token) + req.Header.Set("Authorization", "Bearer "+p.Token) client := &http.Client{Timeout: Timeout} resp, err := client.Do(req) @@ -178,7 +171,7 @@ func (c *Context) makeRequest(method string, path string, data []byte) ([]byte, } -// Token() returns context token -func (c *Context) GetToken() string { - return c.Token +// Token() returns Portainer token +func (p *Portainer) GetToken() string { + return p.Token } From 4408acf77f1607b614eb4f5dbe260b3dc7511614 Mon Sep 17 00:00:00 2001 From: ilzheev Date: Tue, 15 Sep 2020 13:04:35 +0400 Subject: [PATCH 13/19] remove unnessecary function --- portainer.go | 5 ----- portainer_test.go | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/portainer.go b/portainer.go index 59be4a1..701bcbb 100644 --- a/portainer.go +++ b/portainer.go @@ -170,8 +170,3 @@ func (p *Portainer) makeRequest(method string, path string, data []byte) ([]byte return ioutil.ReadAll(resp.Body) } - -// Token() returns Portainer token -func (p *Portainer) GetToken() string { - return p.Token -} diff --git a/portainer_test.go b/portainer_test.go index f9bd8c3..6a17076 100644 --- a/portainer_test.go +++ b/portainer_test.go @@ -28,7 +28,7 @@ func TestNewPortainer(t *testing.T) { // start portainer client p := NewPortainer("test", "test", srv.URL) - assert.Equal(p.GetToken(), success.JWT) + assert.Equal(p.Token, success.JWT) } From 1031e5f6d849cf479163363b7d777dd1cf9ce6cb Mon Sep 17 00:00:00 2001 From: ilzheev Date: Tue, 15 Sep 2020 13:36:37 +0400 Subject: [PATCH 14/19] remove logrus --- go.mod | 2 -- go.sum | 11 ----------- main.go | 47 ++++++++++++++++++++++++----------------------- portainer.go | 7 +++---- 4 files changed, 27 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index 4445e33..5239779 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,4 @@ require ( github.com/go-yaml/yaml v2.1.0+incompatible github.com/jinzhu/configor v1.2.0 github.com/mcuadros/go-defaults v1.2.0 - github.com/sirupsen/logrus v1.6.0 - github.com/stretchr/testify v1.2.2 ) diff --git a/go.sum b/go.sum index b90e0ae..3d86f36 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,14 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/jinzhu/configor v1.2.0 h1:u78Jsrxw2+3sGbGMgpY64ObKU4xWCNmNRJIjGVqxYQA= github.com/jinzhu/configor v1.2.0/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc= github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/main.go b/main.go index fe17f9c..27ae776 100644 --- a/main.go +++ b/main.go @@ -2,12 +2,15 @@ package main import ( "flag" - "fmt" + "log" "os/user" "strings" "sync" +) - log "github.com/sirupsen/logrus" +const ( + ColorRed = "\033[31m" + ColorReset = "\033[0m" ) func main() { @@ -16,11 +19,9 @@ func main() { var live bool var conf *Config - log.SetLevel(5) - usr, err := user.Current() if err != nil { - log.Error(err) + log.Println(err) } // default config location @@ -34,20 +35,20 @@ func main() { flag.Parse() - log.Info("Using config: ", configFile) + log.Printf("Using config: %s\n", configFile) // load config if conf, err = NewConfig(configFile); err != nil { log.Fatal(err) } - log.Info("Starting restart system") - log.Info("Portainer endpoint: ", conf.Endpoint) + log.Printf("Starting restart system\n") + log.Printf("Portainer endpoint: %s\n", conf.Endpoint) if live { - log.Warn("LIVE mode: network will be restarted!") + log.Println("LIVE mode: network will be restarted!") } else { - log.Info("DRY RUN mode: simulating restart") + log.Println("DRY RUN mode: simulating restart") } // initialize portainer @@ -62,29 +63,29 @@ func main() { // get factomd containers for each endpoint for i := range endpoints { if endpoints[i].PublicURL != "" { - fmt.Printf("------------------------------\n") - log.Debug("Trying connecting to ", endpoints[i].Name, " (", endpoints[i].PublicURL, "), ID=", endpoints[i].ID) + log.Println("------------------------------") + log.Printf("Trying connecting to %s (%s), ID=%d\n", endpoints[i].Name, endpoints[i].PublicURL, endpoints[i].ID) endpoints[i].Containers, err = p.GetDockerContainers(endpoints[i].ID) if err != nil { - log.Error(err) + log.Println(string(ColorRed), err, string(ColorReset)) } for j := range endpoints[i].Containers { version := strings.Split(endpoints[i].Containers[j].Image, ":") if endpoints[i].Containers[j].State == "running" { - log.Debug("factomd container is ", endpoints[i].Containers[j].State, "\n", endpoints[i].Containers[j].ID, "\n", version[1]) + log.Printf("factomd container is %s\n%s\n%s\n", endpoints[i].Containers[j].State, endpoints[i].Containers[j].ID, version[1]) } else { - log.Warn("factomd container is ", endpoints[i].Containers[j].State, "\n", endpoints[i].Containers[j].ID, "\n", version[1]) + log.Printf("factomd container is %s\n%s\n%s\n", endpoints[i].Containers[j].State, endpoints[i].Containers[j].ID, version[1]) } } } } // RESTART START - fmt.Printf("------------------------------\n") + log.Println("------------------------------") if live { - log.Info("RESTARTING NOW") + log.Println("RESTARTING NOW") } else { - log.Info("SIMULATING RESTART (DRY-RUN)") + log.Println("SIMULATING RESTART (DRY-RUN)") } var wg sync.WaitGroup @@ -105,12 +106,12 @@ func main() { }(e.Name, e.ID, c.ID) } else { // if dry-run mode, just print endpoint and containerId - log.Info("OK ", e.Name, " (", c.ID, ")") + log.Printf("OK %s (%s)\n", e.Name, c.ID) } } } else { // log skipped endpoints with no containers - log.Warn("SKIP ", e.Name) + log.Printf("SKIP %s", e.Name) } } @@ -125,11 +126,11 @@ func restart(p *Portainer, name string, endpointID int, containerID string) { err := p.RestartDockerContainer(endpointID, containerID) if err != nil { // if restart request failed, print error - log.Error("ERROR ", name, " (", containerID, ")") - log.Error(err) + log.Printf("ERROR %s (%s)\n", name, containerID) + log.Println(string(ColorRed), err, string(ColorReset)) } else { // no error = success restart, print OK - log.Info("OK ", name, " (", containerID, ")") + log.Printf("OK %s (%s)\n", name, containerID) } } diff --git a/portainer.go b/portainer.go index 701bcbb..93ed450 100644 --- a/portainer.go +++ b/portainer.go @@ -5,11 +5,10 @@ import ( "encoding/json" "fmt" "io/ioutil" + "log" "net/http" "strconv" "time" - - log "github.com/sirupsen/logrus" ) // HTTP request timeout @@ -78,7 +77,7 @@ func NewPortainer(username string, password string, endpoint string) *Portainer err = json.Unmarshal(body, token) if err != nil { - log.Error(err) + log.Println(err) } if token.Err != "" { @@ -88,7 +87,7 @@ func NewPortainer(username string, password string, endpoint string) *Portainer log.Fatal("Portainer server didn't return JWT token") } - log.Info("Successfully logged in as " + username) + log.Printf("Successfully logged in as %s\n", username) return &Portainer{Token: token.JWT, Endpoint: endpoint} } From 4dabf5499c4990622503ce0f6ec82c7f75acd6d3 Mon Sep 17 00:00:00 2001 From: ilzheev Date: Tue, 15 Sep 2020 13:42:11 +0400 Subject: [PATCH 15/19] blue/yellow coloring for dry-run/live modes --- main.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 27ae776..2552c86 100644 --- a/main.go +++ b/main.go @@ -9,8 +9,10 @@ import ( ) const ( - ColorRed = "\033[31m" - ColorReset = "\033[0m" + ColorRed = "\033[31m" + ColorReset = "\033[0m" + ColorYellow = "\033[33m" + ColorBlue = "\033[34m" ) func main() { @@ -46,9 +48,9 @@ func main() { log.Printf("Portainer endpoint: %s\n", conf.Endpoint) if live { - log.Println("LIVE mode: network will be restarted!") + log.Println(string(ColorYellow), "LIVE mode: network will be restarted!", string(ColorReset)) } else { - log.Println("DRY RUN mode: simulating restart") + log.Println(string(ColorBlue), "DRY RUN mode: simulating restart", string(ColorReset)) } // initialize portainer From 2ab24a489a1b978968571cd36a6dfe88cef5cdfe Mon Sep 17 00:00:00 2001 From: ilzheev Date: Tue, 15 Sep 2020 13:43:49 +0400 Subject: [PATCH 16/19] remore err declaration --- main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/main.go b/main.go index 2552c86..4c12f37 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,6 @@ const ( func main() { - var err error var live bool var conf *Config From 34f198b963e7e9f1928153727b8f178cfda8d506 Mon Sep 17 00:00:00 2001 From: ilzheev Date: Tue, 15 Sep 2020 13:46:25 +0400 Subject: [PATCH 17/19] using filepath join for config path --- main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 4c12f37..1c590a3 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "flag" "log" "os/user" + "path/filepath" "strings" "sync" ) @@ -26,7 +27,7 @@ func main() { } // default config location - configFile := usr.HomeDir + "/.factom-restart/config.yaml" + configFile := filepath.Join(usr.HomeDir, ".factom-restart/config.yaml") // check if custom config location passed as flag flag.StringVar(&configFile, "c", configFile, "config.yaml path") From f58f205cab982bec86f9ebce20b2f16ed3045ae1 Mon Sep 17 00:00:00 2001 From: ilzheev Date: Tue, 15 Sep 2020 13:49:33 +0400 Subject: [PATCH 18/19] remove extra yaml parsing --- config.go | 13 +------------ go.mod | 1 - go.sum | 2 -- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/config.go b/config.go index 64f76d3..183c3f3 100644 --- a/config.go +++ b/config.go @@ -1,9 +1,6 @@ package main import ( - "io/ioutil" - - "github.com/go-yaml/yaml" "github.com/jinzhu/configor" "github.com/mcuadros/go-defaults" ) @@ -21,15 +18,7 @@ func NewConfig(configFile string) (*Config, error) { config := new(Config) defaults.SetDefaults(config) - configBytes, err := ioutil.ReadFile(configFile) - if err == nil { - err = yaml.Unmarshal(configBytes, &config) - if err != nil { - return nil, err - } - } - - if err := configor.Load(config); err != nil { + if err := configor.Load(config, configFile); err != nil { return nil, err } return config, nil diff --git a/go.mod b/go.mod index 5239779..ee5c07a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/DeFacto-Team/factom-network-restart go 1.14 require ( - github.com/go-yaml/yaml v2.1.0+incompatible github.com/jinzhu/configor v1.2.0 github.com/mcuadros/go-defaults v1.2.0 ) diff --git a/go.sum b/go.sum index 3d86f36..3cd6e9b 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= -github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/jinzhu/configor v1.2.0 h1:u78Jsrxw2+3sGbGMgpY64ObKU4xWCNmNRJIjGVqxYQA= github.com/jinzhu/configor v1.2.0/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= From 20b3ea9f30175f9824284550f90b2e41d3b11a76 Mon Sep 17 00:00:00 2001 From: ilzheev Date: Tue, 15 Sep 2020 13:53:33 +0400 Subject: [PATCH 19/19] readme upd --- README.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 1dc53c2..48771cb 100644 --- a/README.md +++ b/README.md @@ -8,55 +8,55 @@ Config file with Portainer credentials should be placed in `~/.factom-restart/co ### Dry-run Run in dry-run mode (scans Portainer endpoints and containers, no restart happens): ```bash -go run main.go +go run . ``` Output: ```bash -INFO[0000] Using config: /Users/anton/.factom-restart/config.yaml -INFO[0000] Starting restart system -INFO[0000] Portainer endpoint: https://test.com -INFO[0000] DRY RUN mode: simulating restart -INFO[0001] Successfully logged in as anton +2020/09/15 13:49:01 Using config: /Users/anton/.factom-restart/config.yaml +2020/09/15 13:49:01 Starting restart system +2020/09/15 13:49:01 Portainer endpoint: https://test.com +2020/09/15 13:49:01 DRY RUN mode: simulating restart +2020/09/15 13:49:01 Successfully logged in as anton ------------------------------ -DEBU[0001] Trying connecting to X (xxx.xxx.xxx.xxx), ID=2 -ERRO[0001] Can not connect to remote host +2020/09/15 13:49:01 Trying connecting to X (xxx.xxx.xxx.xxx), ID=2 +2020/09/15 13:49:01 Empty response received from Portainer API ------------------------------ -DEBU[0003] Trying connecting to Y (yyy.yyy.yyy.yyy), ID=29 -DEBU[0003] factomd container is running +2020/09/15 13:49:02 Trying connecting to Y (yyy.yyy.yyy.yyy), ID=29 +2020/09/15 13:49:02 factomd container is running 307f871a97e1dd91851c6298cf3a183f1709d908810b0059ce9094622bb78126 v6.6.0-alpine … ------------------------------ -INFO[0033] SIMULATING RESTART (DRY-RUN) -WARN[0033] SKIP X -INFO[0033] OK Y (307f871a97e1dd91851c6298cf3a183f1709d908810b0059ce9094622bb78126) +2020/09/15 13:49:05 SIMULATING RESTART (DRY-RUN) +2020/09/15 13:49:05 SKIP X +2020/09/15 13:49:05 OK Y (307f871a97e1dd91851c6298cf3a183f1709d908810b0059ce9094622bb78126) … ``` ### Live mode Run in live mode (restarts the network): ```bash -go run main.go --live +go run . --live ``` Output: ```bash -INFO[0000] Using config: /Users/anton/.factom-restart/config.yaml -INFO[0000] Starting restart system -INFO[0000] Portainer endpoint: https://test.com -WARN[0000] LIVE mode: network will be restarted! -INFO[0001] Successfully logged in as anton +2020/09/15 13:49:01 Using config: /Users/anton/.factom-restart/config.yaml +2020/09/15 13:49:01 Starting restart system +2020/09/15 13:49:01 Portainer endpoint: https://test.com +2020/09/15 13:49:01 LIVE mode: network will be restarted +2020/09/15 13:49:01 Successfully logged in as anton ------------------------------ -DEBU[0001] Trying connecting to X (xxx.xxx.xxx.xxx), ID=2 -ERRO[0001] Can not connect to remote host +2020/09/15 13:49:01 Trying connecting to X (xxx.xxx.xxx.xxx), ID=2 +2020/09/15 13:49:01 Empty response received from Portainer API ------------------------------ -DEBU[0003] Trying connecting to Y (yyy.yyy.yyy.yyy), ID=29 -DEBU[0003] factomd container is running +2020/09/15 13:49:02 Trying connecting to Y (yyy.yyy.yyy.yyy), ID=29 +2020/09/15 13:49:02 factomd container is running 307f871a97e1dd91851c6298cf3a183f1709d908810b0059ce9094622bb78126 v6.6.0-alpine … ------------------------------ -INFO[0033] RESTARTING NOW -WARN[0033] SKIP X -INFO[0033] OK Y (307f871a97e1dd91851c6298cf3a183f1709d908810b0059ce9094622bb78126) +2020/09/15 13:49:05 RESTARTING NOW +2020/09/15 13:49:05 SKIP X +2020/09/15 13:49:05 OK Y (307f871a97e1dd91851c6298cf3a183f1709d908810b0059ce9094622bb78126) … ```