Skip to content

Commit

Permalink
Merge pull request #3 from ConductorOne/feat/schedules-sync
Browse files Browse the repository at this point in the history
Add support for syncing Schedules
  • Loading branch information
ggreer authored Jan 3, 2024
2 parents 68f436b + 7af713f commit 3e32284
Show file tree
Hide file tree
Showing 16 changed files with 1,792 additions and 17 deletions.
19 changes: 19 additions & 0 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package connector
import (
"context"
"io"
"math"
"net/http"
"time"

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/annotations"
Expand Down Expand Up @@ -32,6 +35,13 @@ var (
},
Annotations: annotationsForUserResourceType(),
}
resourceTypeSchedule = &v2.ResourceType{
Id: "schedule",
DisplayName: "Schedule",
Traits: []v2.ResourceType_Trait{
v2.ResourceType_TRAIT_GROUP,
},
}
)

type Opsgenie struct {
Expand All @@ -49,6 +59,14 @@ func New(ctx context.Context, apiKey string) (*Opsgenie, error) {
clientConfig := &ogclient.Config{
ApiKey: apiKey,
HttpClient: httpClient,
RetryCount: 10,
Backoff: func(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
// exponential backoff - more information about rate limits in OpsGenie here: https://docs.opsgenie.com/docs/api-rate-limiting
exp := math.Pow(2, float64(attemptNum))
t := time.Duration(200) * time.Millisecond

return t * time.Duration(exp)
},
}

rv := &Opsgenie{
Expand Down Expand Up @@ -89,5 +107,6 @@ func (c *Opsgenie) ResourceSyncers(ctx context.Context) []connectorbuilder.Resou
teamBuilder(c.config),
roleBuilder(c.config),
userBuilder(c.config),
scheduleBuilder(c.config),
}
}
34 changes: 34 additions & 0 deletions pkg/connector/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/annotations"
"google.golang.org/protobuf/types/known/structpb"
)

const ResourcesPageSize = 100
Expand All @@ -22,3 +23,36 @@ func annotationsForUserResourceType() annotations.Annotations {
annos.Update(&v2.SkipEntitlementsAndGrants{})
return annos
}

func getProfileStringArray(profile *structpb.Struct, k string) ([]string, bool) {
var values []string
if profile == nil {
return nil, false
}

v, ok := profile.Fields[k]
if !ok {
return nil, false
}

s, ok := v.Kind.(*structpb.Value_ListValue)
if !ok {
return nil, false
}

for _, v := range s.ListValue.Values {
if strVal := v.GetStringValue(); strVal != "" {
values = append(values, strVal)
}
}

return values, true
}

func scheduleParticipantsToInterfaceSlice(p []string) []interface{} {
var i []interface{}
for _, v := range p {
i = append(i, v)
}
return i
}
245 changes: 245 additions & 0 deletions pkg/connector/schedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package connector

import (
"context"
"fmt"

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/annotations"
"github.com/conductorone/baton-sdk/pkg/pagination"
ent "github.com/conductorone/baton-sdk/pkg/types/entitlement"
"github.com/conductorone/baton-sdk/pkg/types/grant"
rs "github.com/conductorone/baton-sdk/pkg/types/resource"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
ogClient "github.com/opsgenie/opsgenie-go-sdk-v2/client"
"github.com/opsgenie/opsgenie-go-sdk-v2/og"
ogSchedule "github.com/opsgenie/opsgenie-go-sdk-v2/schedule"
)

const (
scheduleMember = "member"
scheduleOnCall = "on-call"

userParticipantType = "user"
teamParticipantType = "team"
)

type scheduleResourceType struct {
resourceType *v2.ResourceType
config *ogClient.Config
}

func (s *scheduleResourceType) ResourceType(_ context.Context) *v2.ResourceType {
return s.resourceType
}

// parses array of rotations and returns all teams and users that participate in rotations.
func parseRotations(rotation []og.Rotation) ([]string, []string) {
var teams, users []string

for _, r := range rotation {
for _, p := range r.Participants {
if p.Type == teamParticipantType {
teams = append(teams, p.Id)
} else if p.Type == userParticipantType {
users = append(users, p.Id)
}
}
}

return teams, users
}

// scheduleResource creates a new connector resource for a OpsGenie Schedule.
func scheduleResource(schedule *ogSchedule.Schedule) (*v2.Resource, error) {
profile := map[string]interface{}{
"schedule_id": schedule.Id,
"schedule_name": schedule.Name,
}

teams, users := parseRotations(schedule.Rotations)

if len(teams) > 0 {
profile["schedule_teams"] = scheduleParticipantsToInterfaceSlice(teams)
}

if len(users) > 0 {
profile["schedule_users"] = scheduleParticipantsToInterfaceSlice(users)
}

resource, err := rs.NewGroupResource(
schedule.Name,
resourceTypeSchedule,
schedule.Id,
[]rs.GroupTraitOption{rs.WithGroupProfile(profile)},
)
if err != nil {
return nil, err
}

return resource, nil
}

func (s *scheduleResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) {
client, err := ogSchedule.NewClient(s.config)
if err != nil {
return nil, "", nil, err
}

expand := true
req := &ogSchedule.ListRequest{
BaseRequest: ogClient.BaseRequest{},
Expand: &expand,
}
schedules, err := client.List(ctx, req)
if err != nil {
return nil, "", nil, fmt.Errorf("opsgenie-connector: failed to list schedules: %w", err)
}

var rv []*v2.Resource
for _, schedule := range schedules.Schedule {
scheduleCopy := schedule

sr, err := scheduleResource(&scheduleCopy)
if err != nil {
return nil, "", nil, err
}

rv = append(rv, sr)
}

return rv, "", nil, nil
}

