Update the strategy to check cluster status
authorSofian Brabez <sbz@6dev.net>
Mon, 23 Jul 2018 16:03:41 +0000 (17:03 +0100)
committerSofian Brabez <sbz@6dev.net>
Mon, 23 Jul 2018 16:03:41 +0000 (17:03 +0100)
Vault has an upstream issue /hashicorp/vault/issues/3863 which reports
uncorrectly the health status of the cluster in HA if a node part of the
cluster is sealed.

In order to have a more robust check of the Vault cluster nodes, we are fetching
their private IPs using AWS EC2 and AWS tags on theknowledge cluster and
lookup their sealed status on the sys/health endpoint.

aws.go [new file with mode: 0644]
main.go
vault.go

diff --git a/aws.go b/aws.go
new file mode 100644 (file)
index 0000000..6f4a2dd
--- /dev/null
+++ b/aws.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+       "github.com/aws/aws-sdk-go/aws"
+       "github.com/aws/aws-sdk-go/aws/endpoints"
+       "github.com/aws/aws-sdk-go/aws/session"
+       "github.com/aws/aws-sdk-go/service/ec2"
+
+       log "github.com/sirupsen/logrus"
+)
+
+func GetInstancesPrivateIps(filter map[string]string) ([]string, error) {
+
+       sess := session.Must(session.NewSession(&aws.Config{
+               Region: aws.String(endpoints.EuCentral1RegionID),
+       }))
+
+       service := ec2.New(sess)
+
+       input := &ec2.DescribeInstancesInput{
+               Filters: []*ec2.Filter{
+                       {
+                               Name:   aws.String(filter["Name"]),
+                               Values: []*string{aws.String(filter["Value"])},
+                       },
+               },
+       }
+
+       result, err := service.DescribeInstances(input)
+       if err != nil {
+               log.Debugf("Unable to DescribeInstances using input: %+v", result)
+               return nil, err
+       }
+
+       nodeIps := make([]string, 0, len(result.Reservations))
+
+       for _, r := range result.Reservations {
+               if len(r.Instances) > 0 {
+                       nodeIps = append(nodeIps, *r.Instances[0].PrivateIpAddress)
+               }
+       }
+
+       log.Debugf("Found %d private IPs: %v", len(nodeIps), nodeIps)
+
+       return nodeIps, nil
+}
diff --git a/main.go b/main.go
index 3385021a07005c259a6f620d9af3f5ed420e121b..557c3e4ffbcec1cf4e69a607ed3eb779e616ee2e 100644 (file)
--- a/main.go
+++ b/main.go
@@ -51,4 +51,6 @@ func main() {
        if err != nil {
                log.Fatalf("Error sending to Slack: %s", err)
        }
+
+       log.Info("Done.")
 }
index 6538d9260e989b5058c81050032c2fed154fb1e3..4efd63e5f9b2b0d8c7f493a33b7983bdecbf761f 100644 (file)
--- a/vault.go
+++ b/vault.go
@@ -9,14 +9,30 @@ import (
        log "github.com/sirupsen/logrus"
 )
 
+const (
+       HealthEndpointURL = "/v1/sys/health"
+       LeaderEndpointURL = "/v1/sys/leader"
+
+       // Vault Health Status https://www.vaultproject.io/api/system/health.html
+       VaultActive   = http.StatusOK
+       VaultStandby  = http.StatusTooManyRequests
+       VaultRecovery = 472
+       VaultNotReady = http.StatusNotImplemented
+       VaultSealed   = http.StatusServiceUnavailable
+)
+
 type Vault struct {
        URL              string
        ConnectionStatus string
        ClusterStatus    string
 }
 
+type VaultSealedResult struct {
+       Sealed bool `json:"sealed"`
+}
+
 func (v *Vault) CheckHealth() (bool, error) {
-       healthURL := v.URL + "/v1/sys/health"
+       healthURL := v.URL + HealthEndpointURL
        cnx := NewConnection(healthURL)
        log.Debugf("Vault Connection on %s", healthURL)
        resp, err := cnx.Head()
@@ -27,40 +43,62 @@ func (v *Vault) CheckHealth() (bool, error) {
 
        log.Debugf("Vault CheckHealth response: %s", resp.Status)
 
-       return resp.StatusCode == http.StatusOK, nil
+       return isHealthy(resp.StatusCode), nil
 }
 
 func (v *Vault) CheckCluster() (bool, error) {
-       ClusterURL := v.URL + "/v1/sys/leader"
-       cnx := NewConnection(ClusterURL)
-       log.Debugf("Vault Connection on %s", ClusterURL)
+       vaultFilter := make(map[string]string)
+       vaultFilter["Name"] = "tag:Cluster"
+       vaultFilter["Value"] = "theknowledge"
+
+       vaultNodeIps, err := GetInstancesPrivateIps(vaultFilter)
+       if err != nil {
+               return false, err
+       }
+
+       for _, nodeIp := range vaultNodeIps {
+               healthURL := "https://" + nodeIp + ":8200" + HealthEndpointURL
+               if sealed, err := isSealed(healthURL); sealed {
+                       return false, err
+               }
+       }
+
+       return true, nil
+}
+
+func isHealthy(statusCode int) bool {
+       return statusCode == VaultActive || statusCode == VaultStandby
+}
+
+func isSealed(healthURL string) (bool, error) {
+       cnx := NewConnection(healthURL)
+       log.Debugf("Vault isSealed node connection on %s", healthURL)
        resp, err := cnx.Get()
        if err != nil {
                return false, err
        }
        defer resp.Body.Close()
 
-       log.Debugf("Vault CheckCluster response: %s", resp.Status)
+       log.Debugf("Vault isSealed connection response: %s", resp.Status)
+       if resp.StatusCode == VaultSealed {
+               return true, nil
+       }
 
        buf, err := ioutil.ReadAll(resp.Body)
        if err != nil {
                return false, err
        }
 
-       var result struct {
-               LeaderAddress        string `json:"leader_address"`
-               IsSelf               bool   `json:"is_self"`
-               HAEnabled            bool   `json:"ha_enabled"`
-               LeaderClusterAddress string `json:"leader_cluster_address"`
-       }
+       var result VaultSealedResult
+
        err = json.Unmarshal(buf, &result)
        if err != nil {
                return false, err
        }
 
-       log.Debugf("Vault CheckCluster JSON result: %+v", result)
+       log.Debugf("Vault isSealed JSON result: %+v", result)
 
-       return result.HAEnabled == true, nil
+       return result.Sealed, nil
 }
 
 func (v *Vault) Test() (bool, string) {
@@ -70,7 +108,12 @@ func (v *Vault) Test() (bool, string) {
                log.Errorf("Vault CheckHealth error: %s", err)
                v.ConnectionStatus = "KO"
        }
-       v.ConnectionStatus = "OK"
+
+       if result == false {
+               v.ConnectionStatus = "KO"
+       } else {
+               v.ConnectionStatus = "OK"
+       }
        log.Debugf("Vault CheckHealth result: %t", result)
 
        // test cluster
@@ -79,7 +122,12 @@ func (v *Vault) Test() (bool, string) {
                log.Errorf("Vault CheckCluster error: %s", err)
                v.ClusterStatus = "KO"
        }
-       v.ClusterStatus = "OK"
+
+       if result == false {
+               v.ClusterStatus = "KO"
+       } else {
+               v.ClusterStatus = "OK"
+       }
        log.Debugf("Vault CheckCluster result: %t", result)
 
        return result, fmt.Sprintf("> %s `%s` status *%s*. Cluster status *%s*.", v.Name(), v.URL, v.ConnectionStatus, v.ClusterStatus)