-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
489 additions
and
263 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package health | ||
|
||
import ( | ||
"context" | ||
"strings" | ||
|
||
"cloud.google.com/go/compute/apiv1/computepb" | ||
"github.com/googleapis/gax-go" | ||
) | ||
|
||
// GCPChecker queries the VM's load balancer to check its status. | ||
type GCPChecker struct { | ||
client GCEClient | ||
md Metadata | ||
} | ||
|
||
// Metadata returns environmental metadata for a machine. | ||
type Metadata interface { | ||
Project() string | ||
Backend() string | ||
Region() string | ||
Group() string | ||
} | ||
|
||
// GCEClient queries the Compute API for health updates. | ||
type GCEClient interface { | ||
GetHealth(context.Context, *computepb.GetHealthRegionBackendServiceRequest, ...gax.CallOption) (*computepb.BackendServiceGroupHealth, error) | ||
} | ||
|
||
// NewGCPChecker returns a new instance of GCPChecker. | ||
func NewGCPChecker(c GCEClient, md Metadata) *GCPChecker { | ||
return &GCPChecker{ | ||
client: c, | ||
md: md, | ||
} | ||
} | ||
|
||
// GetHealth contacts the GCP load balancer to get the latest VM health status | ||
// and uses the data to generate a health score. | ||
func (c *GCPChecker) GetHealth(ctx context.Context) float64 { | ||
g := c.md.Group() | ||
req := &computepb.GetHealthRegionBackendServiceRequest{ | ||
BackendService: c.md.Backend(), | ||
Project: c.md.Project(), | ||
Region: c.md.Region(), | ||
ResourceGroupReferenceResource: &computepb.ResourceGroupReference{ | ||
Group: &g, | ||
}, | ||
} | ||
lbHealth, err := c.client.GetHealth(ctx, req) | ||
if err != nil { | ||
return 0 | ||
} | ||
|
||
for _, h := range lbHealth.HealthStatus { | ||
// The group is healthy if at least one of the instances has a 'HEALTHY' health state. | ||
if strings.EqualFold(*h.HealthState, "HEALTHY") { | ||
return 1 | ||
} | ||
} | ||
|
||
return 0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package health | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"testing" | ||
|
||
"cloud.google.com/go/compute/apiv1/computepb" | ||
"github.com/googleapis/gax-go" | ||
"github.com/m-lab/locate/cmd/heartbeat/metadata" | ||
) | ||
|
||
func TestGCPChecker_GetHealth(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
client GCEClient | ||
want float64 | ||
}{ | ||
{ | ||
name: "healthy", | ||
client: &fakeGCEClient{ | ||
status: []string{"HEALTHY"}, | ||
err: false, | ||
}, | ||
want: 1, | ||
}, | ||
{ | ||
name: "unhealthy", | ||
client: &fakeGCEClient{ | ||
status: []string{"UNHEALTHY"}, | ||
err: false, | ||
}, | ||
want: 0, | ||
}, | ||
{ | ||
name: "mix", | ||
client: &fakeGCEClient{ | ||
status: []string{"HEALTHY", "HEALTHY", "UNHEALTHY"}, | ||
err: false, | ||
}, | ||
want: 1, | ||
}, | ||
{ | ||
name: "healthy-lower-case", | ||
client: &fakeGCEClient{ | ||
status: []string{"healthy"}, | ||
err: false, | ||
}, | ||
want: 1, | ||
}, | ||
{ | ||
name: "error", | ||
client: &fakeGCEClient{ | ||
err: true, | ||
}, | ||
want: 0, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
c := NewGCPChecker(tt.client, &metadata.GCPMetadata{}) | ||
if got := c.GetHealth(context.Background()); got != tt.want { | ||
t.Errorf("GCPChecker.GetHealth() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
type fakeGCEClient struct { | ||
status []string | ||
err bool | ||
} | ||
|
||
func (c *fakeGCEClient) GetHealth(ctx context.Context, req *computepb.GetHealthRegionBackendServiceRequest, opts ...gax.CallOption) (*computepb.BackendServiceGroupHealth, error) { | ||
if c.err { | ||
return nil, errors.New("health error") | ||
} | ||
|
||
health := make([]*computepb.HealthStatus, 0) | ||
for _, s := range c.status { | ||
statusPtr := s | ||
health = append(health, &computepb.HealthStatus{HealthState: &statusPtr}) | ||
} | ||
return &computepb.BackendServiceGroupHealth{ | ||
HealthStatus: health, | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package metadata | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/m-lab/go/host" | ||
) | ||
|
||
const groupTemplate = "https://www.googleapis.com/compute/v1/projects/%s/regions/%s/instanceGroups/%s" | ||
|
||
// GCPMetadata contains metadata about a GCP VM. | ||
type GCPMetadata struct { | ||
project string | ||
backend string | ||
region string | ||
group string | ||
} | ||
|
||
// Client uses HTTP requests to query the metadata service. | ||
type Client interface { | ||
ProjectID() (string, error) | ||
Zone() (string, error) | ||
} | ||
|
||
// NewGCPMetadata returns a new instance of GCPMetadata. | ||
func NewGCPMetadata(c Client, hostname string) (*GCPMetadata, error) { | ||
h, err := host.Parse(hostname) | ||
if err != nil { | ||
return nil, err | ||
} | ||
// Backend refers to the GCP load balancer. | ||
// Resources for a GCP load balancer all have the same name. That is, | ||
// the VM hostname with dots turned to dashes (since GCP does not allow | ||
// dots in names). | ||
backend := strings.ReplaceAll(h.String(), ".", "-") | ||
|
||
project, err := c.ProjectID() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
zone, err := c.Zone() | ||
if err != nil { | ||
return nil, err | ||
} | ||
region := zone[:len(zone)-2] | ||
|
||
return &GCPMetadata{ | ||
project: project, | ||
backend: backend, | ||
region: region, | ||
group: fmt.Sprintf(groupTemplate, project, region, backend), | ||
}, nil | ||
} | ||
|
||
// Project ID (e.g., mlab-sandbox). | ||
func (m *GCPMetadata) Project() string { | ||
return m.project | ||
} | ||
|
||
// Backend in GCE. | ||
func (m *GCPMetadata) Backend() string { | ||
return m.backend | ||
} | ||
|
||
// Region derived from zone (e.g., us-west1). | ||
func (m *GCPMetadata) Region() string { | ||
return m.region | ||
} | ||
|
||
// Group is the the URI referencing the instance group. | ||
func (m *GCPMetadata) Group() string { | ||
return m.group | ||
} |
Oops, something went wrong.