Skip to content

Commit

Permalink
feat: add support for schedules (#289)
Browse files Browse the repository at this point in the history
* feat(constants): add schedule event type

* chore: alphanumeric sort events

* chore: address linter feedback

* chore(pipeline): add tests cases for schedule event

* chore(yaml): add tests cases for schedule event

* feat(database): add schedule type

* feat(library): add schedule type

* chore: update go dependencies

* chore: update constants
  • Loading branch information
jbrockopp committed May 16, 2023
1 parent 1203118 commit f538de0
Show file tree
Hide file tree
Showing 13 changed files with 817 additions and 17 deletions.
25 changes: 14 additions & 11 deletions constants/event.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
// Copyright (c) 2022 Target Brands, Inc. All rights reserved.
// Copyright (c) 2023 Target Brands, Inc. All rights reserved.
//
// Use of this source code is governed by the LICENSE file in this repository.

package constants

// Build and repo events.
const (
// EventPush defines the event type for build and repo push events.
EventPush = "push"

// EventPull defines the event type for build and repo pull_request events.
EventPull = "pull_request"

// EventTag defines the event type for build and repo tag events.
EventTag = "tag"
// EventComment defines the event type for comments added to a pull request.
EventComment = "comment"

// EventDeploy defines the event type for build and repo deployment events.
EventDeploy = "deployment"

// EventComment defines the event type for comments added to a pull request.
EventComment = "comment"
// EventPull defines the event type for build and repo pull_request events.
EventPull = "pull_request"

// EventPush defines the event type for build and repo push events.
EventPush = "push"

// EventRepository defines the general event type for repo management.
EventRepository = "repository"

// EventSchedule defines the event type for build and repo schedule events.
EventSchedule = "schedule"

// EventTag defines the event type for build and repo tag events.
EventTag = "tag"
)
3 changes: 3 additions & 0 deletions constants/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const (
// TableRepo defines the table type for the database repos table.
TableRepo = "repos"

// TableSchedule defines the table type for the database schedules table.
TableSchedule = "schedules"

// TableSecret defines the table type for the database secrets table.
TableSecret = "secrets"

Expand Down
137 changes: 137 additions & 0 deletions database/schedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) 2023 Target Brands, Inc. All rights reserved.
//
// Use of this source code is governed by the LICENSE file in this repository.

package database

import (
"database/sql"
"errors"

"github.com/adhocore/gronx"
"github.com/go-vela/types/library"
)

var (
// ErrEmptyScheduleEntry defines the error type when a Schedule type has an empty Entry field provided.
ErrEmptyScheduleEntry = errors.New("empty schedule entry provided")

// ErrEmptyScheduleName defines the error type when a Schedule type has an empty Name field provided.
ErrEmptyScheduleName = errors.New("empty schedule name provided")

// ErrEmptyScheduleRepoID defines the error type when a Schedule type has an empty RepoID field provided.
ErrEmptyScheduleRepoID = errors.New("empty schedule repo_id provided")

// ErrInvalidScheduleEntry defines the error type when a Schedule type has an invalid Entry field provided.
ErrInvalidScheduleEntry = errors.New("invalid schedule entry provided")
)

// Schedule is the database representation of a schedule for a repo.
type Schedule struct {
ID sql.NullInt64 `sql:"id"`
RepoID sql.NullInt64 `sql:"repo_id"`
Active sql.NullBool `sql:"active"`
Name sql.NullString `sql:"name"`
Entry sql.NullString `sql:"entry"`
CreatedAt sql.NullInt64 `sql:"created_at"`
CreatedBy sql.NullString `sql:"created_by"`
UpdatedAt sql.NullInt64 `sql:"updated_at"`
UpdatedBy sql.NullString `sql:"updated_by"`
ScheduledAt sql.NullInt64 `sql:"scheduled_at"`
}

// ScheduleFromLibrary converts the library.Schedule type to a database Schedule type.
func ScheduleFromLibrary(s *library.Schedule) *Schedule {
schedule := &Schedule{
ID: sql.NullInt64{Int64: s.GetID(), Valid: true},
RepoID: sql.NullInt64{Int64: s.GetRepoID(), Valid: true},
Active: sql.NullBool{Bool: s.GetActive(), Valid: true},
Name: sql.NullString{String: s.GetName(), Valid: true},
Entry: sql.NullString{String: s.GetEntry(), Valid: true},
CreatedAt: sql.NullInt64{Int64: s.GetCreatedAt(), Valid: true},
CreatedBy: sql.NullString{String: s.GetCreatedBy(), Valid: true},
UpdatedAt: sql.NullInt64{Int64: s.GetUpdatedAt(), Valid: true},
UpdatedBy: sql.NullString{String: s.GetUpdatedBy(), Valid: true},
ScheduledAt: sql.NullInt64{Int64: s.GetScheduledAt(), Valid: true},
}

return schedule.Nullify()
}

// Nullify ensures the valid flag for the sql.Null types are properly set.
//
// When a field within the Schedule type is the zero value for the field, the
// valid flag is set to false causing it to be NULL in the database.
func (s *Schedule) Nullify() *Schedule {
if s == nil {
return nil
}

// check if the ID field should be valid
s.ID.Valid = s.ID.Int64 != 0
// check if the RepoID field should be valid
s.RepoID.Valid = s.RepoID.Int64 != 0
// check if the ID field should be valid
s.Active.Valid = s.RepoID.Int64 != 0
// check if the Name field should be valid
s.Name.Valid = len(s.Name.String) != 0
// check if the Entry field should be valid
s.Entry.Valid = len(s.Entry.String) != 0
// check if the CreatedAt field should be valid
s.CreatedAt.Valid = s.CreatedAt.Int64 != 0
// check if the CreatedBy field should be valid
s.CreatedBy.Valid = len(s.CreatedBy.String) != 0
// check if the UpdatedAt field should be valid
s.UpdatedAt.Valid = s.UpdatedAt.Int64 != 0
// check if the UpdatedBy field should be valid
s.UpdatedBy.Valid = len(s.UpdatedBy.String) != 0
// check if the ScheduledAt field should be valid
s.ScheduledAt.Valid = s.ScheduledAt.Int64 != 0

return s
}

// ToLibrary converts the Schedule type to a library.Schedule type.
func (s *Schedule) ToLibrary() *library.Schedule {
return &library.Schedule{
ID: &s.ID.Int64,
RepoID: &s.RepoID.Int64,
Active: &s.Active.Bool,
Name: &s.Name.String,
Entry: &s.Entry.String,
CreatedAt: &s.CreatedAt.Int64,
CreatedBy: &s.CreatedBy.String,
UpdatedAt: &s.UpdatedAt.Int64,
UpdatedBy: &s.UpdatedBy.String,
ScheduledAt: &s.ScheduledAt.Int64,
}
}

// Validate verifies the necessary fields for the Schedule type are populated correctly.
func (s *Schedule) Validate() error {
// verify the RepoID field is populated
if s.RepoID.Int64 <= 0 {
return ErrEmptyScheduleRepoID
}

// verify the Name field is populated
if len(s.Name.String) <= 0 {
return ErrEmptyScheduleName
}

// verify the Entry field is populated
if len(s.Entry.String) <= 0 {
return ErrEmptyScheduleEntry
}

gron := gronx.New()
if !gron.IsValid(s.Entry.String) {
return ErrInvalidScheduleEntry
}

// ensure that all Schedule string fields that can be returned as JSON are sanitized to avoid unsafe HTML content
s.Name = sql.NullString{String: sanitize(s.Name.String), Valid: s.Name.Valid}
s.Entry = sql.NullString{String: sanitize(s.Entry.String), Valid: s.Entry.Valid}

return nil
}
180 changes: 180 additions & 0 deletions database/schedule_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright (c) 2023 Target Brands, Inc. All rights reserved.
//
// Use of this source code is governed by the LICENSE file in this repository.

package database

import (
"database/sql"
"reflect"
"testing"
"time"

"github.com/go-vela/types/library"
)

func TestDatabase_ScheduleFromLibrary(t *testing.T) {
s := new(library.Schedule)
s.SetID(1)
s.SetRepoID(1)
s.SetActive(true)
s.SetName("nightly")
s.SetEntry("0 0 * * *")
s.SetCreatedAt(time.Now().UTC().Unix())
s.SetCreatedBy("user1")
s.SetUpdatedAt(time.Now().Add(time.Hour * 1).UTC().Unix())
s.SetUpdatedBy("user2")
s.SetScheduledAt(time.Now().Add(time.Hour * 2).UTC().Unix())

want := testSchedule()

got := ScheduleFromLibrary(s)
if !reflect.DeepEqual(got, want) {
t.Errorf("ScheduleFromAPI is %v, want %v", got, want)
}
}

func TestDatabase_Schedule_Nullify(t *testing.T) {
tests := []struct {
name string
schedule *Schedule
want *Schedule
}{
{
name: "schedule with fields",
schedule: testSchedule(),
want: testSchedule(),
},
{
name: "schedule with empty fields",
schedule: new(Schedule),
want: &Schedule{
ID: sql.NullInt64{Int64: 0, Valid: false},
RepoID: sql.NullInt64{Int64: 0, Valid: false},
Active: sql.NullBool{Bool: false, Valid: false},
Name: sql.NullString{String: "", Valid: false},
Entry: sql.NullString{String: "", Valid: false},
CreatedAt: sql.NullInt64{Int64: 0, Valid: false},
CreatedBy: sql.NullString{String: "", Valid: false},
UpdatedAt: sql.NullInt64{Int64: 0, Valid: false},
UpdatedBy: sql.NullString{String: "", Valid: false},
ScheduledAt: sql.NullInt64{Int64: 0, Valid: false},
},
},
{
name: "empty schedule",
schedule: nil,
want: nil,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := test.schedule.Nullify()
if !reflect.DeepEqual(got, test.want) {
t.Errorf("Nullify is %v, want %v", got, test.want)
}
})
}
}

