diff --git a/cmd/synchronization/app/BUILD.bazel b/cmd/synchronization/app/BUILD.bazel index 892f839d..4400865b 100644 --- a/cmd/synchronization/app/BUILD.bazel +++ b/cmd/synchronization/app/BUILD.bazel @@ -14,6 +14,7 @@ go_library( "//pkg/composition/client:go_default_library", "//pkg/k8s:go_default_library", "//pkg/k8s/updater:go_default_library", + "//pkg/opsgenie:go_default_library", "//pkg/options:go_default_library", "//pkg/releases:go_default_library", "//pkg/releases/deployinator/client:go_default_library", diff --git a/cmd/synchronization/app/controller_constructor.go b/cmd/synchronization/app/controller_constructor.go index 25258e83..fc593365 100644 --- a/cmd/synchronization/app/controller_constructor.go +++ b/cmd/synchronization/app/controller_constructor.go @@ -7,6 +7,7 @@ import ( comp_v1_client "github.com/atlassian/voyager/pkg/composition/client" "github.com/atlassian/voyager/pkg/k8s" "github.com/atlassian/voyager/pkg/k8s/updater" + "github.com/atlassian/voyager/pkg/opsgenie" "github.com/atlassian/voyager/pkg/releases" "github.com/atlassian/voyager/pkg/releases/deployinator/client" "github.com/atlassian/voyager/pkg/servicecentral" @@ -79,6 +80,10 @@ func (cc *ControllerConstructor) New(config *ctrl.Config, cctx *ctrl.Context) (* scHTTPClient := util.HTTPClient() scClient := servicecentral.NewServiceCentralClient(config.Logger, scHTTPClient, opts.ASAPClientConfig, opts.Providers.ServiceCentralURL) + // create a client for talking to OpsGenie Integration Manager + ogHTTPClient := util.HTTPClient() + ogClient := opsgenie.New(config.Logger, ogHTTPClient, opts.ASAPClientConfig, opts.Providers.OpsgenieIntegrationsManagerURL) + scErrorCounter := prometheus.NewCounter( prometheus.CounterOpts{ Namespace: config.AppName, @@ -133,6 +138,7 @@ func (cc *ControllerConstructor) New(config *ctrl.Config, cctx *ctrl.Context) (* ServiceCentral: servicecentral.NewStore(config.Logger, scClient), ReleaseManagement: releases.NewReleaseManagement(deployinatorHTTPClient, config.Logger), ClusterLocation: opts.Location.ClusterLocation(), + OpsGenie: ogClient, ConfigMapUpdater: configMapObjectUpdater, RoleBindingUpdater: roleBindingObjectUpdater, diff --git a/cmd/synchronization/app/options.go b/cmd/synchronization/app/options.go index 0fb9e74d..765c33c2 100644 --- a/cmd/synchronization/app/options.go +++ b/cmd/synchronization/app/options.go @@ -21,15 +21,17 @@ type Options struct { } type Providers struct { - ServiceCentralURL *url.URL // we use custom json marshalling to read it - DeployinatorURL *url.URL + ServiceCentralURL *url.URL // we use custom json marshalling to read it + DeployinatorURL *url.URL + OpsgenieIntegrationsManagerURL *url.URL } // UnmarshalJSON unmarshals our untyped config file into a typed struct including URLs func (p *Providers) UnmarshalJSON(data []byte) error { var rawProviders struct { - ServiceCentral string `json:"serviceCentral"` - Deployinator string `json:"deployinator"` + ServiceCentral string `json:"serviceCentral"` + Deployinator string `json:"deployinator"` + OpsgenieIntegrationsManager string `json:"opsgenieIntegrationsManager"` } if err := json.Unmarshal(data, &rawProviders); err != nil { @@ -43,7 +45,17 @@ func (p *Providers) UnmarshalJSON(data []byte) error { } depURL, err := url.Parse(rawProviders.Deployinator) p.DeployinatorURL = depURL - return errors.Wrap(err, "unable to parse Deployinator URL") + if err != nil { + return errors.Wrap(err, "unable to parse Deployinator URL") + } + + ogUrl, err := url.Parse(rawProviders.OpsgenieIntegrationsManager) + if err != nil { + return errors.Wrap(err, "unable to parse OpsGenie Integrations Manager URL") + } + p.OpsgenieIntegrationsManagerURL = ogUrl + + return nil } func (o *Options) DefaultAndValidate() []error { @@ -54,6 +66,10 @@ func (o *Options) DefaultAndValidate() []error { allErrors = append(allErrors, errors.New("providers.serviceCentral must be a valid URL")) } + if o.Providers.OpsgenieIntegrationsManagerURL == nil { + allErrors = append(allErrors, errors.New("providers.OpsgenieIntegrationsManagerURL must be a valid URL")) + } + return allErrors } diff --git a/pkg/apis/creator/v1/types.go b/pkg/apis/creator/v1/types.go index 9ed00d26..25bb14f7 100644 --- a/pkg/apis/creator/v1/types.go +++ b/pkg/apis/creator/v1/types.go @@ -59,6 +59,7 @@ func (ss *ServiceSpec) EmailAddress() string { // +k8s:deepcopy-gen=true type ServiceMetadata struct { PagerDuty *PagerDutyMetadata `json:"pagerDuty,omitempty"` + OpsGenie *OpsGenieMetadata `json:"opsGenie,omitempty"` Bamboo *BambooMetadata `json:"bamboo,omitempty"` } @@ -100,6 +101,10 @@ type PagerDutyIntegrationMetadata struct { IntegrationKey string `json:"integrationKey,omitempty"` } +type OpsGenieMetadata struct { + Team string `json:"team,omitempty"` +} + // +k8s:deepcopy-gen=true type Compliance struct { PRGBControl *bool `json:"prgbControl,omitempty"` diff --git a/pkg/opsgenie/it/client_manual_test.go b/pkg/opsgenie/it/client_manual_test.go index e30de768..0a862ace 100644 --- a/pkg/opsgenie/it/client_manual_test.go +++ b/pkg/opsgenie/it/client_manual_test.go @@ -12,7 +12,7 @@ import ( "github.com/atlassian/voyager/pkg/util/pkiutil" "github.com/atlassian/voyager/pkg/util/testutil" "github.com/stretchr/testify/require" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes/scheme" ) diff --git a/pkg/synchronization/BUILD.bazel b/pkg/synchronization/BUILD.bazel index dc0ba44c..6018ac26 100644 --- a/pkg/synchronization/BUILD.bazel +++ b/pkg/synchronization/BUILD.bazel @@ -19,6 +19,7 @@ go_library( "//pkg/composition/client:go_default_library", "//pkg/k8s:go_default_library", "//pkg/k8s/updater:go_default_library", + "//pkg/opsgenie:go_default_library", "//pkg/orchestration/wiring/k8scompute/api:go_default_library", "//pkg/pagerduty:go_default_library", "//pkg/releases:go_default_library", diff --git a/pkg/synchronization/controller.go b/pkg/synchronization/controller.go index 2a6e0c8c..291de4ff 100644 --- a/pkg/synchronization/controller.go +++ b/pkg/synchronization/controller.go @@ -17,6 +17,7 @@ import ( compClient "github.com/atlassian/voyager/pkg/composition/client" "github.com/atlassian/voyager/pkg/k8s" "github.com/atlassian/voyager/pkg/k8s/updater" + "github.com/atlassian/voyager/pkg/opsgenie" apik8scompute "github.com/atlassian/voyager/pkg/orchestration/wiring/k8scompute/api" "github.com/atlassian/voyager/pkg/pagerduty" "github.com/atlassian/voyager/pkg/releases" @@ -58,13 +59,15 @@ const ( ) const ( - serviceCentralPollPeriod = 30 * time.Second - serviceCentralListAllPeriod = 30 * time.Minute - serviceCentralListDriftCompensation = 5 * time.Second - releaseManagementPollPeriod = 5 * time.Second - releaseManagementSyncAllPeriod = 30 * time.Minute - baseDelayProcSec = 15 - rmsPollJitterFactor = 1.2 + serviceCentralPollPeriod = 30 * time.Second + serviceCentralListAllPeriod = 30 * time.Minute + serviceCentralListDriftCompensation = 5 * time.Second + releaseManagementPollPeriod = 5 * time.Second + releaseManagementSyncAllPeriod = 30 * time.Minute + baseDelayProcSec = 15 + rmsPollJitterFactor = 1.2 + secretTypeOpsGenie core_v1.SecretType = voyager.Domain + "/opsgenie" + secretNameOpsGenie = "opsgenie-integrations" ) type ServiceMetadataStore interface { @@ -73,6 +76,10 @@ type ServiceMetadataStore interface { ListModifiedServices(ctx context.Context, user auth.OptionalUser, modifiedSince time.Time) ([]creator_v1.Service, error) } +type OpsGenieIntegrationManagerClient interface { + GetOrCreateIntegrations(ctx context.Context, teamName string) (*opsgenie.IntegrationsResponse, bool /* retriable */, error) +} + type Controller struct { Logger *zap.Logger ReadyForWork func() @@ -85,6 +92,7 @@ type Controller struct { ServiceCentral ServiceMetadataStore ReleaseManagement releases.ReleaseManagementStore ClusterLocation voyager.ClusterLocation + OpsGenie OpsGenieIntegrationManagerClient RoleBindingUpdater updater.ObjectUpdater ConfigMapUpdater updater.ObjectUpdater @@ -439,6 +447,22 @@ func (c *Controller) createOrUpdateServiceMetadata(logger *zap.Logger, ns *core_ }, } + // OpsGenie team is currently optional + if serviceData.Spec.Metadata.OpsGenie != nil { + resp, retriable, err := c.OpsGenie.GetOrCreateIntegrations(context.TODO(), serviceData.Spec.Metadata.OpsGenie.Team) + if err != nil { + return retriable, err + } + spec, err := createOpsGenieSecretSpec(resp, ns.Name) + if err != nil { + return false, err // Error indicates json marshalling problem + } + retriable, err = c.createOrUpdateSecret(logger, spec) + if err != nil { + return retriable, nil + } + } + conflict, retriable, _, err := c.ConfigMapUpdater.CreateOrUpdate( logger, func(r runtime.Object) error { @@ -455,6 +479,29 @@ func (c *Controller) createOrUpdateServiceMetadata(logger *zap.Logger, ns *core_ return false, nil } +// createOpsGenieSecretSpec creates an OpsGenieIntegrations secret object suitable for Secret creation +func createOpsGenieSecretSpec(ogInt *opsgenie.IntegrationsResponse, ns string) (*core_v1.Secret, error) { + jData, err := json.Marshal(ogInt) + if err != nil { + return &core_v1.Secret{}, err + } + + return &core_v1.Secret{ + TypeMeta: meta_v1.TypeMeta{ + Kind: k8s.SecretKind, + APIVersion: core_v1.SchemeGroupVersion.String(), + }, + ObjectMeta: meta_v1.ObjectMeta{ + Name: secretNameOpsGenie, + Namespace: ns, + }, + Data: map[string][]byte{ + secretNameOpsGenie: jData, + }, + Type: secretTypeOpsGenie, + }, nil +} + func (c *Controller) getServiceData(user auth.OptionalUser, name voyager.ServiceName) (*creator_v1.Service, error) { return c.ServiceCentral.GetService(context.Background(), user, servicecentral.ServiceName(name)) } diff --git a/pkg/trebuchet/server/apiserver/apiserver.go b/pkg/trebuchet/server/apiserver/apiserver.go index 127b6478..42b3ad69 100644 --- a/pkg/trebuchet/server/apiserver/apiserver.go +++ b/pkg/trebuchet/server/apiserver/apiserver.go @@ -3,7 +3,7 @@ package apiserver import ( apis_trebuchet "github.com/atlassian/voyager/pkg/apis/trebuchet" "github.com/atlassian/voyager/pkg/apis/trebuchet/install" - "github.com/atlassian/voyager/pkg/apis/trebuchet/v1" + v1 "github.com/atlassian/voyager/pkg/apis/trebuchet/v1" "github.com/atlassian/voyager/pkg/trebuchet" trebuchetrest "github.com/atlassian/voyager/pkg/trebuchet/server/rest" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"