2022-08-31 22:36:32 +00:00
|
|
|
// Copyright 2022 Woodpecker Authors
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
package cron
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/robfig/cron"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
2022-11-04 23:35:06 +00:00
|
|
|
"github.com/woodpecker-ci/woodpecker/server/forge"
|
2022-08-31 22:36:32 +00:00
|
|
|
"github.com/woodpecker-ci/woodpecker/server/model"
|
|
|
|
"github.com/woodpecker-ci/woodpecker/server/pipeline"
|
|
|
|
"github.com/woodpecker-ci/woodpecker/server/store"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// checkTime specifies the interval woodpecker checks for new crons to exec
|
|
|
|
checkTime = 10 * time.Second
|
|
|
|
|
|
|
|
// checkItems specifies the batch size of crons to retrieve per check from database
|
|
|
|
checkItems = 10
|
|
|
|
)
|
|
|
|
|
|
|
|
// Start starts the cron scheduler loop
|
2022-11-04 23:35:06 +00:00
|
|
|
func Start(ctx context.Context, store store.Store, forge forge.Forge) error {
|
2022-08-31 22:36:32 +00:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return nil
|
|
|
|
case <-time.After(checkTime):
|
|
|
|
go func() {
|
|
|
|
now := time.Now()
|
|
|
|
log.Trace().Msg("Cron: fetch next crons")
|
|
|
|
|
|
|
|
crons, err := store.CronListNextExecute(now.Unix(), checkItems)
|
|
|
|
if err != nil {
|
2022-11-09 07:12:17 +00:00
|
|
|
log.Error().Err(err).Int64("now", now.Unix()).Msg("obtain cron list")
|
2022-08-31 22:36:32 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, cron := range crons {
|
2022-11-04 23:35:06 +00:00
|
|
|
if err := runCron(store, forge, cron, now); err != nil {
|
2022-08-31 22:36:32 +00:00
|
|
|
log.Error().Err(err).Int64("cronID", cron.ID).Msg("run cron failed")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// CalcNewNext parses a cron string and calculates the next exec time based on it
|
|
|
|
func CalcNewNext(schedule string, now time.Time) (time.Time, error) {
|
|
|
|
// remove local timezone
|
|
|
|
now = now.UTC()
|
|
|
|
|
|
|
|
// TODO: allow the users / the admin to set a specific timezone
|
|
|
|
|
|
|
|
c, err := cron.Parse(schedule)
|
|
|
|
if err != nil {
|
2023-02-01 23:08:02 +00:00
|
|
|
return time.Time{}, fmt.Errorf("cron parse schedule: %w", err)
|
2022-08-31 22:36:32 +00:00
|
|
|
}
|
|
|
|
return c.Next(now), nil
|
|
|
|
}
|
|
|
|
|
2022-11-04 23:35:06 +00:00
|
|
|
func runCron(store store.Store, forge forge.Forge, cron *model.Cron, now time.Time) error {
|
2022-08-31 22:36:32 +00:00
|
|
|
log.Trace().Msgf("Cron: run id[%d]", cron.ID)
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
newNext, err := CalcNewNext(cron.Schedule, now)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// try to get lock on cron
|
|
|
|
gotLock, err := store.CronGetLock(cron, newNext.Unix())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !gotLock {
|
|
|
|
// another go routine caught it
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-11-04 23:35:06 +00:00
|
|
|
repo, newPipeline, err := CreatePipeline(ctx, store, forge, cron)
|
2022-08-31 22:36:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-10-25 23:23:28 +00:00
|
|
|
_, err = pipeline.Create(ctx, store, repo, newPipeline)
|
2022-08-31 22:36:32 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-11-19 20:47:47 +00:00
|
|
|
func CreatePipeline(ctx context.Context, store store.Store, f forge.Forge, cron *model.Cron) (*model.Repo, *model.Pipeline, error) {
|
2022-08-31 22:36:32 +00:00
|
|
|
repo, err := store.GetRepo(cron.RepoID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if cron.Branch == "" {
|
|
|
|
// fallback to the repos default branch
|
|
|
|
cron.Branch = repo.Branch
|
|
|
|
}
|
|
|
|
|
|
|
|
creator, err := store.GetUser(cron.CreatorID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
2022-11-19 20:47:47 +00:00
|
|
|
// if the forge has a refresh token, the current access token
|
|
|
|
// may be stale. Therefore, we should refresh prior to dispatching
|
|
|
|
// the pipeline.
|
|
|
|
if refresher, ok := f.(forge.Refresher); ok {
|
|
|
|
refreshed, err := refresher.Refresh(ctx, creator)
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msgf("failed to refresh oauth2 token for creator: %s", creator.Login)
|
|
|
|
} else if refreshed {
|
|
|
|
if err := store.UpdateUser(creator); err != nil {
|
|
|
|
log.Error().Err(err).Msgf("error while updating creator: %s", creator.Login)
|
|
|
|
// move forward
|
2023-09-16 08:53:37 +00:00
|
|
|
} else {
|
|
|
|
log.Debug().Msgf("token refreshed for creator: %s", creator.Login)
|
2022-11-19 20:47:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
commit, err := f.BranchHead(ctx, creator, repo, cron.Branch)
|
2022-08-31 22:36:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
2022-10-18 01:24:12 +00:00
|
|
|
return repo, &model.Pipeline{
|
2022-08-31 22:36:32 +00:00
|
|
|
Event: model.EventCron,
|
|
|
|
Commit: commit,
|
|
|
|
Ref: "refs/heads/" + cron.Branch,
|
|
|
|
Branch: cron.Branch,
|
|
|
|
Message: cron.Name,
|
|
|
|
Timestamp: cron.NextExec,
|
|
|
|
Sender: cron.Name,
|
|
|
|
Link: repo.Link,
|
|
|
|
}, nil
|
|
|
|
}
|