func TestDatabase_Schedule_ToLibrary(t *testing.T) {
want := new(library.Schedule)
want.SetID(1)
want.SetRepoID(1)
want.SetActive(true)
want.SetName("nightly")
want.SetEntry("0 0 * * *")
want.SetCreatedAt(time.Now().UTC().Unix())
want.SetCreatedBy("user1")
want.SetUpdatedAt(time.Now().Add(time.Hour * 1).UTC().Unix())
want.SetUpdatedBy("user2")
want.SetScheduledAt(time.Now().Add(time.Hour * 2).UTC().Unix())

got := testSchedule().ToLibrary()
if !reflect.DeepEqual(got, want) {
t.Errorf("ToLibrary is %v, want %v", got, want)
}
}

func TestDatabase_Schedule_Validate(t *testing.T) {
tests := []struct {
name string
failure bool
schedule *Schedule
}{
{
name: "schedule with valid fields",
failure: false,
schedule: testSchedule(),
},
{
name: "schedule with invalid entry",
failure: true,
schedule: &Schedule{
ID: sql.NullInt64{Int64: 1, Valid: true},
RepoID: sql.NullInt64{Int64: 1, Valid: true},
Name: sql.NullString{String: "invalid", Valid: false},
Entry: sql.NullString{String: "!@#$%^&*()", Valid: false},
},
},
{
name: "schedule with missing entry",
failure: true,
schedule: &Schedule{
ID: sql.NullInt64{Int64: 1, Valid: true},
RepoID: sql.NullInt64{Int64: 1, Valid: true},
Name: sql.NullString{String: "nightly", Valid: false},
},
},
{
name: "schedule with missing name",
failure: true,
schedule: &Schedule{
ID: sql.NullInt64{Int64: 1, Valid: true},
RepoID: sql.NullInt64{Int64: 1, Valid: true},
Entry: sql.NullString{String: "0 0 * * *", Valid: false},
},
},
{
name: "schedule with missing repo_id",
failure: true,
schedule: &Schedule{
ID: sql.NullInt64{Int64: 1, Valid: true},
Name: sql.NullString{String: "nightly", Valid: false},
Entry: sql.NullString{String: "0 0 * * *", Valid: false},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.schedule.Validate()
if test.failure {
if err == nil {
t.Errorf("Validate should have returned err")
}
return
}
if err != nil {
t.Errorf("Validate returned err: %v", err)
}
})
}
}

