This repository has been archived by the owner on Oct 11, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
VYGR-391: Add OpsGenie integration manager client (#119)
* VYGR-391: Add OpsGenie integration manager client * VYGR-391: Use correct ASAP audience * VYGR-391: Fix BUILD.bazel ordering for opsgenie pkg
- Loading branch information
1 parent
915a181
commit ba30f9d
Showing
8 changed files
with
477 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") | ||
|
||
go_library( | ||
name = "go_default_library", | ||
srcs = [ | ||
"client.go", | ||
"types.go", | ||
], | ||
importpath = "github.com/atlassian/voyager/pkg/opsgenie", | ||
visibility = ["//visibility:public"], | ||
deps = [ | ||
"//pkg/util:go_default_library", | ||
"//pkg/util/httputil:go_default_library", | ||
"//pkg/util/pkiutil:go_default_library", | ||
"//vendor/bitbucket.org/atlassianlabs/restclient:go_default_library", | ||
"//vendor/github.com/pkg/errors:go_default_library", | ||
"//vendor/go.uber.org/zap:go_default_library", | ||
], | ||
) | ||
|
||
go_test( | ||
name = "go_default_test", | ||
size = "small", | ||
srcs = ["client_test.go"], | ||
data = glob(["testdata/**"]), | ||
embed = [":go_default_library"], | ||
race = "on", | ||
deps = [ | ||
"//pkg/util:go_default_library", | ||
"//pkg/util/httputil/httptest:go_default_library", | ||
"//pkg/util/pkiutil:go_default_library", | ||
"//pkg/util/pkiutil/pkitest:go_default_library", | ||
"//vendor/github.com/stretchr/testify/require:go_default_library", | ||
"//vendor/go.uber.org/zap/zaptest:go_default_library", | ||
], | ||
) |
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,105 @@ | ||
package opsgenie | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"net/url" | ||
|
||
"bitbucket.org/atlassianlabs/restclient" | ||
"github.com/atlassian/voyager/pkg/util" | ||
"github.com/atlassian/voyager/pkg/util/httputil" | ||
"github.com/atlassian/voyager/pkg/util/pkiutil" | ||
"github.com/pkg/errors" | ||
"go.uber.org/zap" | ||
) | ||
|
||
const ( | ||
asapAudience = "micros-server" | ||
asapSubject = "" | ||
integrationsPath = "/api/v1/opsgenie/integrations" | ||
) | ||
|
||
type Client struct { | ||
logger *zap.Logger | ||
httpClient *http.Client | ||
asap pkiutil.ASAP | ||
rm *restclient.RequestMutator | ||
} | ||
|
||
func New(logger *zap.Logger, httpClient *http.Client, asap pkiutil.ASAP, baseURL *url.URL) *Client { | ||
rm := restclient.NewRequestMutator( | ||
restclient.BaseURL(baseURL.String()), | ||
) | ||
return &Client{ | ||
logger: logger, | ||
httpClient: httpClient, | ||
asap: asap, | ||
rm: rm, | ||
} | ||
} | ||
|
||
// Gets OpsGenie integrations | ||
// return codes: | ||
// - 400: Bad request to Opsgenie | ||
// - 401: Unauthorized | ||
// - 404: Not found returned by Opsgenie. Does the specified Opsgenie team exist? | ||
// - 422: Semantic error in request to Opsgenie. | ||
// - 429: Rate limited by Opsgenie. | ||
func (c *Client) GetOrCreateIntegrations(ctx context.Context, teamName string) (*IntegrationsResponse, bool /* retriable */, error) { | ||
req, err := c.rm.NewRequest( | ||
pkiutil.AuthenticateWithASAP(c.asap, asapAudience, asapSubject), | ||
restclient.Method(http.MethodGet), | ||
restclient.JoinPath(fmt.Sprintf("%s/%s", integrationsPath, teamName)), | ||
restclient.Context(ctx), | ||
restclient.Header("Accept", "application/json"), | ||
) | ||
if err != nil { | ||
return nil, false, errors.Wrap(err, "failed to create get integrations request") | ||
} | ||
|
||
response, err := c.httpClient.Do(req) | ||
if err != nil { | ||
return nil, false, errors.Wrap(err, "failed to execute get integrations request") | ||
} | ||
defer util.CloseSilently(response.Body) | ||
|
||
retriable := false | ||
switch response.StatusCode { | ||
case http.StatusInternalServerError: | ||
retriable = true | ||
case http.StatusTooManyRequests: | ||
retriable = true | ||
} | ||
|
||
respBody, err := ioutil.ReadAll(response.Body) | ||
if err != nil { | ||
return nil, retriable, errors.Wrap(err, "failed to read response body") | ||
} | ||
|
||
if response.StatusCode != http.StatusOK { | ||
message := fmt.Sprintf("failed to get integrations for team %q. Response: %s", teamName, respBody) | ||
return nil, retriable, clientError(response.StatusCode, message) | ||
} | ||
|
||
var parsedBody IntegrationsResponse | ||
err = json.Unmarshal(respBody, &parsedBody) | ||
if err != nil { | ||
return nil, retriable, errors.Wrap(err, "failed to unmarshal response body") | ||
} | ||
|
||
return &parsedBody, retriable, nil | ||
} | ||
|
||
func clientError(statusCode int, message string) error { | ||
switch statusCode { | ||
case http.StatusNotFound: | ||
return httputil.NewNotFound(message) | ||
case http.StatusBadRequest: | ||
return httputil.NewBadRequest(message) | ||
default: | ||
return httputil.NewUnknown(fmt.Sprintf("%s (%s)", message, http.StatusText(statusCode))) | ||
} | ||
} |
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,125 @@ | ||
package opsgenie | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"net/http/httptest" | ||
"net/url" | ||
"testing" | ||
|
||
"github.com/atlassian/voyager/pkg/util" | ||
. "github.com/atlassian/voyager/pkg/util/httputil/httptest" | ||
"github.com/atlassian/voyager/pkg/util/pkiutil" | ||
"github.com/atlassian/voyager/pkg/util/pkiutil/pkitest" | ||
"github.com/stretchr/testify/require" | ||
"go.uber.org/zap/zaptest" | ||
) | ||
|
||
func TestGetIntegrations(t *testing.T) { | ||
t.Parallel() | ||
|
||
const teamName = "Platform SRE" | ||
|
||
// given | ||
handler := MockHandler(Match( | ||
Method(http.MethodGet), | ||
Path(fmt.Sprintf("%s/%s", integrationsPath, teamName)), | ||
).Respond( | ||
Status(http.StatusOK), | ||
JSONFromFile(t, "get_or_create_integrations.rsp.json"), | ||
)) | ||
ogIntManagerMockServer := httptest.NewServer(handler) | ||
defer ogIntManagerMockServer.Close() | ||
|
||
// when | ||
ogIntManagerClient := mockOpsGenieIntegrationManagerClient(t, ogIntManagerMockServer.URL, pkitest.MockASAPClientConfig(t)) | ||
_, retriable, err := ogIntManagerClient.GetOrCreateIntegrations(context.Background(), teamName) | ||
|
||
// then | ||
require.NoError(t, err) | ||
require.Equal(t, 1, handler.RequestSnapshots.Calls()) | ||
require.False(t, retriable) | ||
} | ||
|
||
func TestGetIntegrationsTeamNotFound(t *testing.T) { | ||
t.Parallel() | ||
|
||
const teamName = "Platform SRE" | ||
|
||
// given | ||
handler := MockHandler(Match( | ||
Method(http.MethodGet), | ||
Path(fmt.Sprintf("%s/%s", integrationsPath, teamName)), | ||
).Respond( | ||
Status(http.StatusNotFound), | ||
)) | ||
ogIntManagerMockServer := httptest.NewServer(handler) | ||
defer ogIntManagerMockServer.Close() | ||
|
||
// when | ||
ogIntManagerClient := mockOpsGenieIntegrationManagerClient(t, ogIntManagerMockServer.URL, pkitest.MockASAPClientConfig(t)) | ||
_, retriable, err := ogIntManagerClient.GetOrCreateIntegrations(context.Background(), teamName) | ||
|
||
// then | ||
require.Error(t, err) | ||
require.Equal(t, 1, handler.RequestSnapshots.Calls()) | ||
require.False(t, retriable) | ||
} | ||
|
||
func TestGetIntegrationsRateLimited(t *testing.T) { | ||
t.Parallel() | ||
|
||
const teamName = "Platform SRE" | ||
|
||
// given | ||
handler := MockHandler(Match( | ||
Method(http.MethodGet), | ||
Path(fmt.Sprintf("%s/%s", integrationsPath, teamName)), | ||
).Respond( | ||
Status(http.StatusTooManyRequests), | ||
)) | ||
ogIntManagerMockServer := httptest.NewServer(handler) | ||
defer ogIntManagerMockServer.Close() | ||
|
||
// when | ||
ogIntManagerClient := mockOpsGenieIntegrationManagerClient(t, ogIntManagerMockServer.URL, pkitest.MockASAPClientConfig(t)) | ||
_, retriable, err := ogIntManagerClient.GetOrCreateIntegrations(context.Background(), teamName) | ||
|
||
// then | ||
require.Error(t, err) | ||
require.Equal(t, 1, handler.RequestSnapshots.Calls()) | ||
require.True(t, retriable) | ||
} | ||
|
||
func TestGetIntegrationsInternalServerError(t *testing.T) { | ||
t.Parallel() | ||
|
||
const teamName = "Platform SRE" | ||
|
||
// given | ||
handler := MockHandler(Match( | ||
Method(http.MethodGet), | ||
Path(fmt.Sprintf("%s/%s", integrationsPath, teamName)), | ||
).Respond( | ||
Status(http.StatusInternalServerError), | ||
)) | ||
ogIntManagerMockServer := httptest.NewServer(handler) | ||
defer ogIntManagerMockServer.Close() | ||
|
||
// when | ||
ogIntManagerClient := mockOpsGenieIntegrationManagerClient(t, ogIntManagerMockServer.URL, pkitest.MockASAPClientConfig(t)) | ||
_, retriable, err := ogIntManagerClient.GetOrCreateIntegrations(context.Background(), teamName) | ||
|
||
// then | ||
require.Error(t, err) | ||
require.Equal(t, 1, handler.RequestSnapshots.Calls()) | ||
require.True(t, retriable) | ||
} | ||
|
||
func mockOpsGenieIntegrationManagerClient(t *testing.T, serverMockAddress string, asap pkiutil.ASAP) *Client { | ||
opsgenieIntegrationManagerURL, err := url.Parse(serverMockAddress) | ||
require.NoError(t, err) | ||
httpClient := util.HTTPClient() | ||
return New(zaptest.NewLogger(t), httpClient, asap, opsgenieIntegrationManagerURL) | ||
} |
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,22 @@ | ||
load("@io_bazel_rules_go//go:def.bzl", "go_test") | ||
|
||
go_test( | ||
name = "go_default_test", | ||
size = "medium", | ||
srcs = ["client_manual_test.go"], | ||
race = "on", | ||
tags = [ | ||
"external", | ||
"manual", | ||
], | ||
deps = [ | ||
"//pkg/opsgenie:go_default_library", | ||
"//pkg/util:go_default_library", | ||
"//pkg/util/logz:go_default_library", | ||
"//pkg/util/pkiutil:go_default_library", | ||
"//pkg/util/testutil:go_default_library", | ||
"//vendor/github.com/stretchr/testify/require:go_default_library", | ||
"//vendor/k8s.io/api/core/v1:go_default_library", | ||
"//vendor/k8s.io/client-go/kubernetes/scheme:go_default_library", | ||
], | ||
) |
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,65 @@ | ||
package it | ||
|
||
import ( | ||
"encoding/json" | ||
"net/url" | ||
"os" | ||
"testing" | ||
|
||
"github.com/atlassian/voyager/pkg/opsgenie" | ||
"github.com/atlassian/voyager/pkg/util" | ||
"github.com/atlassian/voyager/pkg/util/logz" | ||
"github.com/atlassian/voyager/pkg/util/pkiutil" | ||
"github.com/atlassian/voyager/pkg/util/testutil" | ||
"github.com/stretchr/testify/require" | ||
"k8s.io/api/core/v1" | ||
"k8s.io/client-go/kubernetes/scheme" | ||
) | ||
|
||
const ( | ||
opsGenieIntManURL = "https://micros.prod.atl-paas.net" | ||
) | ||
|
||
// NOTE: THIS WILL CREATE INTEGRATIONS IF NONE EXIST | ||
func TestGetIntegrations(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Prepare ASAP secrets from Kubernetes Secret | ||
asapCreatorSecret := getSecret(t) | ||
ctx := testutil.ContextWithLogger(t) | ||
testLogger := logz.RetrieveLoggerFromContext(ctx) | ||
asapConfig, asapErr := pkiutil.NewASAPClientConfigFromKubernetesSecret(asapCreatorSecret) | ||
require.NoError(t, asapErr) | ||
|
||
client := util.HTTPClient() | ||
c := opsgenie.New(testLogger, client, asapConfig, parseURL(t, opsGenieIntManURL)) | ||
|
||
// Get Service Attributes | ||
resp, _, err := c.GetOrCreateIntegrations(ctx, "Platform SRE") | ||
require.NoError(t, err) | ||
require.True(t, len(resp.Integrations) > 0) | ||
|
||
t.Logf("Number of returned integrations: %v", len(resp.Integrations)) | ||
t.Logf("Response: %#v", resp) | ||
|
||
bytes, err := json.Marshal(resp) | ||
require.NoError(t, err) | ||
t.Logf("Attributes JSON: %#v", string(bytes)) | ||
} | ||
|
||
// data should be "export OPSGENIE_YAML=$(kubectl -n voyager get secrets asap-creator -o yaml)" | ||
func getSecret(t *testing.T) *v1.Secret { | ||
data := os.Getenv("OPSGENIE_YAML") //Envvar containing the yaml contents of the secret | ||
|
||
decode := scheme.Codecs.UniversalDeserializer().Decode | ||
destination := &v1.Secret{} | ||
_, _, err := decode([]byte(data), nil, destination) | ||
require.NoError(t, err) | ||
return destination | ||
} | ||
|
||
func parseURL(t *testing.T, urlstr string) *url.URL { | ||
urlobj, err := url.Parse(urlstr) | ||
require.NoError(t, err) | ||
return urlobj | ||
} |
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,15 @@ | ||
{ | ||
"integrations": [ | ||
{ | ||
"id": "string", | ||
"name": "string", | ||
"type": "string", | ||
"teamId": "string", | ||
"teamName": "string", | ||
"priority": "string", | ||
"apiKey": "string", | ||
"endpoint": "string", | ||
"envType": "string" | ||
} | ||
] | ||
} |
Oops, something went wrong.