diff --git a/cli/repo/cron/cron_add.go b/cli/repo/cron/cron_add.go index df86ef483c6..2884181f27d 100644 --- a/cli/repo/cron/cron_add.go +++ b/cli/repo/cron/cron_add.go @@ -47,6 +47,11 @@ var cronCreateCmd = &cli.Command{ Usage: "cron schedule", Required: true, }, + &cli.BoolFlag{ + Name: "enabled", + Usage: "whether cron is enabled", + Value: true, + }, common.FormatFlag(tmplCronList, true), }, } @@ -58,6 +63,7 @@ func cronCreate(ctx context.Context, c *cli.Command) error { schedule = c.String("schedule") repoIDOrFullName = c.String("repository") format = c.String("format") + "\n" + enabled = c.Bool("enabled") ) if repoIDOrFullName == "" { repoIDOrFullName = c.Args().First() @@ -77,6 +83,7 @@ func cronCreate(ctx context.Context, c *cli.Command) error { Name: cronName, Branch: branch, Schedule: schedule, + Enabled: enabled, } cron, err = client.CronCreate(repoID, cron) if err != nil { diff --git a/cli/repo/cron/cron_update.go b/cli/repo/cron/cron_update.go index 323893027fa..bd1678f2e6c 100644 --- a/cli/repo/cron/cron_update.go +++ b/cli/repo/cron/cron_update.go @@ -50,6 +50,11 @@ var cronUpdateCmd = &cli.Command{ Name: "schedule", Usage: "cron schedule", }, + &cli.BoolFlag{ + Name: "enabled", + Usage: "whether cron is enabled", + Value: true, + }, common.FormatFlag(tmplCronList, true), }, } @@ -62,6 +67,7 @@ func cronUpdate(ctx context.Context, c *cli.Command) error { branch = c.String("branch") schedule = c.String("schedule") format = c.String("format") + "\n" + enabled = c.Bool("enabled") ) if repoIDOrFullName == "" { repoIDOrFullName = c.Args().First() @@ -79,6 +85,7 @@ func cronUpdate(ctx context.Context, c *cli.Command) error { Name: jobName, Branch: branch, Schedule: schedule, + Enabled: enabled, } cron, err = client.CronUpdate(repoID, cron) if err != nil { diff --git a/server/api/cron.go b/server/api/cron.go index 119b22d368b..f982832a3d4 100644 --- a/server/api/cron.go +++ b/server/api/cron.go @@ -15,6 +15,7 @@ package api import ( + "errors" "net/http" "strconv" "time" @@ -28,6 +29,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v3/server/pipeline" "go.woodpecker-ci.org/woodpecker/v3/server/router/middleware/session" "go.woodpecker-ci.org/woodpecker/v3/server/store" + "go.woodpecker-ci.org/woodpecker/v3/server/store/types" ) // GetCron @@ -153,7 +155,11 @@ func PostCron(c *gin.Context) { } if err := _store.CronCreate(cron); err != nil { - c.String(http.StatusInternalServerError, "Error inserting cron %q. %s", in.Name, err) + if errors.Is(err, types.ErrUniqueExists) { + c.String(http.StatusConflict, "cron with this exists for this repo already") + } else { + c.String(http.StatusInternalServerError, "Error inserting cron %q. %s", in.Name, err) + } return } c.JSON(http.StatusOK, cron) diff --git a/server/store/datastore/cron.go b/server/store/datastore/cron.go index 85530036f1c..a27a295af1a 100644 --- a/server/store/datastore/cron.go +++ b/server/store/datastore/cron.go @@ -15,9 +15,12 @@ package datastore import ( + "strings" + "xorm.io/builder" "go.woodpecker-ci.org/woodpecker/v3/server/model" + "go.woodpecker-ci.org/woodpecker/v3/server/store/types" ) func (s storage) CronCreate(cron *model.Cron) error { @@ -25,6 +28,9 @@ func (s storage) CronCreate(cron *model.Cron) error { return err } _, err := s.engine.Insert(cron) + if err != nil && (strings.HasPrefix(err.Error(), "UNIQUE constraint failed") || strings.HasPrefix(err.Error(), "pq: duplicate key value violates unique constraint") || strings.Contains(err.Error(), "Duplicate entry")) { + return types.ErrUniqueExists + } return err } diff --git a/server/store/datastore/cron_test.go b/server/store/datastore/cron_test.go index 289c57a27be..bfe66219bb2 100644 --- a/server/store/datastore/cron_test.go +++ b/server/store/datastore/cron_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" "go.woodpecker-ci.org/woodpecker/v3/server/model" + "go.woodpecker-ci.org/woodpecker/v3/server/store/types" ) func TestCronCreate(t *testing.T) { @@ -33,7 +34,7 @@ func TestCronCreate(t *testing.T) { assert.NotEqualValues(t, 0, cron1.ID) // cannot insert cron job with same repoID and title - assert.Error(t, store.CronCreate(cron1)) + assert.ErrorIs(t, types.ErrUniqueExists, store.CronCreate(cron1)) oldID := cron1.ID assert.NoError(t, store.CronDelete(repo, oldID)) diff --git a/server/store/mocks/mock_Store.go b/server/store/mocks/mock_Store.go index 7a0a6ba7a54..629bdb2e0dc 100644 --- a/server/store/mocks/mock_Store.go +++ b/server/store/mocks/mock_Store.go @@ -940,6 +940,72 @@ func (_c *MockStore_CronDelete_Call) RunAndReturn(run func(repo *model.Repo, n i return _c } +// CronExists provides a mock function for the type MockStore +func (_mock *MockStore) CronExists(repo *model.Repo, s string) (bool, error) { + ret := _mock.Called(repo, s) + + if len(ret) == 0 { + panic("no return value specified for CronExists") + } + + var r0 bool + var r1 error + if returnFunc, ok := ret.Get(0).(func(*model.Repo, string) (bool, error)); ok { + return returnFunc(repo, s) + } + if returnFunc, ok := ret.Get(0).(func(*model.Repo, string) bool); ok { + r0 = returnFunc(repo, s) + } else { + r0 = ret.Get(0).(bool) + } + if returnFunc, ok := ret.Get(1).(func(*model.Repo, string) error); ok { + r1 = returnFunc(repo, s) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockStore_CronExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CronExists' +type MockStore_CronExists_Call struct { + *mock.Call +} + +// CronExists is a helper method to define mock.On call +// - repo *model.Repo +// - s string +func (_e *MockStore_Expecter) CronExists(repo interface{}, s interface{}) *MockStore_CronExists_Call { + return &MockStore_CronExists_Call{Call: _e.mock.On("CronExists", repo, s)} +} + +func (_c *MockStore_CronExists_Call) Run(run func(repo *model.Repo, s string)) *MockStore_CronExists_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 *model.Repo + if args[0] != nil { + arg0 = args[0].(*model.Repo) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockStore_CronExists_Call) Return(b bool, err error) *MockStore_CronExists_Call { + _c.Call.Return(b, err) + return _c +} + +func (_c *MockStore_CronExists_Call) RunAndReturn(run func(repo *model.Repo, s string) (bool, error)) *MockStore_CronExists_Call { + _c.Call.Return(run) + return _c +} + // CronFind provides a mock function for the type MockStore func (_mock *MockStore) CronFind(repo *model.Repo, n int64) (*model.Cron, error) { ret := _mock.Called(repo, n) diff --git a/server/store/types/errors.go b/server/store/types/errors.go index c7901a1c6ed..21efb406a75 100644 --- a/server/store/types/errors.go +++ b/server/store/types/errors.go @@ -14,6 +14,11 @@ package types -import "database/sql" +import ( + "database/sql" + "errors" +) var RecordNotExist = sql.ErrNoRows + +var ErrUniqueExists = errors.New("unique constraint failed") diff --git a/woodpecker-go/woodpecker/types.go b/woodpecker-go/woodpecker/types.go index d9a2a8dd4f0..7fa65c1de4a 100644 --- a/woodpecker-go/woodpecker/types.go +++ b/woodpecker-go/woodpecker/types.go @@ -257,6 +257,7 @@ type ( Schedule string `json:"schedule"` Created int64 `json:"created"` Branch string `json:"branch"` + Enabled bool `json:"enabled"` } // PipelineOptions is the JSON data for creating a new pipeline.