// testSchedule is a test helper function to create a Schedule type with all fields set to a fake value.
func testSchedule() *Schedule {
return &Schedule{
ID: sql.NullInt64{Int64: 1, Valid: true},
RepoID: sql.NullInt64{Int64: 1, Valid: true},
Active: sql.NullBool{Bool: true, Valid: true},
Name: sql.NullString{String: "nightly", Valid: true},
Entry: sql.NullString{String: "0 0 * * *", Valid: true},
CreatedAt: sql.NullInt64{Int64: time.Now().UTC().Unix(), Valid: true},
CreatedBy: sql.NullString{String: "user1", Valid: true},
UpdatedAt: sql.NullInt64{Int64: time.Now().Add(time.Hour * 1).UTC().Unix(), Valid: true},
UpdatedBy: sql.NullString{String: "user2", Valid: true},
ScheduledAt: sql.NullInt64{Int64: time.Now().Add(time.Hour * 2).UTC().Unix(), Valid: true},
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/go-vela/types
go 1.19

require (
github.com/adhocore/gronx v1.6.2
github.com/buildkite/yaml v0.0.0-20181016232759-0caa5f0796e3
github.com/drone/envsubst v1.0.3
github.com/ghodss/yaml v1.0.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/adhocore/gronx v1.6.2 h1:/Pg6cuHFJmUGRIYWhRFjb6iL9fdzNmoMPj+/r6L01KU=
github.com/adhocore/gronx v1.6.2/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/buildkite/yaml v0.0.0-20181016232759-0caa5f0796e3 h1:q+sMKdA6L8LyGVudTkpGoC73h6ak2iWSPFiFo/pFOU8=
Expand Down
Loading

0 comments on commit f538de0

Please sign in to comment.