Calendar interface fixes from code review and refactoring. (#17658)

Calendar interface fixes from code review and manual merge with
@lucasmrod changes.
This commit is contained in:
Victor Lyuboslavsky 2024-03-15 10:26:58 -05:00 committed by Victor Lyuboslavsky
parent 2db8eb3c80
commit 21f95d8b5d
No known key found for this signature in database
3 changed files with 50 additions and 63 deletions

View file

@ -1,16 +0,0 @@
package calendar
import (
"context"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/go-kit/kit/log"
)
type GoogleCalendarConfig struct {
Context context.Context
IntegrationConfig *fleet.GoogleCalendarIntegration
UserEmail string
Logger log.Logger
// Should be nil for production
API GoogleCalendarAPI
}

View file

@ -5,16 +5,18 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
"google.golang.org/api/calendar/v3"
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
"net/http"
"time"
)
const (
@ -30,15 +32,34 @@ var calendarScopes = []string{
"https://www.googleapis.com/auth/calendar.settings.readonly",
}
// GoogleCalendar is an implementation of the Calendar interface that uses the
type GoogleCalendarConfig struct {
Context context.Context
IntegrationConfig *fleet.GoogleCalendarIntegration
Logger kitlog.Logger
// Should be nil for production
API GoogleCalendarAPI
}
// GoogleCalendar is an implementation of the UserCalendar interface that uses the
// Google Calendar API to manage events.
type GoogleCalendar struct {
config *GoogleCalendarConfig
timezoneOffset *int
config *GoogleCalendarConfig
currentUserEmail string
timezoneOffset *int
}
func NewGoogleCalendar(config *GoogleCalendarConfig) *GoogleCalendar {
if config.API == nil {
var lowLevelAPI GoogleCalendarAPI = &GoogleCalendarLowLevelAPI{}
config.API = lowLevelAPI
}
return &GoogleCalendar{
config: config,
}
}
type GoogleCalendarAPI interface {
Connect(ctx context.Context, email, privateKey, subject string) error
Configure(ctx context.Context, serviceAccountEmail, privateKey, userToImpersonateEmail string) error
GetSetting(name string) (*calendar.Setting, error)
ListEvents(timeMin, timeMax string) (*calendar.Events, error)
CreateEvent(event *calendar.Event) (*calendar.Event, error)
@ -55,15 +76,17 @@ type GoogleCalendarLowLevelAPI struct {
service *calendar.Service
}
// Connect creates a new Google Calendar service using the provided credentials.
func (lowLevelAPI *GoogleCalendarLowLevelAPI) Connect(ctx context.Context, email, privateKey, subject string) error {
// Configure creates a new Google Calendar service using the provided credentials.
func (lowLevelAPI *GoogleCalendarLowLevelAPI) Configure(
ctx context.Context, serviceAccountEmail, privateKey, userToImpersonateEmail string,
) error {
// Create a new calendar service
conf := &jwt.Config{
Email: email,
Email: serviceAccountEmail,
Scopes: calendarScopes,
PrivateKey: []byte(privateKey),
TokenURL: google.JWTTokenURL,
Subject: subject,
Subject: userToImpersonateEmail,
}
client := conf.Client(ctx)
service, err := calendar.NewService(ctx, option.WithHTTPClient(client))
@ -95,33 +118,18 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) DeleteEvent(id string) error {
return lowLevelAPI.service.Events.Delete(calendarID, id).Do()
}
func (c *GoogleCalendar) Connect(config any) (fleet.Calendar, error) {
gConfig, ok := config.(*GoogleCalendarConfig)
if !ok {
return nil, errors.New("invalid Google calendar config")
}
if gConfig.API == nil {
var lowLevelAPI GoogleCalendarAPI = &GoogleCalendarLowLevelAPI{}
gConfig.API = lowLevelAPI
}
err := gConfig.API.Connect(
gConfig.Context, gConfig.IntegrationConfig.Email, gConfig.IntegrationConfig.PrivateKey, gConfig.UserEmail,
func (c *GoogleCalendar) Configure(userEmail string) error {
err := c.config.API.Configure(
c.config.Context, c.config.IntegrationConfig.Email, c.config.IntegrationConfig.PrivateKey, userEmail,
)
if err != nil {
return nil, ctxerr.Wrap(gConfig.Context, err, "creating Google calendar service")
return ctxerr.Wrap(c.config.Context, err, "creating Google calendar service")
}
gCal := &GoogleCalendar{
config: gConfig,
}
return gCal, nil
c.currentUserEmail = userEmail
return nil
}
func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn func() string) (*fleet.CalendarEvent, bool, error) {
if c.config == nil {
return nil, false, errors.New("the Google calendar is not connected. Please call Connect first")
}
if event.EndTime.Before(time.Now()) {
return nil, false, ctxerr.Errorf(c.config.Context, "cannot get and update an event that has already ended: %s", event.EndTime)
}
@ -199,16 +207,11 @@ func (c *GoogleCalendar) unmarshalDetails(event *fleet.CalendarEvent) (*eventDet
if details.ID == "" {
return nil, ctxerr.Errorf(c.config.Context, "missing Google calendar event ID")
}
if details.ETag == "" {
return nil, ctxerr.Errorf(c.config.Context, "missing Google calendar event ETag")
}
// ETag is optional, but we need it to check if the event was modified
return &details, nil
}
func (c *GoogleCalendar) CreateEvent(dayOfEvent time.Time, body string) (*fleet.CalendarEvent, error) {
if c.config == nil {
return nil, errors.New("the Google calendar is not connected. Please call Connect first")
}
if c.timezoneOffset == nil {
err := getTimezone(c)
if err != nil {
@ -257,7 +260,7 @@ func (c *GoogleCalendar) CreateEvent(dayOfEvent time.Time, body string) (*fleet.
attending = true
} else {
for _, attendee := range gEvent.Attendees {
if attendee.Email == c.config.UserEmail {
if attendee.Email == c.currentUserEmail {
if attendee.ResponseStatus != "declined" {
attending = true
}
@ -316,8 +319,7 @@ func (c *GoogleCalendar) CreateEvent(dayOfEvent time.Time, body string) (*fleet.
if err != nil {
return nil, err
}
level.Debug(c.config.Logger).Log("msg", "created Google calendar events", "user", c.config.UserEmail, "startTime", eventStart)
fmt.Printf("VICTOR Created event with id:%s and ETag:%s\n", event.Id, event.Etag)
level.Debug(c.config.Logger).Log("msg", "created Google calendar event", "user", c.currentUserEmail, "startTime", eventStart)
return fleetEvent, nil
}
@ -364,7 +366,7 @@ func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime ti
fleetEvent := &fleet.CalendarEvent{}
fleetEvent.StartTime = startTime
fleetEvent.EndTime = endTime
fleetEvent.Email = c.config.UserEmail
fleetEvent.Email = c.currentUserEmail
details := &eventDetails{
ID: event.Id,
ETag: event.Etag,
@ -379,7 +381,7 @@ func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime ti
func (c *GoogleCalendar) DeleteEvent(event *fleet.CalendarEvent) error {
if c.config == nil {
return errors.New("the Google calendar is not connected. Please call Connect first")
return errors.New("the Google calendar is not connected. Please call Configure first")
}
details, err := c.unmarshalDetails(event)
if err != nil {

View file

@ -10,15 +10,16 @@ func (e DayEndedError) Error() string {
return e.Msg
}
type Calendar interface {
// Connect to calendar. This method must be called first. Currently, config must be a *GoogleCalendarConfig
Connect(config any) (Calendar, error)
type UserCalendar interface {
// Configure configures the connection to a user's calendar. Once configured,
// CreateEvent, GetAndUpdateEvent and DeleteEvent reference the user's calendar.
Configure(userEmail string) error
// CreateEvent creates a new event on the calendar on the given date. DayEndedError is returned if there is no time left on the given date to schedule event.
CreateEvent(dateOfEvent time.Time, body string) (event *CalendarEvent, err error)
// GetAndUpdateEvent retrieves the event from the calendar.
// If the event has been modified, it returns the updated event.
// If the event has been deleted, it schedules a new event with given body callback and returns the new event.
GetAndUpdateEvent(event *CalendarEvent, genBodyFn func() string) (updatedEvent *CalendarEvent, updated bool, err error)
// CreateEvent creates a new event on the calendar on the given date. DayEndedError is returned if there is no time left on the given date to schedule event.
CreateEvent(dateOfEvent time.Time, body string) (event *CalendarEvent, err error)
// DeleteEvent deletes the event with the given ID.
DeleteEvent(event *CalendarEvent) error
}