func (s *scheduleResourceType) Entitlements(_ context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) {
var rv []*v2.Entitlement

memberEntitlementOptions := []ent.EntitlementOption{
ent.WithGrantableTo(resourceTypeUser, resourceTypeTeam),
ent.WithDisplayName(fmt.Sprintf("%s schedule %s", resource.DisplayName, scheduleMember)),
ent.WithDescription(fmt.Sprintf("%s OpsGenie schedule %s", resource.DisplayName, scheduleMember)),
}

oncallEntitlementOptions := []ent.EntitlementOption{
ent.WithGrantableTo(resourceTypeUser),
ent.WithDisplayName(fmt.Sprintf("%s schedule %s", resource.DisplayName, scheduleOnCall)),
ent.WithDescription(fmt.Sprintf("%s OpsGenie schedule %s", resource.DisplayName, scheduleOnCall)),
}

rv = append(
rv,
ent.NewAssignmentEntitlement(resource, scheduleMember, memberEntitlementOptions...),
ent.NewAssignmentEntitlement(resource, scheduleOnCall, oncallEntitlementOptions...),
)

return rv, "", nil, nil
}

func (s *scheduleResourceType) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) {
l := ctxzap.Extract(ctx)

// parse resource profile to get schedule members (users or teams)
groupTrait, err := rs.GetGroupTrait(resource)
if err != nil {
return nil, "", nil, err
}

users, ok := getProfileStringArray(groupTrait.Profile, "schedule_users")
if !ok {
l.Info("opsgenie-connector: no users found for schedule resource")
}

teams, ok := getProfileStringArray(groupTrait.Profile, "schedule_teams")
if !ok {
l.Info("opsgenie-connector: no teams found for schedule resource")
}

var rv []*v2.Grant

// grant users and teams under schedule the member entitlement
for _, u := range users {
rv = append(rv, grant.NewGrant(
resource,
scheduleMember,
&v2.ResourceId{
ResourceType: resourceTypeUser.Id,
Resource: u,
},
))
}

for _, t := range teams {
rv = append(rv, grant.NewGrant(
resource,
scheduleMember,
&v2.ResourceId{
ResourceType: resourceTypeTeam.Id,
Resource: t,
},
grant.WithAnnotation(
&v2.GrantExpandable{
EntitlementIds: []string{fmt.Sprintf("team:%s:%s", t, scheduleMember)},
},
),
))
}

// grant users and teams under schedule the on-call entitlement
client, err := ogSchedule.NewClient(s.config)
if err != nil {
return nil, "", nil, err
}

flat := false
req := &ogSchedule.GetOnCallsRequest{
BaseRequest: ogClient.BaseRequest{},
Flat: &flat,
ScheduleIdentifier: resource.DisplayName,
}

oncalls, err := client.GetOnCalls(ctx, req)
if err != nil {
return nil, "", nil, fmt.Errorf("opsgenie-connector: failed to list on-calls: %w", err)
}

for _, p := range oncalls.OnCallParticipants {
var resourceType string
var grantOptions []grant.GrantOption

switch p.Type {
case userParticipantType:
resourceType = resourceTypeUser.Id
case teamParticipantType:
resourceType = resourceTypeTeam.Id
grantOptions = append(
grantOptions,
grant.WithAnnotation(
&v2.GrantExpandable{
EntitlementIds: []string{fmt.Sprintf("team:%s:%s", p.Id, teamMemberEntitlement)},
},
),
)
default:
return nil, "", nil, fmt.Errorf("opsgenie-connector: unknown participant type: %s", p.Type)
}

rv = append(rv, grant.NewGrant(
resource,
scheduleOnCall,
&v2.ResourceId{
ResourceType: resourceType,
Resource: p.Id,
},
grantOptions...,
))
}

return rv, "", nil, nil
}

func scheduleBuilder(config *ogClient.Config) *scheduleResourceType {
return &scheduleResourceType{
resourceType: resourceTypeSchedule,
config: config,
}
}
33 changes: 16 additions & 17 deletions pkg/connector/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,27 +101,26 @@ func (o *teamResourceType) Grants(ctx context.Context, resource *v2.Resource, pt
return nil, "", nil, err
}

teams, err := teamClient.List(ctx, &oteam.ListTeamRequest{BaseRequest: ogclient.BaseRequest{}})
getTeamRequest := &oteam.GetTeamRequest{
BaseRequest: ogclient.BaseRequest{},
IdentifierValue: resource.Id.Resource,
IdentifierType: oteam.Identifier(idIdentifierType),
}

t, err := teamClient.Get(ctx, getTeamRequest)
if err != nil {
return nil, "", nil, err
}

for _, team := range teams.Teams {
teamWithMembers, err := teamClient.Get(ctx, &oteam.GetTeamRequest{BaseRequest: ogclient.BaseRequest{}, IdentifierValue: team.Id, IdentifierType: oteam.Identifier(idIdentifierType)})
if err != nil {
return nil, "", nil, err
}

for _, member := range teamWithMembers.Members {
rv = append(rv, grant.NewGrant(
resource,
teamMemberEntitlement,
&v2.ResourceId{
ResourceType: resourceTypeUser.Id,
Resource: member.User.ID,
},
))
}
for _, member := range t.Members {
rv = append(rv, grant.NewGrant(
resource,
teamMemberEntitlement,
&v2.ResourceId{
ResourceType: resourceTypeUser.Id,
Resource: member.User.ID,
},
))
}

return rv, "", nil, nil
Expand Down
Loading

0 comments on commit 3e32284

Please sign in to comment.