Skip to content
This repository has been archived by the owner on Oct 11, 2019. It is now read-only.

Commit

Permalink
VYGR-391: Add OpsGenie integration manager client (#119)
Browse files Browse the repository at this point in the history
* 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
fraser-atlassian authored Feb 13, 2019
1 parent 915a181 commit ba30f9d
Show file tree
Hide file tree
Showing 8 changed files with 477 additions and 0 deletions.
36 changes: 36 additions & 0 deletions pkg/opsgenie/BUILD.bazel
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",
],
)
105 changes: 105 additions & 0 deletions pkg/opsgenie/client.go
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)))
}
}
125 changes: 125 additions & 0 deletions pkg/opsgenie/client_test.go
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)
}
22 changes: 22 additions & 0 deletions pkg/opsgenie/it/BUILD.bazel
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",
],
)
65 changes: 65 additions & 0 deletions pkg/opsgenie/it/client_manual_test.go
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
}
15 changes: 15 additions & 0 deletions pkg/opsgenie/testdata/get_or_create_integrations.rsp.json
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"
}
]
}
Loading

0 comments on commit ba30f9d

Please sign in to comment.