From c15cc8aeca1726e0e088592dc9bc8f14de732e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Tue, 25 Jul 2023 22:48:41 +0200 Subject: [PATCH 01/22] feat: trigger on deploy status and periodically --- go.mod | 1 + go.sum | 2 + pkg/apis/lighthouse/v1alpha1/types.go | 2 +- pkg/config/config.go | 16 ++- pkg/config/job/config.go | 3 +- pkg/config/job/deployment.go | 13 ++ pkg/config/job/periodic.go | 5 +- pkg/config/job/pipelinekind.go | 2 + pkg/jobutil/jobutil.go | 14 +- pkg/plugins/plugin.go | 21 +-- pkg/plugins/plugins.go | 2 + pkg/plugins/trigger/deployment.go | 39 ++++++ pkg/plugins/trigger/periodic.go | 157 ++++++++++++++++++++++ pkg/plugins/trigger/trigger.go | 11 +- pkg/triggerconfig/inrepo/load_triggers.go | 71 ++++++++++ pkg/triggerconfig/merge/combine.go | 6 + pkg/triggerconfig/merge/merge.go | 29 ++++ pkg/triggerconfig/types.go | 4 + pkg/webhook/events.go | 67 +++++++++ pkg/webhook/webhook.go | 131 ++++++++++-------- 20 files changed, 517 insertions(+), 79 deletions(-) create mode 100644 pkg/config/job/deployment.go create mode 100644 pkg/plugins/trigger/deployment.go create mode 100644 pkg/plugins/trigger/periodic.go diff --git a/go.mod b/go.mod index 7e1e8f5da..f833230e0 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 github.com/tektoncd/pipeline v0.41.0 + golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 golang.org/x/oauth2 v0.15.0 gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5 k8s.io/api v0.25.9 diff --git a/go.sum b/go.sum index 3aaf6a57c..7235ec0d1 100644 --- a/go.sum +++ b/go.sum @@ -420,6 +420,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/pkg/apis/lighthouse/v1alpha1/types.go b/pkg/apis/lighthouse/v1alpha1/types.go index 376d9544b..7e5ee4da4 100644 --- a/pkg/apis/lighthouse/v1alpha1/types.go +++ b/pkg/apis/lighthouse/v1alpha1/types.go @@ -203,7 +203,7 @@ func (s *LighthouseJobSpec) GetEnvVars() map[string]string { env[PullRefsEnv] = s.Refs.String() } - if s.Type == job.PostsubmitJob || s.Type == job.BatchJob { + if s.Type != job.PresubmitJob { return env } diff --git a/pkg/config/config.go b/pkg/config/config.go index 9254778e7..f4536b779 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -251,6 +251,16 @@ func (c *Config) GetPostsubmits(repository scm.Repository) []job.Postsubmit { return answer } +// GetDeployments lets return all the deployments +func (c *Config) GetDeployments(repository scm.Repository) []job.Deployment { + fullNames := util.FullNames(repository) + var answer []job.Deployment + for _, fn := range fullNames { + answer = append(answer, c.Deployments[fn]...) + } + return answer +} + // GetPresubmits lets return all the pre submits for the given repo func (c *Config) GetPresubmits(repository scm.Repository) []job.Presubmit { fullNames := util.FullNames(repository) @@ -262,9 +272,9 @@ func (c *Config) GetPresubmits(repository scm.Repository) []job.Presubmit { } // BranchRequirements partitions status contexts for a given org, repo branch into three buckets: -// - contexts that are always required to be present -// - contexts that are required, _if_ present -// - contexts that are always optional +// - contexts that are always required to be present +// - contexts that are required, _if_ present +// - contexts that are always optional func BranchRequirements(org, repo, branch string, presubmits map[string][]job.Presubmit) ([]string, []string, []string) { jobs, ok := presubmits[org+"/"+repo] if !ok { diff --git a/pkg/config/job/config.go b/pkg/config/job/config.go index 27e3a3b86..7f4191779 100644 --- a/pkg/config/job/config.go +++ b/pkg/config/job/config.go @@ -33,7 +33,8 @@ type Config struct { Presubmits map[string][]Presubmit `json:"presubmits,omitempty"` Postsubmits map[string][]Postsubmit `json:"postsubmits,omitempty"` // Periodics are not associated with any repo. - Periodics []Periodic `json:"periodics,omitempty"` + Periodics []Periodic `json:"periodics,omitempty"` + Deployments map[string][]Deployment `json:"deployments,omitempty"` } func resolvePresets(name string, labels map[string]string, spec *v1.PodSpec, presets []Preset) error { diff --git a/pkg/config/job/deployment.go b/pkg/config/job/deployment.go new file mode 100644 index 000000000..b4a8787ea --- /dev/null +++ b/pkg/config/job/deployment.go @@ -0,0 +1,13 @@ +package job + +type Deployment struct { + Base + Reporter + // The deployment state that trigger this pipeline + // Can be one of: error, failure, inactive, in_progress, queued, pending, success + // If not set all deployment state event triggers + State string `json:"state,omitempty"` + // Deployment for this environment trigger this pipeline + // If not set deployments for all environments trigger + Environment string `json:"environment,omitempty"` +} diff --git a/pkg/config/job/periodic.go b/pkg/config/job/periodic.go index 3dd2199eb..f5bc496d1 100644 --- a/pkg/config/job/periodic.go +++ b/pkg/config/job/periodic.go @@ -19,10 +19,11 @@ package job // Periodic runs on a timer. type Periodic struct { Base + Reporter + // The branch to build + Branch string `json:"branch"` // Cron representation of job trigger time Cron string `json:"cron"` - // Tags for config entries - Tags []string `json:"tags,omitempty"` } // SetDefaults initializes default values diff --git a/pkg/config/job/pipelinekind.go b/pkg/config/job/pipelinekind.go index 306dddb5e..c70681034 100644 --- a/pkg/config/job/pipelinekind.go +++ b/pkg/config/job/pipelinekind.go @@ -35,6 +35,8 @@ const ( PostsubmitJob PipelineKind = "postsubmit" // Periodic job means it runs on a time-basis, unrelated to git changes. PeriodicJob PipelineKind = "periodic" + // Deployment job means it runs on deployment status event + DeploymentJob PipelineKind = "deployment" // BatchJob tests multiple unmerged PRs at the same time. BatchJob PipelineKind = "batch" ) diff --git a/pkg/jobutil/jobutil.go b/pkg/jobutil/jobutil.go index 466d1fbce..1fa92ffdd 100644 --- a/pkg/jobutil/jobutil.go +++ b/pkg/jobutil/jobutil.go @@ -143,10 +143,22 @@ func PostsubmitSpec(logger *logrus.Entry, p job.Postsubmit, refs v1alpha1.Refs) return pjs } +// DeploymentSpec initializes a PipelineOptionsSpec for a given deployment job. +func DeploymentSpec(logger *logrus.Entry, p job.Deployment, refs v1alpha1.Refs) v1alpha1.LighthouseJobSpec { + pjs := specFromJobBase(logger, p.Base) + pjs.Type = job.DeploymentJob + pjs.Context = p.Context + pjs.Refs = completePrimaryRefs(refs, p.Base) + + return pjs +} + // PeriodicSpec initializes a PipelineOptionsSpec for a given periodic job. -func PeriodicSpec(logger *logrus.Entry, p job.Periodic) v1alpha1.LighthouseJobSpec { +func PeriodicSpec(logger *logrus.Entry, p job.Periodic, refs v1alpha1.Refs) v1alpha1.LighthouseJobSpec { pjs := specFromJobBase(logger, p.Base) pjs.Type = job.PeriodicJob + pjs.Context = p.Context + pjs.Refs = completePrimaryRefs(refs, p.Base) return pjs } diff --git a/pkg/plugins/plugin.go b/pkg/plugins/plugin.go index 9bbdbc95c..b4b248ae2 100644 --- a/pkg/plugins/plugin.go +++ b/pkg/plugins/plugin.go @@ -8,16 +8,17 @@ import ( // Plugin defines a plugin and its handlers type Plugin struct { - Description string - ExcludedProviders sets.String - ConfigHelpProvider ConfigHelpProvider - IssueHandler IssueHandler - PullRequestHandler PullRequestHandler - PushEventHandler PushEventHandler - ReviewEventHandler ReviewEventHandler - StatusEventHandler StatusEventHandler - GenericCommentHandler GenericCommentHandler - Commands []Command + Description string + ExcludedProviders sets.String + ConfigHelpProvider ConfigHelpProvider + IssueHandler IssueHandler + PullRequestHandler PullRequestHandler + PushEventHandler PushEventHandler + ReviewEventHandler ReviewEventHandler + StatusEventHandler StatusEventHandler + DeploymentStatusHandler DeploymentStatusHandler + GenericCommentHandler GenericCommentHandler + Commands []Command } // InvokeCommandHandler calls InvokeHandler on all commands diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index b383b0e18..d2c7d0cca 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -63,6 +63,8 @@ type PullRequestHandler func(Agent, scm.PullRequestHook) error // StatusEventHandler defines the function contract for a scm.Status handler. type StatusEventHandler func(Agent, scm.Status) error +type DeploymentStatusHandler func(Agent, scm.DeploymentStatusHook) error + // PushEventHandler defines the function contract for a scm.PushHook handler. type PushEventHandler func(Agent, scm.PushHook) error diff --git a/pkg/plugins/trigger/deployment.go b/pkg/plugins/trigger/deployment.go new file mode 100644 index 000000000..0d15647dd --- /dev/null +++ b/pkg/plugins/trigger/deployment.go @@ -0,0 +1,39 @@ +package trigger + +import ( + "github.com/jenkins-x/go-scm/scm" + "github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1" + "github.com/jenkins-x/lighthouse/pkg/jobutil" + "github.com/jenkins-x/lighthouse/pkg/scmprovider" +) + +func handleDeployment(c Client, ds scm.DeploymentStatusHook) error { + for _, j := range c.Config.GetDeployments(ds.Repo) { + if j.State != "" && j.State != ds.DeploymentStatus.State { + continue + } + if j.Environment != "" && j.Environment != ds.Deployment.Environment { + continue + } + labels := make(map[string]string) + for k, v := range j.Labels { + labels[k] = v + } + refs := v1alpha1.Refs{ + Org: ds.Repo.Namespace, + Repo: ds.Repo.Name, + BaseRef: ds.Deployment.Ref, + BaseSHA: ds.Deployment.Sha, + BaseLink: ds.Deployment.RepositoryLink, + CloneURI: ds.Repo.Clone, + } + labels[scmprovider.EventGUID] = ds.DeploymentStatus.ID + pj := jobutil.NewLighthouseJob(jobutil.DeploymentSpec(c.Logger, j, refs), labels, j.Annotations) + c.Logger.WithFields(jobutil.LighthouseJobFields(&pj)).Info("Creating a new LighthouseJob.") + if _, err := c.LauncherClient.Launch(&pj); err != nil { + return err + } + + } + return nil +} diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go new file mode 100644 index 000000000..b58dbb681 --- /dev/null +++ b/pkg/plugins/trigger/periodic.go @@ -0,0 +1,157 @@ +package trigger + +import ( + "regexp" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/jenkins-x/go-scm/scm" + "github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1" + "github.com/jenkins-x/lighthouse/pkg/config" + "github.com/jenkins-x/lighthouse/pkg/config/job" + "github.com/jenkins-x/lighthouse/pkg/filebrowser" + "github.com/jenkins-x/lighthouse/pkg/jobutil" + "github.com/jenkins-x/lighthouse/pkg/plugins" + "github.com/jenkins-x/lighthouse/pkg/scmprovider" + "github.com/jenkins-x/lighthouse/pkg/triggerconfig/inrepo" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" + "gopkg.in/robfig/cron.v2" +) + +type PeriodicAgent struct { + Cron *cron.Cron + Periodics map[string]map[string]PeriodicExec +} + +func (p PeriodicAgent) UpdatePeriodics(org string, repo string, agent plugins.Agent, pe *scm.PushHook) { + fullName := org + "/" + repo + // FIXME Here is a race condition with StartPeriodics so that if StartPeriodics and UpdatePeriodics update + // for a repo at the same time a periodic could be scheduled multiple times + // Another potential cause for duplicate jobs is that multiple lighthouse webhook processes could run at the same time + // StartPeriodics is likely so slow though that the risk for missed jobs are greater though + // Ideally an external lock should be used to synchronise, but it is unlikely to work well. It would probably be better to use something external + // Maybe integrate with Tekton Triggers, but that would mean another moving part... + // https://github.com/tektoncd/triggers/tree/main/examples/v1beta1/cron + // Probably better then to just create CronJobs that create LighthouseJobs/PipelineRuns using kubectl/tkn. + // Possibly with the Pipeline stored as separate resource and then either have the LighthouseJob refer to it or do tkn pipeline start. + // With proper labels these CronJobs/Pipelines could be handled fairly efficiently + repoPeriodics := maps.Clone(p.Periodics[fullName]) + if repoPeriodics == nil { + repoPeriodics = make(map[string]PeriodicExec) + p.Periodics[fullName] = repoPeriodics + } + l := logrus.WithField(scmprovider.RepoLogField, repo).WithField(scmprovider.OrgLogField, org) + for _, periodic := range agent.Config.Periodics { + exec, exists := repoPeriodics[periodic.Name] + if !exists || hasChanged(exec.Periodic, periodic, pe, l) { + // New or changed. Changed will have old entry removed below + addPeriodic(l, periodic, org, repo, agent.LauncherClient, p.Cron, fullName, p.Periodics[fullName]) + } else { + // Nothing to change + delete(repoPeriodics, periodic.Name) + } + } + // Deschedule periodic no longer found in repo + for _, periodic := range repoPeriodics { + p.Cron.Remove(periodic.EntryID) + } +} + +// hasChanged return true if any fields have changed except pipelineLoader and PipelineRunSpec since lazyLoading means you can't compare these +// Also check if the file pointed to by SourcePath has changed +func hasChanged(existing, imported job.Periodic, pe *scm.PushHook, l *logrus.Entry) bool { + if !cmp.Equal(existing, imported, cmpopts.IgnoreFields(job.Periodic{}, "pipelineLoader", "PipelineRunSpec")) { + return true + } + // Since We don't know which directory SourcePath is relative to we check for any file with that name + changeMatcher := job.RegexpChangeMatcher{RunIfChanged: regexp.QuoteMeta(existing.SourcePath)} + _, run, err := changeMatcher.ShouldRun(listPushEventChanges(*pe)) + if err != nil { + l.WithError(err).Warnf("Can't determine if %s has changed, assumes it hasn't", existing.SourcePath) + } + return run +} + +type PeriodicExec struct { + job.Periodic + Owner, Repo string + LauncherClient launcher + EntryID cron.EntryID +} + +func (p *PeriodicExec) Run() { + labels := make(map[string]string) + for k, v := range p.Labels { + labels[k] = v + } + refs := v1alpha1.Refs{ + Org: p.Owner, + Repo: p.Repo, + BaseRef: p.Branch, + } + l := logrus.WithField(scmprovider.RepoLogField, p.Repo).WithField(scmprovider.OrgLogField, p.Owner) + + pj := jobutil.NewLighthouseJob(jobutil.PeriodicSpec(l, p.Periodic, refs), labels, p.Annotations) + l.WithFields(jobutil.LighthouseJobFields(&pj)).Info("Creating a new LighthouseJob.") + _, err := p.LauncherClient.Launch(&pj) + if err != nil { + l.WithError(err).Error("Failed to create lighthouse job for cron ") + } +} + +func StartPeriodics(configAgent *config.Agent, launcher launcher, fileBrowsers *filebrowser.FileBrowsers, periodicAgent *PeriodicAgent) { + cronAgent := cron.New() + periodicAgent.Cron = cronAgent + periodics := make(map[string]map[string]PeriodicExec) + periodicAgent.Periodics = periodics + resolverCache := inrepo.NewResolverCache() + fc := filebrowser.NewFetchCache() + c := configAgent.Config() + for fullName := range c.InRepoConfig.Enabled { + repoPeriodics := make(map[string]PeriodicExec) + org, repo := scm.Split(fullName) + if org == "" { + logrus.Errorf("Wrong format of %s, not owner/repo", fullName) + continue + } + l := logrus.WithField(scmprovider.RepoLogField, repo).WithField(scmprovider.OrgLogField, org) + // TODO use github code search to see if any periodics exists before loading? + // in:file filename:trigger.yaml path:.lighthouse repo:fullName periodics + // Would need to accommodate for rate limit since only 10 searches per minute are allowed + // This should make the next TODO less important since not as many clones would be created + // TODO Ensure that the repo clones are removed and deregistered as soon as possible + cfg, err := inrepo.LoadTriggerConfig(fileBrowsers, fc, resolverCache, org, repo, "") + if err != nil { + l.Error(errors.Wrapf(err, "failed to calculate in repo config")) + continue + } + + for _, periodic := range cfg.Spec.Periodics { + addPeriodic(l, periodic, org, repo, launcher, cronAgent, fullName, repoPeriodics) + } + if len(repoPeriodics) > 0 { + periodics[fullName] = repoPeriodics + } + } + + cronAgent.Start() +} + +func addPeriodic(l *logrus.Entry, periodic job.Periodic, owner, repo string, launcher launcher, cronAgent *cron.Cron, fullName string, repoPeriodics map[string]PeriodicExec) { + exec := PeriodicExec{ + Periodic: periodic, + Owner: owner, + Repo: repo, + LauncherClient: launcher, + } + var err error + exec.EntryID, err = cronAgent.AddJob(periodic.Cron, &exec) + if err != nil { + l.WithError(err).Errorf("failed to schedule job %s", periodic.Name) + } else { + repoPeriodics[periodic.Name] = exec + l.Infof("Periodic %s is scheduled since it is new or has changed", periodic.Name) + } +} diff --git a/pkg/plugins/trigger/trigger.go b/pkg/plugins/trigger/trigger.go index ef749954a..330d06305 100644 --- a/pkg/plugins/trigger/trigger.go +++ b/pkg/plugins/trigger/trigger.go @@ -45,9 +45,10 @@ var plugin = plugins.Plugin{ Description: `The trigger plugin starts tests in reaction to commands and pull request events. It is responsible for ensuring that test jobs are only run on trusted PRs. A PR is considered trusted if the author is a member of the 'trusted organization' for the repository or if such a member has left an '/ok-to-test' command on the PR.
Trigger starts jobs automatically when a new trusted PR is created or when an untrusted PR becomes trusted, but it can also be used to start jobs manually via the '/test' command.
The '/retest' command can be used to rerun jobs that have reported failure.`, - ConfigHelpProvider: configHelp, - PullRequestHandler: handlePullRequest, - PushEventHandler: handlePush, + ConfigHelpProvider: configHelp, + PullRequestHandler: handlePullRequest, + PushEventHandler: handlePush, + DeploymentStatusHandler: handleDeploymentStatus, Commands: []plugins.Command{{ Name: "ok-to-test", Description: "Marks a PR as 'trusted' and starts tests.", @@ -75,6 +76,10 @@ var plugin = plugins.Plugin{ }}, } +func handleDeploymentStatus(agent plugins.Agent, ds scm.DeploymentStatusHook) error { + return handleDeployment(getClient(agent), ds) +} + func init() { customTriggerCommand := os.Getenv(customerTriggerCommandEnvVar) if customTriggerCommand != "" { diff --git a/pkg/triggerconfig/inrepo/load_triggers.go b/pkg/triggerconfig/inrepo/load_triggers.go index 58f022cad..0af32fea7 100644 --- a/pkg/triggerconfig/inrepo/load_triggers.go +++ b/pkg/triggerconfig/inrepo/load_triggers.go @@ -95,6 +95,8 @@ func mergeConfigs(m map[string]*triggerconfig.Config) (*triggerconfig.Config, er // lets check for duplicates presubmitNames := map[string]string{} postsubmitNames := map[string]string{} + periodicNames := map[string]string{} + deploymentNames := map[string]string{} for file, cfg := range m { for _, ps := range cfg.Spec.Presubmits { name := ps.Name @@ -114,6 +116,24 @@ func mergeConfigs(m map[string]*triggerconfig.Config) (*triggerconfig.Config, er return nil, errors.Errorf("duplicate postsubmit %s in file %s and %s", name, otherFile, file) } } + for _, ps := range cfg.Spec.Periodics { + name := ps.Name + otherFile := periodicNames[name] + if otherFile == "" { + periodicNames[name] = file + } else { + return nil, errors.Errorf("duplicate periodic %s in file %s and %s", name, otherFile, file) + } + } + for _, ps := range cfg.Spec.Deployments { + name := ps.Name + otherFile := deploymentNames[name] + if otherFile == "" { + deploymentNames[name] = file + } else { + return nil, errors.Errorf("duplicate deployment %s in file %s and %s", name, otherFile, file) + } + } answer = merge.CombineConfigs(answer, cfg) } if answer == nil { @@ -193,6 +213,57 @@ func loadConfigFile(filePath string, fileBrowsers *filebrowser.FileBrowsers, fc }) } } + for i := range repoConfig.Spec.Deployments { + r := &repoConfig.Spec.Deployments[i] + sourcePath := r.SourcePath + if sourcePath != "" { + if r.Agent == "" { + r.Agent = job.TektonPipelineAgent + } + // lets load the local file data now as we have locked the git file system + data, err := loadLocalFile(dir, sourcePath, sha) + if err != nil { + return nil, err + } + r.SetPipelineLoader(func(base *job.Base) error { + err = loadJobBaseFromSourcePath(data, fileBrowsers, fc, cache, base, ownerName, repoName, sourcePath, sha) + if err != nil { + return errors.Wrapf(err, "failed to load source for deployment %s", r.Name) + } + r.Base = *base + if r.Agent == "" && r.PipelineRunSpec != nil { + r.Agent = job.TektonPipelineAgent + } + return nil + }) + } + } + for i := range repoConfig.Spec.Periodics { + r := &repoConfig.Spec.Periodics[i] + sourcePath := r.SourcePath + if sourcePath != "" { + if r.Agent == "" { + r.Agent = job.TektonPipelineAgent + } + // lets load the local file data now as we have locked the git file system + data, err := loadLocalFile(dir, sourcePath, sha) + if err != nil { + return nil, err + } + r.SetPipelineLoader(func(base *job.Base) error { + err = loadJobBaseFromSourcePath(data, fileBrowsers, fc, cache, base, ownerName, repoName, sourcePath, sha) + if err != nil { + return errors.Wrapf(err, "failed to load source for periodic %s", r.Name) + } + r.Base = *base + if r.Agent == "" && r.PipelineRunSpec != nil { + r.Agent = job.TektonPipelineAgent + } + return nil + }) + } + } + return repoConfig, nil } diff --git a/pkg/triggerconfig/merge/combine.go b/pkg/triggerconfig/merge/combine.go index 0fbc3f32b..4b054ed54 100644 --- a/pkg/triggerconfig/merge/combine.go +++ b/pkg/triggerconfig/merge/combine.go @@ -18,5 +18,11 @@ func CombineConfigs(a, b *triggerconfig.Config) *triggerconfig.Config { for _, r := range b.Spec.Postsubmits { a.Spec.Postsubmits = append(a.Spec.Postsubmits, r) } + for _, r := range b.Spec.Periodics { + a.Spec.Periodics = append(a.Spec.Periodics, r) + } + for _, r := range b.Spec.Deployments { + a.Spec.Deployments = append(a.Spec.Deployments, r) + } return a } diff --git a/pkg/triggerconfig/merge/merge.go b/pkg/triggerconfig/merge/merge.go index d8b226dec..57739ed6c 100644 --- a/pkg/triggerconfig/merge/merge.go +++ b/pkg/triggerconfig/merge/merge.go @@ -64,6 +64,35 @@ func ConfigMerge(cfg *config.Config, pluginsCfg *plugins.Configuration, repoConf } cfg.Postsubmits[repoKey] = ps } + if len(repoConfig.Spec.Periodics) > 0 { + cfg.Periodics = append(cfg.Periodics, repoConfig.Spec.Periodics...) + } + if len(repoConfig.Spec.Deployments) > 0 { + // lets make a new map to avoid concurrent modifications + m := map[string][]job.Deployment{} + if cfg.Deployments != nil { + for k, v := range cfg.Deployments { + m[k] = append([]job.Deployment{}, v...) + } + } + cfg.Deployments = m + + ps := cfg.Deployments[repoKey] + for _, p := range repoConfig.Spec.Deployments { + found := false + for i := range ps { + pt2 := &ps[i] + if pt2.Name == p.Name { + ps[i] = p + found = true + } + } + if !found { + ps = append(ps, p) + } + } + cfg.Deployments[repoKey] = ps + } // lets make sure we've got a trigger added idx := len(pluginsCfg.Triggers) - 1 diff --git a/pkg/triggerconfig/types.go b/pkg/triggerconfig/types.go index 279303b6e..96a1e14b2 100644 --- a/pkg/triggerconfig/types.go +++ b/pkg/triggerconfig/types.go @@ -23,6 +23,10 @@ type ConfigSpec struct { // Postsubmit zero or more postsubmits Postsubmits []job.Postsubmit `json:"postsubmits,omitempty"` + + Periodics []job.Periodic `json:"periodics,omitempty"` + + Deployments []job.Deployment `json:"deployments,omitempty"` } // ConfigList contains a list of Config diff --git a/pkg/webhook/events.go b/pkg/webhook/events.go index bf61ca938..eb3cf9316 100644 --- a/pkg/webhook/events.go +++ b/pkg/webhook/events.go @@ -28,9 +28,12 @@ import ( "github.com/jenkins-x/go-scm/scm" "github.com/jenkins-x/lighthouse/pkg/config" "github.com/jenkins-x/lighthouse/pkg/filebrowser" + gitv2 "github.com/jenkins-x/lighthouse/pkg/git/v2" "github.com/jenkins-x/lighthouse/pkg/plugins" + "github.com/jenkins-x/lighthouse/pkg/plugins/trigger" "github.com/jenkins-x/lighthouse/pkg/scmprovider" "github.com/jenkins-x/lighthouse/pkg/util" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -39,6 +42,7 @@ type Server struct { ClientAgent *plugins.ClientAgent Plugins *plugins.ConfigAgent ConfigAgent *config.Agent + PeriodicAgent *trigger.PeriodicAgent ServerURL *url.URL TokenGenerator func() []byte Metrics *Metrics @@ -214,6 +218,7 @@ func (s *Server) handlePushEvent(l *logrus.Entry, pe *scm.PushHook) { }(p, h.PushEventHandler) } } + s.PeriodicAgent.UpdatePeriodics(repo.Namespace, repo.Name, agent, pe) l.WithField("count", strconv.Itoa(c)).Info("number of push handlers") }() } @@ -358,6 +363,36 @@ func (s *Server) handleReviewEvent(l *logrus.Entry, re scm.ReviewHook) { }() } +func (s *Server) handleDeploymentStatusEvent(l *logrus.Entry, ds scm.DeploymentStatusHook) { + l = l.WithFields(logrus.Fields{ + scmprovider.OrgLogField: ds.Repo.Namespace, + scmprovider.RepoLogField: ds.Repo.Name, + }) + l.Infof("Deployment %s.", ds.Action) + + // lets invoke the agent creation async as this can take a little while + go func() { + repo := ds.Repo + agent, err := s.CreateAgent(l, repo.Namespace, repo.Name, ds.Deployment.Sha) + if err != nil { + agent.Logger.WithError(err).Error("Error creating agent for DeploymentStatusEvent.") + return + } + for p, h := range s.getPlugins(ds.Repo.Namespace, ds.Repo.Name) { + if h.DeploymentStatusHandler != nil { + s.wg.Add(1) + go func(p string, h plugins.DeploymentStatusHandler) { + defer s.wg.Done() + if err := h(agent, ds); err != nil { + agent.Logger.WithError(err).Error("Error handling ReviewEvent.") + } + }(p, h.DeploymentStatusHandler) + } + } + }() + +} + func (s *Server) reportErrorToPullRequest(l *logrus.Entry, agent plugins.Agent, repo scm.Repository, pr *scm.PullRequestHook, err error) { fileLink := repo.Link + "/blob/" + pr.PullRequest.Sha + "/" message := "failed to trigger Pull Request pipeline\n" + util.ErrorToMarkdown(err, fileLink) @@ -393,3 +428,35 @@ func actionRelatesToPullRequestComment(action scm.Action, l *logrus.Entry) bool return false } } + +func (s *Server) initializeFileBrowser(token string, gitCloneUser, gitServerURL string) error { + configureOpts := func(opts *gitv2.ClientFactoryOpts) { + opts.Token = func() []byte { + return []byte(token) + } + opts.GitUser = func() (name, email string, err error) { + name = gitCloneUser + return + } + opts.Username = func() (login string, err error) { + login = gitCloneUser + return + } + if s.ServerURL.Host != "" { + opts.Host = s.ServerURL.Host + } + if s.ServerURL.Scheme != "" { + opts.Scheme = s.ServerURL.Scheme + } + } + gitFactory, err := gitv2.NewNoMirrorClientFactory(configureOpts) + if err != nil { + return errors.Wrapf(err, "failed to create git client factory for server %s", gitServerURL) + } + fb := filebrowser.NewFileBrowserFromGitClient(gitFactory) + s.FileBrowsers, err = filebrowser.NewFileBrowsers(gitServerURL, fb) + if err != nil { + return errors.Wrapf(err, "failed to create git filebrowser %s", gitServerURL) + } + return nil +} diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 70c11c23a..3a38ba778 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -8,22 +8,20 @@ import ( "net/http" "net/url" "os" + "strconv" "strings" - "github.com/jenkins-x/go-scm/pkg/hmac" - - "github.com/jenkins-x/lighthouse/pkg/externalplugincfg" - lru "github.com/hashicorp/golang-lru" + "github.com/jenkins-x/go-scm/pkg/hmac" "github.com/jenkins-x/go-scm/scm" "github.com/jenkins-x/lighthouse/pkg/clients" "github.com/jenkins-x/lighthouse/pkg/config" - "github.com/jenkins-x/lighthouse/pkg/filebrowser" + "github.com/jenkins-x/lighthouse/pkg/externalplugincfg" "github.com/jenkins-x/lighthouse/pkg/git" - gitv2 "github.com/jenkins-x/lighthouse/pkg/git/v2" "github.com/jenkins-x/lighthouse/pkg/launcher" "github.com/jenkins-x/lighthouse/pkg/metrics" "github.com/jenkins-x/lighthouse/pkg/plugins" + "github.com/jenkins-x/lighthouse/pkg/plugins/trigger" "github.com/jenkins-x/lighthouse/pkg/util" "github.com/jenkins-x/lighthouse/pkg/version" "github.com/jenkins-x/lighthouse/pkg/watcher" @@ -220,29 +218,16 @@ func (o *WebhooksController) handleWebhookOrPollRequest(w http.ResponseWriter, r ghaSecretDir := util.GetGitHubAppSecretDir() - var gitCloneUser string - var token string - if ghaSecretDir != "" { - gitCloneUser = util.GitHubAppGitRemoteUsername - tokenFinder := util.NewOwnerTokensDir(serverURL, ghaSecretDir) - token, err = tokenFinder.FindToken(webhook.Repository().Namespace) - if err != nil { - logrus.Errorf("failed to read owner token: %s", err.Error()) - responseHTTPError(w, http.StatusInternalServerError, fmt.Sprintf("500 Internal Server Error: failed to read owner token: %s", err.Error())) - return - } - } else { - gitCloneUser = util.GetBotName(cfg) - token, err = util.GetSCMToken(util.GitKind(cfg)) - if err != nil { - logrus.Errorf("no scm token specified: %s", err.Error()) - responseHTTPError(w, http.StatusInternalServerError, fmt.Sprintf("500 Internal Server Error: no scm token specified: %s", err.Error())) - return - } + gitCloneUser, token, err := getCredentials(ghaSecretDir, serverURL, webhook.Repository().Namespace, cfg) + if err != nil { + logrus.Error(err.Error()) + responseHTTPError(w, http.StatusInternalServerError, fmt.Sprintf("500 Internal Server Error: %s", err.Error())) + return } _, kubeClient, lhClient, _, err := clients.GetAPIClients() if err != nil { responseHTTPError(w, http.StatusInternalServerError, fmt.Sprintf("500 Internal Server Error: %s", err.Error())) + return } o.gitClient.SetCredentials(gitCloneUser, func() []byte { @@ -260,35 +245,8 @@ func (o *WebhooksController) handleWebhookOrPollRequest(w http.ResponseWriter, r } if o.server.FileBrowsers == nil { - configureOpts := func(opts *gitv2.ClientFactoryOpts) { - opts.Token = func() []byte { - return []byte(token) - } - opts.GitUser = func() (name, email string, err error) { - name = gitCloneUser - return - } - opts.Username = func() (login string, err error) { - login = gitCloneUser - return - } - if o.server.ServerURL.Host != "" { - opts.Host = o.server.ServerURL.Host - } - if o.server.ServerURL.Scheme != "" { - opts.Scheme = o.server.ServerURL.Scheme - } - } - gitFactory, err := gitv2.NewNoMirrorClientFactory(configureOpts) + err := o.server.initializeFileBrowser(token, gitCloneUser, o.gitServerURL) if err != nil { - err = errors.Wrapf(err, "failed to create git client factory for server %s", o.gitServerURL) - responseHTTPError(w, http.StatusInternalServerError, fmt.Sprintf("500 Internal Server Error: %s", err.Error())) - return - } - fb := filebrowser.NewFileBrowserFromGitClient(gitFactory) - o.server.FileBrowsers, err = filebrowser.NewFileBrowsers(o.gitServerURL, fb) - if err != nil { - err = errors.Wrapf(err, "failed to create git filebrowsers%s", o.gitServerURL) responseHTTPError(w, http.StatusInternalServerError, fmt.Sprintf("500 Internal Server Error: %s", err.Error())) return } @@ -318,6 +276,24 @@ func (o *WebhooksController) handleWebhookOrPollRequest(w http.ResponseWriter, r } } +func getCredentials(ghaSecretDir string, serverURL string, owner string, cfg func() *config.Config) (gitCloneUser string, token string, err error) { + if ghaSecretDir != "" { + gitCloneUser = util.GitHubAppGitRemoteUsername + tokenFinder := util.NewOwnerTokensDir(serverURL, ghaSecretDir) + token, err = tokenFinder.FindToken(owner) + if err != nil { + err = errors.Wrap(err, "failed to read owner token") + } + } else { + gitCloneUser = util.GetBotName(cfg) + token, err = util.GetSCMToken(util.GitKind(cfg)) + if err != nil { + err = errors.Wrap(err, "no scm token specified") + } + } + return +} + // ProcessWebHook process a webhook func (o *WebhooksController) ProcessWebHook(l *logrus.Entry, webhook scm.Webhook) (*logrus.Entry, string, error) { repository := webhook.Repository() @@ -475,6 +451,21 @@ func (o *WebhooksController) ProcessWebHook(l *logrus.Entry, webhook scm.Webhook o.server.handleReviewEvent(l, *prReviewHook) return l, "processed PR review hook", nil } + deploymentStatusHook, ok := webhook.(*scm.DeploymentStatusHook) + if ok { + action := deploymentStatusHook.Action + fields["Action"] = action.String() + status := deploymentStatusHook.DeploymentStatus + fields["Status.State"] = status.State + fields["Status.Author"] = status.Author + fields["Status.LogLink"] = status.LogLink + l = l.WithFields(fields) + + l.Info("invoking PR Review handler") + + o.server.handleDeploymentStatusEvent(l, *deploymentStatusHook) + return l, "processed PR review hook", nil + } l.Debugf("unknown kind %s webhook %#v", webhook.Kind(), webhook) return l, fmt.Sprintf("unknown hook %s", webhook.Kind()), nil } @@ -521,13 +512,37 @@ func (o *WebhooksController) createHookServer() (*Server, error) { } server := &Server{ - ConfigAgent: configAgent, - Plugins: pluginAgent, - Metrics: promMetrics, - ServerURL: serverURL, - InRepoCache: cache, + ConfigAgent: configAgent, + Plugins: pluginAgent, + Metrics: promMetrics, + ServerURL: serverURL, + InRepoCache: cache, + PeriodicAgent: &trigger.PeriodicAgent{}, //TokenGenerator: secretAgent.GetTokenGenerator(o.webhookSecretFile), } + + // If we don't use sparse checkout this would become unreasonably slow + // TODO: Also add separate toggle + sparseCheckout, _ := strconv.ParseBool(os.Getenv("SPARSE_CHECKOUT")) + if sparseCheckout { + if server.FileBrowsers == nil { + ghaSecretDir := util.GetGitHubAppSecretDir() + + gitCloneUser, token, err := getCredentials(ghaSecretDir, o.gitServerURL, "", configAgent.Config) + if err != nil { + logrus.Error(err.Error()) + } else { + err = server.initializeFileBrowser(token, gitCloneUser, o.gitServerURL) + if err != nil { + logrus.Error(err.Error()) + } + } + } + if server.FileBrowsers != nil { + go trigger.StartPeriodics(configAgent, server.ClientAgent.LauncherClient, server.FileBrowsers, server.PeriodicAgent) + } + } + return server, nil } From 0ca6de6997f7f654e3965211d5c5c06d3adc96c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 26 Jul 2023 23:16:33 +0200 Subject: [PATCH 02/22] feat: trigger on deploy status and periodically --- pkg/config/job/periodic.go | 2 -- pkg/plugins/trigger/periodic.go | 9 ++++++--- pkg/webhook/events.go | 7 ++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pkg/config/job/periodic.go b/pkg/config/job/periodic.go index f5bc496d1..471d7d99e 100644 --- a/pkg/config/job/periodic.go +++ b/pkg/config/job/periodic.go @@ -20,8 +20,6 @@ package job type Periodic struct { Base Reporter - // The branch to build - Branch string `json:"branch"` // Cron representation of job trigger time Cron string `json:"cron"` } diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index b58dbb681..b7a9541e6 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -37,6 +37,10 @@ func (p PeriodicAgent) UpdatePeriodics(org string, repo string, agent plugins.Ag // Probably better then to just create CronJobs that create LighthouseJobs/PipelineRuns using kubectl/tkn. // Possibly with the Pipeline stored as separate resource and then either have the LighthouseJob refer to it or do tkn pipeline start. // With proper labels these CronJobs/Pipelines could be handled fairly efficiently + // So with LighthouseJobs it could be rendered and put in a configmap which is mounted in the cronjob. + // Then kubectl apply -f to create the job and then to set the status kubectl patch LighthouseJob myresource --type=merge --subresource status --patch 'status: {state: triggered}' + // Would really only need to run StartPeriodics when in a new cluster. How do I know when it is needed? I would + // need to store in cluster when StartPeriodics has been run. repoPeriodics := maps.Clone(p.Periodics[fullName]) if repoPeriodics == nil { repoPeriodics = make(map[string]PeriodicExec) @@ -87,9 +91,8 @@ func (p *PeriodicExec) Run() { labels[k] = v } refs := v1alpha1.Refs{ - Org: p.Owner, - Repo: p.Repo, - BaseRef: p.Branch, + Org: p.Owner, + Repo: p.Repo, } l := logrus.WithField(scmprovider.RepoLogField, p.Repo).WithField(scmprovider.OrgLogField, p.Owner) diff --git a/pkg/webhook/events.go b/pkg/webhook/events.go index eb3cf9316..ac5f08816 100644 --- a/pkg/webhook/events.go +++ b/pkg/webhook/events.go @@ -22,6 +22,7 @@ import ( "net/url" "regexp" "strconv" + "strings" "sync" lru "github.com/hashicorp/golang-lru" @@ -218,7 +219,11 @@ func (s *Server) handlePushEvent(l *logrus.Entry, pe *scm.PushHook) { }(p, h.PushEventHandler) } } - s.PeriodicAgent.UpdatePeriodics(repo.Namespace, repo.Name, agent, pe) + // Update periodics from the default branch + refBranch := strings.TrimPrefix(pe.Ref, "refs/heads/") + if refBranch == pe.Repository().Branch { + s.PeriodicAgent.UpdatePeriodics(repo.Namespace, repo.Name, agent, pe) + } l.WithField("count", strconv.Itoa(c)).Info("number of push handlers") }() } From eaa655a9e36c8d245064c6285411016c1174f7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Sun, 6 Aug 2023 23:52:40 +0200 Subject: [PATCH 03/22] feat: store periodics as cronjobs/configmaps --- pkg/plugins/trigger/periodic.go | 481 +++++++++++++++++++++++++------- pkg/webhook/events.go | 2 +- pkg/webhook/webhook.go | 25 +- 3 files changed, 393 insertions(+), 115 deletions(-) diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index b7a9541e6..82b4d5e1a 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -1,10 +1,14 @@ package trigger import ( - "regexp" + "context" + "encoding/json" + "fmt" + "path/filepath" + "strconv" + "strings" + "time" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "github.com/jenkins-x/go-scm/scm" "github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1" "github.com/jenkins-x/lighthouse/pkg/config" @@ -14,147 +18,424 @@ import ( "github.com/jenkins-x/lighthouse/pkg/plugins" "github.com/jenkins-x/lighthouse/pkg/scmprovider" "github.com/jenkins-x/lighthouse/pkg/triggerconfig/inrepo" + "github.com/jenkins-x/lighthouse/pkg/util" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "golang.org/x/exp/maps" - "gopkg.in/robfig/cron.v2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + applybatchv1 "k8s.io/client-go/applyconfigurations/batch/v1" + applyv1 "k8s.io/client-go/applyconfigurations/core/v1" + kubeclient "k8s.io/client-go/kubernetes" + typedbatchv1 "k8s.io/client-go/kubernetes/typed/batch/v1" + typedv1 "k8s.io/client-go/kubernetes/typed/core/v1" ) type PeriodicAgent struct { - Cron *cron.Cron - Periodics map[string]map[string]PeriodicExec + Namespace string } -func (p PeriodicAgent) UpdatePeriodics(org string, repo string, agent plugins.Agent, pe *scm.PushHook) { - fullName := org + "/" + repo - // FIXME Here is a race condition with StartPeriodics so that if StartPeriodics and UpdatePeriodics update - // for a repo at the same time a periodic could be scheduled multiple times - // Another potential cause for duplicate jobs is that multiple lighthouse webhook processes could run at the same time - // StartPeriodics is likely so slow though that the risk for missed jobs are greater though - // Ideally an external lock should be used to synchronise, but it is unlikely to work well. It would probably be better to use something external - // Maybe integrate with Tekton Triggers, but that would mean another moving part... - // https://github.com/tektoncd/triggers/tree/main/examples/v1beta1/cron - // Probably better then to just create CronJobs that create LighthouseJobs/PipelineRuns using kubectl/tkn. - // Possibly with the Pipeline stored as separate resource and then either have the LighthouseJob refer to it or do tkn pipeline start. - // With proper labels these CronJobs/Pipelines could be handled fairly efficiently - // So with LighthouseJobs it could be rendered and put in a configmap which is mounted in the cronjob. - // Then kubectl apply -f to create the job and then to set the status kubectl patch LighthouseJob myresource --type=merge --subresource status --patch 'status: {state: triggered}' - // Would really only need to run StartPeriodics when in a new cluster. How do I know when it is needed? I would - // need to store in cluster when StartPeriodics has been run. - repoPeriodics := maps.Clone(p.Periodics[fullName]) - if repoPeriodics == nil { - repoPeriodics = make(map[string]PeriodicExec) - p.Periodics[fullName] = repoPeriodics - } - l := logrus.WithField(scmprovider.RepoLogField, repo).WithField(scmprovider.OrgLogField, org) - for _, periodic := range agent.Config.Periodics { - exec, exists := repoPeriodics[periodic.Name] - if !exists || hasChanged(exec.Periodic, periodic, pe, l) { - // New or changed. Changed will have old entry removed below - addPeriodic(l, periodic, org, repo, agent.LauncherClient, p.Cron, fullName, p.Periodics[fullName]) - } else { - // Nothing to change - delete(repoPeriodics, periodic.Name) - } - } - // Deschedule periodic no longer found in repo - for _, periodic := range repoPeriodics { - p.Cron.Remove(periodic.EntryID) +const fieldManager = "lighthouse" +const initializedField = "isPeriodicsInitialized" +const initStartedField = "periodicsInitializationStarted" + +func (pa *PeriodicAgent) UpdatePeriodics(kc kubeclient.Interface, agent plugins.Agent, pe *scm.PushHook) { + repo := pe.Repository() + fullName := repo.FullName + l := logrus.WithField(scmprovider.RepoLogField, repo.Name).WithField(scmprovider.OrgLogField, repo.Namespace) + if !hasChanges(pe, agent) { + return + } + cmInterface := kc.CoreV1().ConfigMaps(pa.Namespace) + cjInterface := kc.BatchV1().CronJobs(pa.Namespace) + cmList, cronList, done := pa.getExistingResources(l, cmInterface, cjInterface, + fmt.Sprintf("app=lighthouse-webhooks,component=periodic,repo=%s,trigger", fullName)) + if done { + return } -} -// hasChanged return true if any fields have changed except pipelineLoader and PipelineRunSpec since lazyLoading means you can't compare these -// Also check if the file pointed to by SourcePath has changed -func hasChanged(existing, imported job.Periodic, pe *scm.PushHook, l *logrus.Entry) bool { - if !cmp.Equal(existing, imported, cmpopts.IgnoreFields(job.Periodic{}, "pipelineLoader", "PipelineRunSpec")) { - return true + getExistingConfigMap := func(p job.Periodic) *corev1.ConfigMap { + for i, cm := range cmList.Items { + if cm.Labels["trigger"] == p.Name { + cmList.Items[i] = corev1.ConfigMap{} + return &cm + } + } + return nil } - // Since We don't know which directory SourcePath is relative to we check for any file with that name - changeMatcher := job.RegexpChangeMatcher{RunIfChanged: regexp.QuoteMeta(existing.SourcePath)} - _, run, err := changeMatcher.ShouldRun(listPushEventChanges(*pe)) - if err != nil { - l.WithError(err).Warnf("Can't determine if %s has changed, assumes it hasn't", existing.SourcePath) + getExistingCron := func(p job.Periodic) *batchv1.CronJob { + for i, cj := range cronList.Items { + if cj.Labels["trigger"] == p.Name { + cronList.Items[i] = batchv1.CronJob{} + return &cj + } + } + return nil } - return run -} -type PeriodicExec struct { - job.Periodic - Owner, Repo string - LauncherClient launcher - EntryID cron.EntryID + if pa.UpdatePeriodicsForRepo( + agent.Config.Periodics, + fullName, + l, + getExistingConfigMap, + getExistingCron, + repo.Namespace, + repo.Name, + cmInterface, + cjInterface) { + return + } + + for _, cj := range cronList.Items { + if cj.Name != "" { + deleteCronJob(cjInterface, &cj) + } + } + for _, cm := range cmList.Items { + if cm.Name != "" { + deleteConfigMap(cmInterface, &cm) + } + } } -func (p *PeriodicExec) Run() { - labels := make(map[string]string) - for k, v := range p.Labels { - labels[k] = v +// hasChanges return true if any triggers.yaml or file pointed to by SourcePath has changed +func hasChanges(pe *scm.PushHook, agent plugins.Agent) bool { + changedFiles, err := listPushEventChanges(*pe)() + if err != nil { + return false + } + lighthouseFiles := make(map[string]bool) + for _, changedFile := range changedFiles { + if strings.HasPrefix(changedFile, ".lighthouse/") { + _, changedFile = filepath.Split(changedFile) + if changedFile == "triggers.yaml" { + return true + } + lighthouseFiles[changedFile] = true + } } - refs := v1alpha1.Refs{ - Org: p.Owner, - Repo: p.Repo, + for _, p := range agent.Config.Periodics { + _, sourcePath := filepath.Split(p.SourcePath) + if lighthouseFiles[sourcePath] { + return true + } } - l := logrus.WithField(scmprovider.RepoLogField, p.Repo).WithField(scmprovider.OrgLogField, p.Owner) + return false +} - pj := jobutil.NewLighthouseJob(jobutil.PeriodicSpec(l, p.Periodic, refs), labels, p.Annotations) - l.WithFields(jobutil.LighthouseJobFields(&pj)).Info("Creating a new LighthouseJob.") - _, err := p.LauncherClient.Launch(&pj) +func (pa *PeriodicAgent) PeriodicsInitialized(namespace string, kc kubeclient.Interface) bool { + cmInterface := kc.CoreV1().ConfigMaps(namespace) + cm, err := cmInterface.Get(context.TODO(), util.ProwConfigMapName, metav1.GetOptions{}) if err != nil { - l.WithError(err).Error("Failed to create lighthouse job for cron ") + logrus.Errorf("Can't get ConfigMap config. Can't check if periodics as initialized") + return true } + isInit := cm.Data[initializedField] + if "true" == isInit { + return true + } + if isInit == "pending" { + initStarted, err := strconv.ParseInt(cm.Data[initStartedField], 10, 64) + // If started less than 24 hours ago we assume it still goes on so return true + if err == nil && time.Unix(initStarted, 0).Before(time.Now().Add(-24*time.Hour)) { + return true + } + } + cmApply, err := applyv1.ExtractConfigMap(cm, fieldManager) + cmApply.Data[initializedField] = "pending" + cm.Data[initStartedField] = strconv.FormatInt(time.Now().Unix(), 10) + _, err = cmInterface.Apply(context.TODO(), cmApply, metav1.ApplyOptions{FieldManager: "lighthouse"}) + if err != nil { + // Somebody else has updated the configmap, so don't initialize periodics now + return true + } + return false } -func StartPeriodics(configAgent *config.Agent, launcher launcher, fileBrowsers *filebrowser.FileBrowsers, periodicAgent *PeriodicAgent) { - cronAgent := cron.New() - periodicAgent.Cron = cronAgent - periodics := make(map[string]map[string]PeriodicExec) - periodicAgent.Periodics = periodics +func (pa *PeriodicAgent) InitializePeriodics(kc kubeclient.Interface, configAgent *config.Agent, fileBrowsers *filebrowser.FileBrowsers) { + // TODO: Add lock so 2 InitializePeriodics can't run at the same time resolverCache := inrepo.NewResolverCache() fc := filebrowser.NewFetchCache() c := configAgent.Config() - for fullName := range c.InRepoConfig.Enabled { - repoPeriodics := make(map[string]PeriodicExec) + cmInterface := kc.CoreV1().ConfigMaps(pa.Namespace) + cjInterface := kc.BatchV1().CronJobs(pa.Namespace) + cmList, cronList, done := pa.getExistingResources(nil, cmInterface, cjInterface, "app=lighthouse-webhooks,component=periodic,repo,trigger") + if done { + return + } + cmMap := make(map[string]map[string]*corev1.ConfigMap) + for _, cm := range cmList.Items { + cmMap[cm.Labels["repo"]][cm.Labels["trigger"]] = &cm + } + cronMap := make(map[string]map[string]*batchv1.CronJob) + for _, cronjob := range cronList.Items { + cronMap[cronjob.Labels["repo"]][cronjob.Labels["trigger"]] = &cronjob + } + + for fullName := range filterPeriodics(c.InRepoConfig.Enabled, configAgent) { + repoCronJobs, repoCronExists := cronMap[fullName] + repoCM, repoCmExists := cmMap[fullName] org, repo := scm.Split(fullName) if org == "" { logrus.Errorf("Wrong format of %s, not owner/repo", fullName) continue } l := logrus.WithField(scmprovider.RepoLogField, repo).WithField(scmprovider.OrgLogField, org) - // TODO use github code search to see if any periodics exists before loading? - // in:file filename:trigger.yaml path:.lighthouse repo:fullName periodics - // Would need to accommodate for rate limit since only 10 searches per minute are allowed - // This should make the next TODO less important since not as many clones would be created // TODO Ensure that the repo clones are removed and deregistered as soon as possible + // One solution would be to run InitializePeriodics in a separate job cfg, err := inrepo.LoadTriggerConfig(fileBrowsers, fc, resolverCache, org, repo, "") if err != nil { l.Error(errors.Wrapf(err, "failed to calculate in repo config")) + // Keeping existing cronjobs if trigger config can not be read + delete(cronMap, fullName) + delete(cmMap, fullName) continue } + getExistingCron := func(p job.Periodic) *batchv1.CronJob { + if repoCronExists { + cj, cjExists := repoCronJobs[p.Name] + if cjExists { + delete(repoCronJobs, p.Name) + return cj + } + } + return nil + } + + getExistingConfigMap := func(p job.Periodic) *corev1.ConfigMap { + if repoCmExists { + cm, cmExist := repoCM[p.Name] + if cmExist { + delete(repoCM, p.Name) + return cm + } + } + return nil + } + + if pa.UpdatePeriodicsForRepo(cfg.Spec.Periodics, fullName, l, getExistingConfigMap, getExistingCron, org, repo, cmInterface, cjInterface) { + return + } + } - for _, periodic := range cfg.Spec.Periodics { - addPeriodic(l, periodic, org, repo, launcher, cronAgent, fullName, repoPeriodics) + // Removing CronJobs not corresponding to any found triggers + for _, repoCron := range cronMap { + for _, aCron := range repoCron { + deleteCronJob(cjInterface, aCron) } - if len(repoPeriodics) > 0 { - periodics[fullName] = repoPeriodics + } + for _, repoCm := range cmMap { + for _, cm := range repoCm { + deleteConfigMap(cmInterface, cm) } } + cmInterface.Apply(context.TODO(), + (&applyv1.ConfigMapApplyConfiguration{}). + WithName("config"). + WithData(map[string]string{initializedField: "true"}), + metav1.ApplyOptions{Force: true, FieldManager: "lighthouse"}) +} + +func deleteConfigMap(cmInterface typedv1.ConfigMapInterface, cm *corev1.ConfigMap) { + err := cmInterface.Delete(context.TODO(), cm.Name, metav1.DeleteOptions{}) + if err != nil { + logrus.WithError(err). + Errorf("Failed to delete ConfigMap %s corresponding to removed trigger %s for repo %s", + cm.Name, cm.Labels["trigger"], cm.Labels["repo"]) + } +} - cronAgent.Start() +func deleteCronJob(cjInterface typedbatchv1.CronJobInterface, cj *batchv1.CronJob) { + err := cjInterface.Delete(context.TODO(), cj.Name, metav1.DeleteOptions{}) + if err != nil { + logrus.WithError(err). + Errorf("Failed to delete CronJob %s corresponding to removed trigger %s for repo %s", + cj.Name, cj.Labels["trigger"], cj.Labels["repo"]) + } } -func addPeriodic(l *logrus.Entry, periodic job.Periodic, owner, repo string, launcher launcher, cronAgent *cron.Cron, fullName string, repoPeriodics map[string]PeriodicExec) { - exec := PeriodicExec{ - Periodic: periodic, - Owner: owner, - Repo: repo, - LauncherClient: launcher, +func (pa *PeriodicAgent) UpdatePeriodicsForRepo( + periodics []job.Periodic, + fullName string, + l *logrus.Entry, + getExistingConfigMap func(p job.Periodic) *corev1.ConfigMap, + getExistingCron func(p job.Periodic) *batchv1.CronJob, + org string, + repo string, + cmInterface typedv1.ConfigMapInterface, + cjInterface typedbatchv1.CronJobInterface, +) bool { + for _, p := range periodics { + labels := map[string]string{ + "app": "lighthouse-webhooks", + "component": "periodic", + "repo": fullName, + "trigger": p.Name, + } + for k, v := range p.Labels { + // don't overwrite labels since that would disturb the logic + _, predef := labels[k] + if !predef { + labels[k] = v + } + } + + resourceName := fmt.Sprintf("lighthouse-%s-%s", fullName, p.Name) + + err := p.LoadPipeline(l) + if err != nil { + l.WithError(err).Warnf("Failed to load pipeline %s from %s", p.Name, p.SourcePath) + continue + } + refs := v1alpha1.Refs{ + Org: org, + Repo: repo, + } + + pj := jobutil.NewLighthouseJob(jobutil.PeriodicSpec(l, p, refs), labels, p.Annotations) + lighthouseData, err := json.Marshal(pj) + + // Only apply if any value have changed + existingCm := getExistingConfigMap(p) + + if existingCm == nil || existingCm.Data["lighthousejob.yaml"] != string(lighthouseData) { + var cm *applyv1.ConfigMapApplyConfiguration + if existingCm != nil { + cm, err = applyv1.ExtractConfigMap(existingCm, fieldManager) + if err != nil { + l.Error(errors.Wrapf(err, "failed to extract ConfigMap")) + return true + } + } else { + cm = (&applyv1.ConfigMapApplyConfiguration{}).WithName(resourceName).WithLabels(labels) + } + cm.Data["lighthousejob.yaml"] = string(lighthouseData) + + _, err := cmInterface.Apply(context.TODO(), cm, metav1.ApplyOptions{Force: true, FieldManager: fieldManager}) + if err != nil { + return false + } + } + existingCron := getExistingCron(p) + if existingCron == nil || existingCron.Spec.Schedule != p.Cron { + var cj *applybatchv1.CronJobApplyConfiguration + if existingCron != nil { + cj, err = applybatchv1.ExtractCronJob(existingCron, fieldManager) + if err != nil { + l.Error(errors.Wrapf(err, "failed to extract CronJob")) + return true + } + } else { + cj = pa.constructCronJob(resourceName, *cj.Name, labels) + } + cj.Spec.Schedule = &p.Cron + _, err := cjInterface.Apply(context.TODO(), cj, metav1.ApplyOptions{Force: true, FieldManager: fieldManager}) + if err != nil { + return false + } + } } - var err error - exec.EntryID, err = cronAgent.AddJob(periodic.Cron, &exec) + return false +} + +func (pa *PeriodicAgent) getExistingResources( + l *logrus.Entry, + cmInterface typedv1.ConfigMapInterface, + cjInterface typedbatchv1.CronJobInterface, + selector string, +) (*corev1.ConfigMapList, *batchv1.CronJobList, bool) { + cmList, err := cmInterface.List(context.TODO(), metav1.ListOptions{ + LabelSelector: selector, + }) if err != nil { - l.WithError(err).Errorf("failed to schedule job %s", periodic.Name) - } else { - repoPeriodics[periodic.Name] = exec - l.Infof("Periodic %s is scheduled since it is new or has changed", periodic.Name) + l.Error("Can't get periodic ConfigMaps. Periodics will not be initialized", err) + return nil, nil, true + } + + cronList, err := cjInterface.List(context.TODO(), metav1.ListOptions{ + LabelSelector: selector, + }) + if err != nil { + l.Error("Can't get periodic CronJobs. Periodics will not be initialized", err) + return nil, nil, true + } + return cmList, cronList, false +} + +func (pa *PeriodicAgent) constructCronJob(resourceName, configMapName string, labels map[string]string) *applybatchv1.CronJobApplyConfiguration { + const volumeName = "ligthousejob" + return (&applybatchv1.CronJobApplyConfiguration{}). + WithName(resourceName). + WithLabels(labels). + WithSpec((&applybatchv1.CronJobSpecApplyConfiguration{}). + WithJobTemplate((&applybatchv1.JobTemplateSpecApplyConfiguration{}). + WithLabels(labels). + WithSpec((&applybatchv1.JobSpecApplyConfiguration{}). + WithBackoffLimit(0). + WithTemplate((&applyv1.PodTemplateSpecApplyConfiguration{}). + WithLabels(labels). + WithSpec((&applyv1.PodSpecApplyConfiguration{}). + WithEnableServiceLinks(false). + // TODO: Get service account from somewhere? + WithServiceAccountName("lighthouse-webhooks"). + WithRestartPolicy("Never"). + WithContainers((&applyv1.ContainerApplyConfiguration{}). + WithName("create-lighthousejob"). + // TODO: make image configurable. Should have yq as well + + WithImage("bitnami/kubectl"). + WithCommand("/bin/sh"). + WithArgs("-c", ` +yq '.metadata.name = "'$HOSTNAME'"' /config/lighthousejob.yaml | kubectl apply -f +kubectl patch LighthouseJob $HOSTNAME --type=merge --subresource status --patch 'status: {state: triggered}' +`). + WithVolumeMounts((&applyv1.VolumeMountApplyConfiguration{}). + WithName(volumeName). + WithMountPath("/config"))). + WithVolumes((&applyv1.VolumeApplyConfiguration{}). + WithName(volumeName). + WithConfigMap((&applyv1.ConfigMapVolumeSourceApplyConfiguration{}). + WithName(configMapName)))))))) +} + +func filterPeriodics(enabled map[string]*bool, agent *config.Agent) map[string]*bool { + _, scmClient, _, _, err := util.GetSCMClient("", agent.Config) + if err != nil { + logrus.Errorf("failed to create SCM scmClient: %s", err.Error()) + return enabled + } + if scmClient.Contents == nil { + return enabled + } + + enable := true + hasPeriodics := make(map[string]*bool) + for fullName := range enabled { + list, _, err := scmClient.Contents.List(context.TODO(), fullName, ".lighthouse", "HEAD") + if err != nil { + continue + } + for _, file := range list { + if file.Type == "dir" { + triggers, _, err := scmClient.Contents.Find(context.TODO(), fullName, file.Path+"/triggers.yaml", "HEAD") + if err != nil { + continue + } + if strings.Contains(string(triggers.Data), "periodics:") { + hasPeriodics[fullName] = &enable + } + } + } + delayForRate(scmClient.Rate()) + } + + return hasPeriodics +} + +func delayForRate(r scm.Rate) { + if r.Remaining < 100 { + duration := time.Duration(r.Reset - time.Now().Unix()) + logrus.Warnf("waiting for %s seconds until rate limit reset: %+v", duration, r) + time.Sleep(duration * time.Second) } } diff --git a/pkg/webhook/events.go b/pkg/webhook/events.go index ac5f08816..76d49551f 100644 --- a/pkg/webhook/events.go +++ b/pkg/webhook/events.go @@ -222,7 +222,7 @@ func (s *Server) handlePushEvent(l *logrus.Entry, pe *scm.PushHook) { // Update periodics from the default branch refBranch := strings.TrimPrefix(pe.Ref, "refs/heads/") if refBranch == pe.Repository().Branch { - s.PeriodicAgent.UpdatePeriodics(repo.Namespace, repo.Name, agent, pe) + s.PeriodicAgent.UpdatePeriodics(s.ClientAgent.KubernetesClient, agent, pe) } l.WithField("count", strconv.Itoa(c)).Info("number of push handlers") }() diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 3a38ba778..251dac485 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -8,7 +8,6 @@ import ( "net/http" "net/url" "os" - "strconv" "strings" lru "github.com/hashicorp/golang-lru" @@ -28,6 +27,7 @@ import ( "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" + kubeclient "k8s.io/client-go/kubernetes" ) // WebhooksController holds the command line arguments @@ -60,8 +60,11 @@ func NewWebhooksController(path, namespace, botName, pluginFilename, configFilen if o.logWebHooks { logrus.Info("enabling webhook logging") } - var err error - o.server, err = o.createHookServer() + _, kubeClient, lhClient, _, err := clients.GetAPIClients() + if err != nil { + return nil, errors.Wrap(err, "Error creating kubernetes resource clients.") + } + o.server, err = o.createHookServer(kubeClient) if err != nil { return nil, errors.Wrapf(err, "failed to create Hook Server") } @@ -73,10 +76,6 @@ func NewWebhooksController(path, namespace, botName, pluginFilename, configFilen } o.gitClient = gitClient - _, _, lhClient, _, err := clients.GetAPIClients() - if err != nil { - return nil, errors.Wrap(err, "Error creating kubernetes resource clients.") - } o.launcher = launcher.NewLauncher(lhClient, o.namespace) return o, nil @@ -474,7 +473,7 @@ func (o *WebhooksController) secretFn(webhook scm.Webhook) (string, error) { return util.HMACToken(), nil } -func (o *WebhooksController) createHookServer() (*Server, error) { +func (o *WebhooksController) createHookServer(kc kubeclient.Interface) (*Server, error) { configAgent := &config.Agent{} pluginAgent := &plugins.ConfigAgent{} @@ -517,14 +516,12 @@ func (o *WebhooksController) createHookServer() (*Server, error) { Metrics: promMetrics, ServerURL: serverURL, InRepoCache: cache, - PeriodicAgent: &trigger.PeriodicAgent{}, + PeriodicAgent: &trigger.PeriodicAgent{Namespace: o.namespace}, //TokenGenerator: secretAgent.GetTokenGenerator(o.webhookSecretFile), } - // If we don't use sparse checkout this would become unreasonably slow - // TODO: Also add separate toggle - sparseCheckout, _ := strconv.ParseBool(os.Getenv("SPARSE_CHECKOUT")) - if sparseCheckout { + // TODO: Add toggle + if !server.PeriodicAgent.PeriodicsInitialized(o.namespace, kc) { if server.FileBrowsers == nil { ghaSecretDir := util.GetGitHubAppSecretDir() @@ -539,7 +536,7 @@ func (o *WebhooksController) createHookServer() (*Server, error) { } } if server.FileBrowsers != nil { - go trigger.StartPeriodics(configAgent, server.ClientAgent.LauncherClient, server.FileBrowsers, server.PeriodicAgent) + go server.PeriodicAgent.InitializePeriodics(kc, configAgent, server.FileBrowsers) } } From 09c335c832c94d021b1bd50c0e80cc3177045c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 9 Aug 2023 21:34:56 +0200 Subject: [PATCH 04/22] fix: test of InitializePeriodics implemented --- pkg/plugins/trigger/periodic.go | 36 ++++-- pkg/plugins/trigger/periodic_test.go | 120 ++++++++++++++++++ .../myapp/.lighthouse/jenkins-x/dailyjob.yaml | 18 +++ .../myapp/.lighthouse/jenkins-x/triggers.yaml | 10 ++ 4 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 pkg/plugins/trigger/periodic_test.go create mode 100644 pkg/plugins/trigger/test_data/testorg/myapp/.lighthouse/jenkins-x/dailyjob.yaml create mode 100644 pkg/plugins/trigger/test_data/testorg/myapp/.lighthouse/jenkins-x/triggers.yaml diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index 82b4d5e1a..fa897d65b 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -33,6 +33,7 @@ import ( type PeriodicAgent struct { Namespace string + SCMClient *scm.Client } const fieldManager = "lighthouse" @@ -154,6 +155,15 @@ func (pa *PeriodicAgent) PeriodicsInitialized(namespace string, kc kubeclient.In func (pa *PeriodicAgent) InitializePeriodics(kc kubeclient.Interface, configAgent *config.Agent, fileBrowsers *filebrowser.FileBrowsers) { // TODO: Add lock so 2 InitializePeriodics can't run at the same time + if pa.SCMClient == nil { + _, scmClient, _, _, err := util.GetSCMClient("", configAgent.Config) + if err != nil { + logrus.Errorf("failed to create SCM scmClient: %s", err.Error()) + return + } + pa.SCMClient = scmClient + } + resolverCache := inrepo.NewResolverCache() fc := filebrowser.NewFetchCache() c := configAgent.Config() @@ -172,7 +182,7 @@ func (pa *PeriodicAgent) InitializePeriodics(kc kubeclient.Interface, configAgen cronMap[cronjob.Labels["repo"]][cronjob.Labels["trigger"]] = &cronjob } - for fullName := range filterPeriodics(c.InRepoConfig.Enabled, configAgent) { + for fullName := range pa.filterPeriodics(c.InRepoConfig.Enabled, configAgent) { repoCronJobs, repoCronExists := cronMap[fullName] repoCM, repoCmExists := cmMap[fullName] org, repo := scm.Split(fullName) @@ -280,7 +290,7 @@ func (pa *PeriodicAgent) UpdatePeriodicsForRepo( } } - resourceName := fmt.Sprintf("lighthouse-%s-%s", fullName, p.Name) + resourceName := fmt.Sprintf("lighthouse-%s-%s-%s", org, repo, p.Name) err := p.LoadPipeline(l) if err != nil { @@ -309,10 +319,14 @@ func (pa *PeriodicAgent) UpdatePeriodicsForRepo( } else { cm = (&applyv1.ConfigMapApplyConfiguration{}).WithName(resourceName).WithLabels(labels) } + if cm.Data == nil { + cm.Data = make(map[string]string) + } cm.Data["lighthousejob.yaml"] = string(lighthouseData) _, err := cmInterface.Apply(context.TODO(), cm, metav1.ApplyOptions{Force: true, FieldManager: fieldManager}) if err != nil { + l.WithError(err).Errorf("failed to apply configmap") return false } } @@ -326,11 +340,12 @@ func (pa *PeriodicAgent) UpdatePeriodicsForRepo( return true } } else { - cj = pa.constructCronJob(resourceName, *cj.Name, labels) + cj = pa.constructCronJob(resourceName, resourceName, labels) } cj.Spec.Schedule = &p.Cron _, err := cjInterface.Apply(context.TODO(), cj, metav1.ApplyOptions{Force: true, FieldManager: fieldManager}) if err != nil { + l.WithError(err).Errorf("failed to apply cronjob") return false } } @@ -398,26 +413,21 @@ kubectl patch LighthouseJob $HOSTNAME --type=merge --subresource status --patch WithName(configMapName)))))))) } -func filterPeriodics(enabled map[string]*bool, agent *config.Agent) map[string]*bool { - _, scmClient, _, _, err := util.GetSCMClient("", agent.Config) - if err != nil { - logrus.Errorf("failed to create SCM scmClient: %s", err.Error()) - return enabled - } - if scmClient.Contents == nil { +func (pa *PeriodicAgent) filterPeriodics(enabled map[string]*bool, agent *config.Agent) map[string]*bool { + if pa.SCMClient.Contents == nil { return enabled } enable := true hasPeriodics := make(map[string]*bool) for fullName := range enabled { - list, _, err := scmClient.Contents.List(context.TODO(), fullName, ".lighthouse", "HEAD") + list, _, err := pa.SCMClient.Contents.List(context.TODO(), fullName, ".lighthouse", "HEAD") if err != nil { continue } for _, file := range list { if file.Type == "dir" { - triggers, _, err := scmClient.Contents.Find(context.TODO(), fullName, file.Path+"/triggers.yaml", "HEAD") + triggers, _, err := pa.SCMClient.Contents.Find(context.TODO(), fullName, file.Path+"/triggers.yaml", "HEAD") if err != nil { continue } @@ -426,7 +436,7 @@ func filterPeriodics(enabled map[string]*bool, agent *config.Agent) map[string]* } } } - delayForRate(scmClient.Rate()) + delayForRate(pa.SCMClient.Rate()) } return hasPeriodics diff --git a/pkg/plugins/trigger/periodic_test.go b/pkg/plugins/trigger/periodic_test.go new file mode 100644 index 000000000..9e490c98c --- /dev/null +++ b/pkg/plugins/trigger/periodic_test.go @@ -0,0 +1,120 @@ +package trigger + +import ( + "context" + "testing" + + scmfake "github.com/jenkins-x/go-scm/scm/driver/fake" + "github.com/jenkins-x/lighthouse/pkg/config" + "github.com/jenkins-x/lighthouse/pkg/config/lighthouse" + "github.com/jenkins-x/lighthouse/pkg/filebrowser" + fbfake "github.com/jenkins-x/lighthouse/pkg/filebrowser/fake" + "github.com/stretchr/testify/require" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + kubefake "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" +) + +var kubeClient *kubefake.Clientset + +// TODO: verify creation of configmap and cronjob from trigger + +func TestInitializePeriodics(t *testing.T) { + const namespace = "default" + newDefault, data := scmfake.NewDefault() + data.ContentDir = "test_data" + + p := &PeriodicAgent{Namespace: namespace, SCMClient: newDefault} + kubeClient = kubefake.NewSimpleClientset(&v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "config"}, + }) + + kubeClient.PrependReactor( + "patch", + "configmaps", + fakeUpsert, + ) + + kubeClient.PrependReactor( + "patch", + "cronjobs", + fakeUpsert, + ) + + var enabled = true + configAgent := &config.Agent{} + configAgent.Set(&config.Config{ + ProwConfig: lighthouse.Config{ + InRepoConfig: lighthouse.InRepoConfig{ + Enabled: map[string]*bool{"testorg/myapp": &enabled}, + }, + }, + }) + fileBrowsers, err := filebrowser.NewFileBrowsers(filebrowser.GitHubURL, fbfake.NewFakeFileBrowser("test_data", true)) + require.NoError(t, err, "failed to create filebrowsers") + + p.InitializePeriodics(kubeClient, configAgent, fileBrowsers) + + selector := "app=lighthouse-webhooks,component=periodic,repo,trigger" + cms, err := kubeClient.CoreV1().ConfigMaps(namespace). + List(context.TODO(), metav1.ListOptions{LabelSelector: selector}) + require.NoError(t, err, "failed to get ConfigMaps") + require.Len(t, cms.Items, 1) + require.Equal(t, lighthouseJob, cms.Items[0].Data["lighthousejob.yaml"]) + + cjs, err := kubeClient.BatchV1().CronJobs(namespace).List(context.TODO(), metav1.ListOptions{}) + require.NoError(t, err, "failed to get CronJobs") + require.Len(t, cjs.Items, 1) + cj := cjs.Items[0].Spec + require.Equal(t, "0 4 * * MON-FRI", cj.Schedule) + containers := cj.JobTemplate.Spec.Template.Spec.Containers + require.Len(t, containers, 1) + require.Len(t, containers[0].Args, 2) +} + +func fakeUpsert(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + pa := action.(clienttesting.PatchAction) + if pa.GetPatchType() == types.ApplyPatchType { + // Apply patches are supposed to upsert, but fake client fails if the object doesn't exist, + // if an apply patch occurs for a deployment that doesn't yet exist, create it. + // However, we already hold the fakeclient lock, so we can't use the front door. + rfunc := clienttesting.ObjectReaction(kubeClient.Tracker()) + _, obj, err := rfunc( + clienttesting.NewGetAction(pa.GetResource(), pa.GetNamespace(), pa.GetName()), + ) + if kerrors.IsNotFound(err) || obj == nil { + objmeta := metav1.ObjectMeta{ + Name: pa.GetName(), + Namespace: pa.GetNamespace(), + } + var newobj runtime.Object + switch pa.GetResource().Resource { + case "configmaps": + newobj = &v1.ConfigMap{ObjectMeta: objmeta} + case "cronjobs": + newobj = &batchv1.CronJob{ObjectMeta: objmeta} + } + _, _, _ = rfunc( + clienttesting.NewCreateAction( + pa.GetResource(), + pa.GetNamespace(), + newobj, + ), + ) + } + return rfunc(clienttesting.NewPatchAction( + pa.GetResource(), + pa.GetNamespace(), + pa.GetName(), + types.StrategicMergePatchType, + pa.GetPatch())) + } + return false, nil, nil +} + +const lighthouseJob = `{"kind":"LighthouseJob","apiVersion":"lighthouse.jenkins.io/v1alpha1","metadata":{"generateName":"testorg-myapp-","creationTimestamp":null,"labels":{"app":"lighthouse-webhooks","component":"periodic","created-by-lighthouse":"true","lighthouse.jenkins-x.io/job":"dailyjob","lighthouse.jenkins-x.io/type":"periodic","repo":"myapp","trigger":"dailyjob"},"annotations":{"lighthouse.jenkins-x.io/job":"dailyjob"}},"spec":{"type":"periodic","agent":"tekton-pipeline","job":"dailyjob","refs":{"org":"testorg","repo":"myapp"},"pipeline_run_spec":{"pipelineSpec":{"tasks":[{"name":"echo-greeting","taskRef":{"name":"task-echo-message"},"params":[{"name":"MESSAGE","value":"$(params.GREETINGS)"},{"name":"BUILD_ID","value":"$(params.BUILD_ID)"},{"name":"JOB_NAME","value":"$(params.JOB_NAME)"},{"name":"JOB_SPEC","value":"$(params.JOB_SPEC)"},{"name":"JOB_TYPE","value":"$(params.JOB_TYPE)"},{"name":"PULL_BASE_REF","value":"$(params.PULL_BASE_REF)"},{"name":"PULL_BASE_SHA","value":"$(params.PULL_BASE_SHA)"},{"name":"PULL_NUMBER","value":"$(params.PULL_NUMBER)"},{"name":"PULL_PULL_REF","value":"$(params.PULL_PULL_REF)"},{"name":"PULL_PULL_SHA","value":"$(params.PULL_PULL_SHA)"},{"name":"PULL_REFS","value":"$(params.PULL_REFS)"},{"name":"REPO_NAME","value":"$(params.REPO_NAME)"},{"name":"REPO_OWNER","value":"$(params.REPO_OWNER)"},{"name":"REPO_URL","value":"$(params.REPO_URL)"}]}],"params":[{"name":"GREETINGS","type":"string","description":"morning greetings, default is Good Morning!","default":"Good Morning!"},{"name":"BUILD_ID","type":"string","description":"the unique build number"},{"name":"JOB_NAME","type":"string","description":"the name of the job which is the trigger context name"},{"name":"JOB_SPEC","type":"string","description":"the specification of the job"},{"name":"JOB_TYPE","type":"string","description":"'the kind of job: postsubmit or presubmit'"},{"name":"PULL_BASE_REF","type":"string","description":"the base git reference of the pull request"},{"name":"PULL_BASE_SHA","type":"string","description":"the git sha of the base of the pull request"},{"name":"PULL_NUMBER","type":"string","description":"git pull request number","default":""},{"name":"PULL_PULL_REF","type":"string","description":"git pull request ref in the form 'refs/pull/$PULL_NUMBER/head'","default":""},{"name":"PULL_PULL_SHA","type":"string","description":"git revision to checkout (branch, tag, sha, ref…)","default":""},{"name":"PULL_REFS","type":"string","description":"git pull reference strings of base and latest in the form 'master:$PULL_BASE_SHA,$PULL_NUMBER:$PULL_PULL_SHA:refs/pull/$PULL_NUMBER/head'"},{"name":"REPO_NAME","type":"string","description":"git repository name"},{"name":"REPO_OWNER","type":"string","description":"git repository owner (user or organisation)"},{"name":"REPO_URL","type":"string","description":"git url to clone"}]}},"pipeline_run_params":[{"name":"GREETINGS"}]},"status":{"startTime":null}}` diff --git a/pkg/plugins/trigger/test_data/testorg/myapp/.lighthouse/jenkins-x/dailyjob.yaml b/pkg/plugins/trigger/test_data/testorg/myapp/.lighthouse/jenkins-x/dailyjob.yaml new file mode 100644 index 000000000..2c3ad3550 --- /dev/null +++ b/pkg/plugins/trigger/test_data/testorg/myapp/.lighthouse/jenkins-x/dailyjob.yaml @@ -0,0 +1,18 @@ +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: dailyjob +spec: + pipelineSpec: + params: + - name: GREETINGS + description: "morning greetings, default is Good Morning!" + type: string + default: "Good Morning!" + tasks: + - name: echo-greeting + taskRef: + name: task-echo-message + params: + - name: MESSAGE + value: $(params.GREETINGS) diff --git a/pkg/plugins/trigger/test_data/testorg/myapp/.lighthouse/jenkins-x/triggers.yaml b/pkg/plugins/trigger/test_data/testorg/myapp/.lighthouse/jenkins-x/triggers.yaml new file mode 100644 index 000000000..83a239ae2 --- /dev/null +++ b/pkg/plugins/trigger/test_data/testorg/myapp/.lighthouse/jenkins-x/triggers.yaml @@ -0,0 +1,10 @@ +apiVersion: config.lighthouse.jenkins-x.io/v1alpha1 +kind: TriggerConfig +spec: + periodics: + - name: dailyjob + cron: "0 4 * * MON-FRI" + source: dailyjob.yaml + pipeline_run_params: + - name: GREETINGS + valueTemplate: 'Howdy!' From b3c1c3b0dd315b35c90c8b0863fb0be83382bfa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Thu, 10 Aug 2023 11:22:43 +0200 Subject: [PATCH 05/22] fix: get service account of lighthouse a lighthousejob gets a unique name (using generateName) so needs to get it to update status --- .../templates/webhooks-deployment.yaml | 4 ++++ pkg/plugins/trigger/periodic.go | 21 +++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/charts/lighthouse/templates/webhooks-deployment.yaml b/charts/lighthouse/templates/webhooks-deployment.yaml index 452bd9b14..753642ef9 100644 --- a/charts/lighthouse/templates/webhooks-deployment.yaml +++ b/charts/lighthouse/templates/webhooks-deployment.yaml @@ -42,6 +42,10 @@ spec: args: - "--namespace={{ .Release.Namespace }}" env: + - name: SERVICE_ACCOUNT + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName - name: "GIT_KIND" value: "{{ .Values.git.kind }}" - name: "LH_CUSTOM_TRIGGER_COMMAND" diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index fa897d65b..66a494d4e 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "os" "path/filepath" "strconv" "strings" @@ -154,7 +155,6 @@ func (pa *PeriodicAgent) PeriodicsInitialized(namespace string, kc kubeclient.In } func (pa *PeriodicAgent) InitializePeriodics(kc kubeclient.Interface, configAgent *config.Agent, fileBrowsers *filebrowser.FileBrowsers) { - // TODO: Add lock so 2 InitializePeriodics can't run at the same time if pa.SCMClient == nil { _, scmClient, _, _, err := util.GetSCMClient("", configAgent.Config) if err != nil { @@ -182,7 +182,7 @@ func (pa *PeriodicAgent) InitializePeriodics(kc kubeclient.Interface, configAgen cronMap[cronjob.Labels["repo"]][cronjob.Labels["trigger"]] = &cronjob } - for fullName := range pa.filterPeriodics(c.InRepoConfig.Enabled, configAgent) { + for fullName := range pa.filterPeriodics(c.InRepoConfig.Enabled) { repoCronJobs, repoCronExists := cronMap[fullName] repoCM, repoCmExists := cmMap[fullName] org, repo := scm.Split(fullName) @@ -379,6 +379,10 @@ func (pa *PeriodicAgent) getExistingResources( func (pa *PeriodicAgent) constructCronJob(resourceName, configMapName string, labels map[string]string) *applybatchv1.CronJobApplyConfiguration { const volumeName = "ligthousejob" + serviceAccount, found := os.LookupEnv("SERVICE_ACCOUNT") + if !found { + serviceAccount = "lighthouse-webhooks" + } return (&applybatchv1.CronJobApplyConfiguration{}). WithName(resourceName). WithLabels(labels). @@ -391,18 +395,17 @@ func (pa *PeriodicAgent) constructCronJob(resourceName, configMapName string, la WithLabels(labels). WithSpec((&applyv1.PodSpecApplyConfiguration{}). WithEnableServiceLinks(false). - // TODO: Get service account from somewhere? - WithServiceAccountName("lighthouse-webhooks"). + WithServiceAccountName(serviceAccount). WithRestartPolicy("Never"). WithContainers((&applyv1.ContainerApplyConfiguration{}). WithName("create-lighthousejob"). - // TODO: make image configurable. Should have yq as well - WithImage("bitnami/kubectl"). WithCommand("/bin/sh"). WithArgs("-c", ` -yq '.metadata.name = "'$HOSTNAME'"' /config/lighthousejob.yaml | kubectl apply -f -kubectl patch LighthouseJob $HOSTNAME --type=merge --subresource status --patch 'status: {state: triggered}' +set -o errexit +create_output=$(kubectl create -f /config/lighthousejob.yaml) +[[ $create_output =~ (.*)\ ]] +kubectl patch ${BASH_REMATCH[1]} --type=merge --subresource status --patch 'status: {state: triggered}' `). WithVolumeMounts((&applyv1.VolumeMountApplyConfiguration{}). WithName(volumeName). @@ -413,7 +416,7 @@ kubectl patch LighthouseJob $HOSTNAME --type=merge --subresource status --patch WithName(configMapName)))))))) } -func (pa *PeriodicAgent) filterPeriodics(enabled map[string]*bool, agent *config.Agent) map[string]*bool { +func (pa *PeriodicAgent) filterPeriodics(enabled map[string]*bool) map[string]*bool { if pa.SCMClient.Contents == nil { return enabled } From cbfb59b2087c821fb6620f6e264e8083e0b9bafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Thu, 10 Aug 2023 18:11:26 +0200 Subject: [PATCH 06/22] fix: test for UpdatePeriodics --- pkg/plugins/trigger/periodic.go | 23 +++--- pkg/plugins/trigger/periodic_test.go | 103 +++++++++++++++++++++------ 2 files changed, 93 insertions(+), 33 deletions(-) diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index 66a494d4e..705068ab0 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -43,7 +43,6 @@ const initStartedField = "periodicsInitializationStarted" func (pa *PeriodicAgent) UpdatePeriodics(kc kubeclient.Interface, agent plugins.Agent, pe *scm.PushHook) { repo := pe.Repository() - fullName := repo.FullName l := logrus.WithField(scmprovider.RepoLogField, repo.Name).WithField(scmprovider.OrgLogField, repo.Namespace) if !hasChanges(pe, agent) { return @@ -51,7 +50,7 @@ func (pa *PeriodicAgent) UpdatePeriodics(kc kubeclient.Interface, agent plugins. cmInterface := kc.CoreV1().ConfigMaps(pa.Namespace) cjInterface := kc.BatchV1().CronJobs(pa.Namespace) cmList, cronList, done := pa.getExistingResources(l, cmInterface, cjInterface, - fmt.Sprintf("app=lighthouse-webhooks,component=periodic,repo=%s,trigger", fullName)) + fmt.Sprintf("app=lighthouse-webhooks,component=periodic,org=%s,repo=%s,trigger", repo.Namespace, repo.Name)) if done { return } @@ -77,14 +76,14 @@ func (pa *PeriodicAgent) UpdatePeriodics(kc kubeclient.Interface, agent plugins. if pa.UpdatePeriodicsForRepo( agent.Config.Periodics, - fullName, l, getExistingConfigMap, getExistingCron, repo.Namespace, repo.Name, cmInterface, - cjInterface) { + cjInterface, + ) { return } @@ -169,17 +168,17 @@ func (pa *PeriodicAgent) InitializePeriodics(kc kubeclient.Interface, configAgen c := configAgent.Config() cmInterface := kc.CoreV1().ConfigMaps(pa.Namespace) cjInterface := kc.BatchV1().CronJobs(pa.Namespace) - cmList, cronList, done := pa.getExistingResources(nil, cmInterface, cjInterface, "app=lighthouse-webhooks,component=periodic,repo,trigger") + cmList, cronList, done := pa.getExistingResources(nil, cmInterface, cjInterface, "app=lighthouse-webhooks,component=periodic,org,repo,trigger") if done { return } cmMap := make(map[string]map[string]*corev1.ConfigMap) for _, cm := range cmList.Items { - cmMap[cm.Labels["repo"]][cm.Labels["trigger"]] = &cm + cmMap[cm.Labels["org"]+"/"+cm.Labels["repo"]][cm.Labels["trigger"]] = &cm } cronMap := make(map[string]map[string]*batchv1.CronJob) for _, cronjob := range cronList.Items { - cronMap[cronjob.Labels["repo"]][cronjob.Labels["trigger"]] = &cronjob + cronMap[cronjob.Labels["org"]+"/"+cronjob.Labels["repo"]][cronjob.Labels["trigger"]] = &cronjob } for fullName := range pa.filterPeriodics(c.InRepoConfig.Enabled) { @@ -223,7 +222,7 @@ func (pa *PeriodicAgent) InitializePeriodics(kc kubeclient.Interface, configAgen return nil } - if pa.UpdatePeriodicsForRepo(cfg.Spec.Periodics, fullName, l, getExistingConfigMap, getExistingCron, org, repo, cmInterface, cjInterface) { + if pa.UpdatePeriodicsForRepo(cfg.Spec.Periodics, l, getExistingConfigMap, getExistingCron, org, repo, cmInterface, cjInterface) { return } } @@ -250,8 +249,8 @@ func deleteConfigMap(cmInterface typedv1.ConfigMapInterface, cm *corev1.ConfigMa err := cmInterface.Delete(context.TODO(), cm.Name, metav1.DeleteOptions{}) if err != nil { logrus.WithError(err). - Errorf("Failed to delete ConfigMap %s corresponding to removed trigger %s for repo %s", - cm.Name, cm.Labels["trigger"], cm.Labels["repo"]) + Errorf("Failed to delete ConfigMap %s corresponding to removed trigger %s for repo %s/%s", + cm.Name, cm.Labels["trigger"], cm.Labels["org"], cm.Labels["repo"]) } } @@ -266,7 +265,6 @@ func deleteCronJob(cjInterface typedbatchv1.CronJobInterface, cj *batchv1.CronJo func (pa *PeriodicAgent) UpdatePeriodicsForRepo( periodics []job.Periodic, - fullName string, l *logrus.Entry, getExistingConfigMap func(p job.Periodic) *corev1.ConfigMap, getExistingCron func(p job.Periodic) *batchv1.CronJob, @@ -279,7 +277,8 @@ func (pa *PeriodicAgent) UpdatePeriodicsForRepo( labels := map[string]string{ "app": "lighthouse-webhooks", "component": "periodic", - "repo": fullName, + "org": org, + "repo": repo, "trigger": p.Name, } for k, v := range p.Labels { diff --git a/pkg/plugins/trigger/periodic_test.go b/pkg/plugins/trigger/periodic_test.go index 9e490c98c..62d327278 100644 --- a/pkg/plugins/trigger/periodic_test.go +++ b/pkg/plugins/trigger/periodic_test.go @@ -4,11 +4,15 @@ import ( "context" "testing" + "github.com/jenkins-x/go-scm/scm" scmfake "github.com/jenkins-x/go-scm/scm/driver/fake" "github.com/jenkins-x/lighthouse/pkg/config" "github.com/jenkins-x/lighthouse/pkg/config/lighthouse" "github.com/jenkins-x/lighthouse/pkg/filebrowser" fbfake "github.com/jenkins-x/lighthouse/pkg/filebrowser/fake" + "github.com/jenkins-x/lighthouse/pkg/plugins" + "github.com/jenkins-x/lighthouse/pkg/triggerconfig/inrepo" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" @@ -22,29 +26,62 @@ import ( var kubeClient *kubefake.Clientset -// TODO: verify creation of configmap and cronjob from trigger +// TODO: test more cases +func TestUpdatePeriodics(t *testing.T) { + namespace, p := setupPeriodicsTest() + fileBrowsers, err := filebrowser.NewFileBrowsers(filebrowser.GitHubURL, fbfake.NewFakeFileBrowser("test_data", true)) + resolverCache := inrepo.NewResolverCache() + fc := filebrowser.NewFetchCache() + cfg, err := inrepo.LoadTriggerConfig(fileBrowsers, fc, resolverCache, "testorg", "myapp", "") -func TestInitializePeriodics(t *testing.T) { - const namespace = "default" - newDefault, data := scmfake.NewDefault() - data.ContentDir = "test_data" + agent := plugins.Agent{ + Config: &config.Config{ + JobConfig: config.JobConfig{ + Periodics: cfg.Spec.Periodics, + }, + }, + Logger: logrus.WithField("plugin", pluginName), + } - p := &PeriodicAgent{Namespace: namespace, SCMClient: newDefault} - kubeClient = kubefake.NewSimpleClientset(&v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "config"}, - }) + pe := &scm.PushHook{ + Ref: "refs/heads/master", + Repo: scm.Repository{ + Namespace: "testorg", + Name: "myapp", + FullName: "testorg/myapp", + }, + Commits: []scm.PushCommit{ + { + ID: "12345678909876", + Message: "Adding periodics", + Modified: []string{ + ".lighthouse/jenkins-x/triggers.yaml", + }, + }, + }, + } - kubeClient.PrependReactor( - "patch", - "configmaps", - fakeUpsert, - ) + p.UpdatePeriodics(kubeClient, agent, pe) - kubeClient.PrependReactor( - "patch", - "cronjobs", - fakeUpsert, - ) + selector := "app=lighthouse-webhooks,component=periodic,repo,trigger" + cms, err := kubeClient.CoreV1().ConfigMaps(namespace). + List(context.TODO(), metav1.ListOptions{LabelSelector: selector}) + require.NoError(t, err, "failed to get ConfigMaps") + require.Len(t, cms.Items, 1) + require.Equal(t, lighthouseJob, cms.Items[0].Data["lighthousejob.yaml"]) + + cjs, err := kubeClient.BatchV1().CronJobs(namespace).List(context.TODO(), metav1.ListOptions{}) + require.NoError(t, err, "failed to get CronJobs") + require.Len(t, cjs.Items, 1) + cj := cjs.Items[0].Spec + require.Equal(t, "0 4 * * MON-FRI", cj.Schedule) + containers := cj.JobTemplate.Spec.Template.Spec.Containers + require.Len(t, containers, 1) + require.Len(t, containers[0].Args, 2) +} + +func TestInitializePeriodics(t *testing.T) { + namespace, p := setupPeriodicsTest() var enabled = true configAgent := &config.Agent{} @@ -60,7 +97,7 @@ func TestInitializePeriodics(t *testing.T) { p.InitializePeriodics(kubeClient, configAgent, fileBrowsers) - selector := "app=lighthouse-webhooks,component=periodic,repo,trigger" + selector := "app=lighthouse-webhooks,component=periodic,org,repo,trigger" cms, err := kubeClient.CoreV1().ConfigMaps(namespace). List(context.TODO(), metav1.ListOptions{LabelSelector: selector}) require.NoError(t, err, "failed to get ConfigMaps") @@ -77,6 +114,30 @@ func TestInitializePeriodics(t *testing.T) { require.Len(t, containers[0].Args, 2) } +func setupPeriodicsTest() (string, *PeriodicAgent) { + const namespace = "default" + newDefault, data := scmfake.NewDefault() + data.ContentDir = "test_data" + + p := &PeriodicAgent{Namespace: namespace, SCMClient: newDefault} + kubeClient = kubefake.NewSimpleClientset(&v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "config"}, + }) + + kubeClient.PrependReactor( + "patch", + "configmaps", + fakeUpsert, + ) + + kubeClient.PrependReactor( + "patch", + "cronjobs", + fakeUpsert, + ) + return namespace, p +} + func fakeUpsert(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { pa := action.(clienttesting.PatchAction) if pa.GetPatchType() == types.ApplyPatchType { @@ -117,4 +178,4 @@ func fakeUpsert(action clienttesting.Action) (handled bool, ret runtime.Object, return false, nil, nil } -const lighthouseJob = `{"kind":"LighthouseJob","apiVersion":"lighthouse.jenkins.io/v1alpha1","metadata":{"generateName":"testorg-myapp-","creationTimestamp":null,"labels":{"app":"lighthouse-webhooks","component":"periodic","created-by-lighthouse":"true","lighthouse.jenkins-x.io/job":"dailyjob","lighthouse.jenkins-x.io/type":"periodic","repo":"myapp","trigger":"dailyjob"},"annotations":{"lighthouse.jenkins-x.io/job":"dailyjob"}},"spec":{"type":"periodic","agent":"tekton-pipeline","job":"dailyjob","refs":{"org":"testorg","repo":"myapp"},"pipeline_run_spec":{"pipelineSpec":{"tasks":[{"name":"echo-greeting","taskRef":{"name":"task-echo-message"},"params":[{"name":"MESSAGE","value":"$(params.GREETINGS)"},{"name":"BUILD_ID","value":"$(params.BUILD_ID)"},{"name":"JOB_NAME","value":"$(params.JOB_NAME)"},{"name":"JOB_SPEC","value":"$(params.JOB_SPEC)"},{"name":"JOB_TYPE","value":"$(params.JOB_TYPE)"},{"name":"PULL_BASE_REF","value":"$(params.PULL_BASE_REF)"},{"name":"PULL_BASE_SHA","value":"$(params.PULL_BASE_SHA)"},{"name":"PULL_NUMBER","value":"$(params.PULL_NUMBER)"},{"name":"PULL_PULL_REF","value":"$(params.PULL_PULL_REF)"},{"name":"PULL_PULL_SHA","value":"$(params.PULL_PULL_SHA)"},{"name":"PULL_REFS","value":"$(params.PULL_REFS)"},{"name":"REPO_NAME","value":"$(params.REPO_NAME)"},{"name":"REPO_OWNER","value":"$(params.REPO_OWNER)"},{"name":"REPO_URL","value":"$(params.REPO_URL)"}]}],"params":[{"name":"GREETINGS","type":"string","description":"morning greetings, default is Good Morning!","default":"Good Morning!"},{"name":"BUILD_ID","type":"string","description":"the unique build number"},{"name":"JOB_NAME","type":"string","description":"the name of the job which is the trigger context name"},{"name":"JOB_SPEC","type":"string","description":"the specification of the job"},{"name":"JOB_TYPE","type":"string","description":"'the kind of job: postsubmit or presubmit'"},{"name":"PULL_BASE_REF","type":"string","description":"the base git reference of the pull request"},{"name":"PULL_BASE_SHA","type":"string","description":"the git sha of the base of the pull request"},{"name":"PULL_NUMBER","type":"string","description":"git pull request number","default":""},{"name":"PULL_PULL_REF","type":"string","description":"git pull request ref in the form 'refs/pull/$PULL_NUMBER/head'","default":""},{"name":"PULL_PULL_SHA","type":"string","description":"git revision to checkout (branch, tag, sha, ref…)","default":""},{"name":"PULL_REFS","type":"string","description":"git pull reference strings of base and latest in the form 'master:$PULL_BASE_SHA,$PULL_NUMBER:$PULL_PULL_SHA:refs/pull/$PULL_NUMBER/head'"},{"name":"REPO_NAME","type":"string","description":"git repository name"},{"name":"REPO_OWNER","type":"string","description":"git repository owner (user or organisation)"},{"name":"REPO_URL","type":"string","description":"git url to clone"}]}},"pipeline_run_params":[{"name":"GREETINGS"}]},"status":{"startTime":null}}` +const lighthouseJob = `{"kind":"LighthouseJob","apiVersion":"lighthouse.jenkins.io/v1alpha1","metadata":{"generateName":"testorg-myapp-","creationTimestamp":null,"labels":{"app":"lighthouse-webhooks","component":"periodic","created-by-lighthouse":"true","lighthouse.jenkins-x.io/job":"dailyjob","lighthouse.jenkins-x.io/type":"periodic","org":"testorg","repo":"myapp","trigger":"dailyjob"},"annotations":{"lighthouse.jenkins-x.io/job":"dailyjob"}},"spec":{"type":"periodic","agent":"tekton-pipeline","job":"dailyjob","refs":{"org":"testorg","repo":"myapp"},"pipeline_run_spec":{"pipelineSpec":{"tasks":[{"name":"echo-greeting","taskRef":{"name":"task-echo-message"},"params":[{"name":"MESSAGE","value":"$(params.GREETINGS)"},{"name":"BUILD_ID","value":"$(params.BUILD_ID)"},{"name":"JOB_NAME","value":"$(params.JOB_NAME)"},{"name":"JOB_SPEC","value":"$(params.JOB_SPEC)"},{"name":"JOB_TYPE","value":"$(params.JOB_TYPE)"},{"name":"PULL_BASE_REF","value":"$(params.PULL_BASE_REF)"},{"name":"PULL_BASE_SHA","value":"$(params.PULL_BASE_SHA)"},{"name":"PULL_NUMBER","value":"$(params.PULL_NUMBER)"},{"name":"PULL_PULL_REF","value":"$(params.PULL_PULL_REF)"},{"name":"PULL_PULL_SHA","value":"$(params.PULL_PULL_SHA)"},{"name":"PULL_REFS","value":"$(params.PULL_REFS)"},{"name":"REPO_NAME","value":"$(params.REPO_NAME)"},{"name":"REPO_OWNER","value":"$(params.REPO_OWNER)"},{"name":"REPO_URL","value":"$(params.REPO_URL)"}]}],"params":[{"name":"GREETINGS","type":"string","description":"morning greetings, default is Good Morning!","default":"Good Morning!"},{"name":"BUILD_ID","type":"string","description":"the unique build number"},{"name":"JOB_NAME","type":"string","description":"the name of the job which is the trigger context name"},{"name":"JOB_SPEC","type":"string","description":"the specification of the job"},{"name":"JOB_TYPE","type":"string","description":"'the kind of job: postsubmit or presubmit'"},{"name":"PULL_BASE_REF","type":"string","description":"the base git reference of the pull request"},{"name":"PULL_BASE_SHA","type":"string","description":"the git sha of the base of the pull request"},{"name":"PULL_NUMBER","type":"string","description":"git pull request number","default":""},{"name":"PULL_PULL_REF","type":"string","description":"git pull request ref in the form 'refs/pull/$PULL_NUMBER/head'","default":""},{"name":"PULL_PULL_SHA","type":"string","description":"git revision to checkout (branch, tag, sha, ref…)","default":""},{"name":"PULL_REFS","type":"string","description":"git pull reference strings of base and latest in the form 'master:$PULL_BASE_SHA,$PULL_NUMBER:$PULL_PULL_SHA:refs/pull/$PULL_NUMBER/head'"},{"name":"REPO_NAME","type":"string","description":"git repository name"},{"name":"REPO_OWNER","type":"string","description":"git repository owner (user or organisation)"},{"name":"REPO_URL","type":"string","description":"git url to clone"}]}},"pipeline_run_params":[{"name":"GREETINGS"}]},"status":{"startTime":null}}` From 73fa9bc1c5a49b8186b4cf420ff1748b133765dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Tue, 2 Jan 2024 17:17:42 +0100 Subject: [PATCH 07/22] fix: test of Deployment trigger implemented --- pkg/plugins/trigger/deployment.go | 4 +- pkg/plugins/trigger/deployment_test.go | 118 +++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 pkg/plugins/trigger/deployment_test.go diff --git a/pkg/plugins/trigger/deployment.go b/pkg/plugins/trigger/deployment.go index 0d15647dd..8592a5983 100644 --- a/pkg/plugins/trigger/deployment.go +++ b/pkg/plugins/trigger/deployment.go @@ -1,6 +1,8 @@ package trigger import ( + "strings" + "github.com/jenkins-x/go-scm/scm" "github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1" "github.com/jenkins-x/lighthouse/pkg/jobutil" @@ -12,7 +14,7 @@ func handleDeployment(c Client, ds scm.DeploymentStatusHook) error { if j.State != "" && j.State != ds.DeploymentStatus.State { continue } - if j.Environment != "" && j.Environment != ds.Deployment.Environment { + if j.Environment != "" && !strings.EqualFold(j.Environment, ds.Deployment.Environment) { continue } labels := make(map[string]string) diff --git a/pkg/plugins/trigger/deployment_test.go b/pkg/plugins/trigger/deployment_test.go new file mode 100644 index 000000000..e8ee5e930 --- /dev/null +++ b/pkg/plugins/trigger/deployment_test.go @@ -0,0 +1,118 @@ +package trigger + +import ( + "testing" + + "github.com/jenkins-x/go-scm/scm" + "github.com/jenkins-x/lighthouse/pkg/config" + "github.com/jenkins-x/lighthouse/pkg/config/job" + "github.com/jenkins-x/lighthouse/pkg/launcher/fake" + fake2 "github.com/jenkins-x/lighthouse/pkg/scmprovider/fake" + "github.com/sirupsen/logrus" +) + +func TestHandleDeployment(t *testing.T) { + testCases := []struct { + name string + dep *scm.DeploymentStatusHook + jobsToRun int + }{ + { + name: "deploy to production", + dep: &scm.DeploymentStatusHook{ + Deployment: scm.Deployment{ + Sha: "df0442b202e6881b88ab6dad774f63459671ebb0", + Ref: "v1.0.1", + Environment: "Production", + RepositoryLink: "https://api.github.com/respos/org/repo", + }, + DeploymentStatus: scm.DeploymentStatus{ + ID: "123456", + State: "success", + }, + Repo: scm.Repository{ + FullName: "org/repo", + Clone: "https://github.com/org/repo.git", + }, + }, + jobsToRun: 1, + }, + { + name: "deploy to production", + dep: &scm.DeploymentStatusHook{ + Deployment: scm.Deployment{ + Sha: "df0442b202e6881b88ab6dad774f63459671ebb0", + Ref: "v1.0.1", + Environment: "Production", + RepositoryLink: "https://api.github.com/respos/org/repo", + }, + DeploymentStatus: scm.DeploymentStatus{ + ID: "123456", + State: "failed", + }, + Repo: scm.Repository{ + FullName: "org/repo", + Clone: "https://github.com/org/repo.git", + }, + }, + jobsToRun: 0, + }, + { + name: "deploy to production", + dep: &scm.DeploymentStatusHook{ + Deployment: scm.Deployment{ + Sha: "df0442b202e6881b88ab6dad774f63459671ebb0", + Ref: "v1.0.1", + Environment: "Staging", + RepositoryLink: "https://api.github.com/respos/org/repo", + }, + DeploymentStatus: scm.DeploymentStatus{ + ID: "123456", + State: "success", + }, + Repo: scm.Repository{ + FullName: "org/repo", + Clone: "https://github.com/org/repo.git", + }, + }, + jobsToRun: 0, + }, + } + + for _, tc := range testCases { + g := &fake2.SCMClient{} + fakeLauncher := fake.NewLauncher() + c := Client{ + SCMProviderClient: g, + LauncherClient: fakeLauncher, + Config: &config.Config{ProwConfig: config.ProwConfig{LighthouseJobNamespace: "lighthouseJobs"}}, + Logger: logrus.WithField("plugin", pluginName), + } + deployments := map[string][]job.Deployment{ + "org/repo": { + { + Base: job.Base{ + Name: "butter-is-served", + }, + Reporter: job.Reporter{}, + State: "success", + Environment: "production", + }, + }, + } + c.Config.Deployments = deployments + err := handleDeployment(c, *tc.dep) + if err != nil { + t.Errorf("test %q: handlePE returned unexpected error %v", tc.name, err) + } + var numStarted int + for _, job := range fakeLauncher.Pipelines { + t.Logf("created job with context %s", job.Spec.Context) + numStarted++ + } + if numStarted != tc.jobsToRun { + t.Errorf("test %q: expected %d jobs to run, got %d", tc.name, tc.jobsToRun, numStarted) + } + } + +} From 4f718121f81ae785d108c3e06e9753210d17617f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Tue, 2 Jan 2024 17:40:28 +0100 Subject: [PATCH 08/22] fix: prevent panic: assignment to entry in nil map --- pkg/plugins/trigger/periodic.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index 705068ab0..a9b298229 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -143,6 +143,10 @@ func (pa *PeriodicAgent) PeriodicsInitialized(namespace string, kc kubeclient.In } } cmApply, err := applyv1.ExtractConfigMap(cm, fieldManager) + if err != nil { + logrus.Error(errors.Wrapf(err, "failed to extract ConfigMap")) + return true + } cmApply.Data[initializedField] = "pending" cm.Data[initStartedField] = strconv.FormatInt(time.Now().Unix(), 10) _, err = cmInterface.Apply(context.TODO(), cmApply, metav1.ApplyOptions{FieldManager: "lighthouse"}) From f75a3e485eb6a97f1f4fa6899b0d2909244824c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 3 Jan 2024 10:05:54 +0100 Subject: [PATCH 09/22] feat: add feature toggle for initialization of periodics --- pkg/webhook/webhook.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 251dac485..b5741c38c 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "os" + "strconv" "strings" lru "github.com/hashicorp/golang-lru" @@ -520,8 +521,8 @@ func (o *WebhooksController) createHookServer(kc kubeclient.Interface) (*Server, //TokenGenerator: secretAgent.GetTokenGenerator(o.webhookSecretFile), } - // TODO: Add toggle - if !server.PeriodicAgent.PeriodicsInitialized(o.namespace, kc) { + initializePeriodics, _ := strconv.ParseBool(os.Getenv("INITIALIZE_PERIODICS")) + if initializePeriodics && !server.PeriodicAgent.PeriodicsInitialized(o.namespace, kc) { if server.FileBrowsers == nil { ghaSecretDir := util.GetGitHubAppSecretDir() From 1105ba1cc891608cf2f309ca95a2bc60752b3056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 3 Jan 2024 16:20:05 +0100 Subject: [PATCH 10/22] fix: improve logging for deployment status --- pkg/plugins/trigger/deployment.go | 5 +++-- pkg/webhook/events.go | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/plugins/trigger/deployment.go b/pkg/plugins/trigger/deployment.go index 8592a5983..70bf4a7b8 100644 --- a/pkg/plugins/trigger/deployment.go +++ b/pkg/plugins/trigger/deployment.go @@ -32,10 +32,11 @@ func handleDeployment(c Client, ds scm.DeploymentStatusHook) error { labels[scmprovider.EventGUID] = ds.DeploymentStatus.ID pj := jobutil.NewLighthouseJob(jobutil.DeploymentSpec(c.Logger, j, refs), labels, j.Annotations) c.Logger.WithFields(jobutil.LighthouseJobFields(&pj)).Info("Creating a new LighthouseJob.") - if _, err := c.LauncherClient.Launch(&pj); err != nil { + lj, err := c.LauncherClient.Launch(&pj) + if err != nil { return err } - + c.Logger.WithFields(jobutil.LighthouseJobFields(lj)).Debug("LighthouseJob created") } return nil } diff --git a/pkg/webhook/events.go b/pkg/webhook/events.go index 76d49551f..b290c5c80 100644 --- a/pkg/webhook/events.go +++ b/pkg/webhook/events.go @@ -389,7 +389,7 @@ func (s *Server) handleDeploymentStatusEvent(l *logrus.Entry, ds scm.DeploymentS go func(p string, h plugins.DeploymentStatusHandler) { defer s.wg.Done() if err := h(agent, ds); err != nil { - agent.Logger.WithError(err).Error("Error handling ReviewEvent.") + agent.Logger.WithError(err).Error("Error handling DeploymentStatusEvent.") } }(p, h.DeploymentStatusHandler) } From 903db09e68fd11012573e0795882cd275f053b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Thu, 4 Jan 2024 12:06:06 +0100 Subject: [PATCH 11/22] fix: prevent false duplicate job error --- pkg/config/config_test.go | 46 +++++++++++++++++++-------------------- pkg/config/job/config.go | 6 ++--- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f3d0ea039..bddec19e9 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -350,29 +350,29 @@ periodics: - image: alpine`, }, }, - { - name: "duplicated periodics", - prowConfig: ``, - jobConfigs: []string{ - ` -periodics: -- cron: '* * * * *' - agent: tekton - name: foo - spec: - containers: - - image: alpine`, - ` -periodics: -- cron: '* * * * *' - agent: tekton - name: foo - spec: - containers: - - image: alpine`, - }, - expectError: true, - }, + // { + // name: "duplicated periodics", + // prowConfig: ``, + // jobConfigs: []string{ + // ` + //periodics: + //- cron: '* * * * *' + // agent: tekton + // name: foo + // spec: + // containers: + // - image: alpine`, + // ` + //periodics: + //- cron: '* * * * *' + // agent: tekton + // name: foo + // spec: + // containers: + // - image: alpine`, + // }, + // expectError: true, + // }, { name: "one presubmit no context should default", prowConfig: ``, diff --git a/pkg/config/job/config.go b/pkg/config/job/config.go index 7f4191779..fd61d4494 100644 --- a/pkg/config/job/config.go +++ b/pkg/config/job/config.go @@ -161,9 +161,9 @@ func (c *Config) Validate(lh lighthouse.Config) error { validPeriodics := sets.NewString() // Ensure that the periodic durations are valid and specs exist. for _, p := range c.Periodics { - if validPeriodics.Has(p.Name) { - return fmt.Errorf("duplicated periodic job : %s", p.Name) - } + //if validPeriodics.Has(p.Name) { + // return fmt.Errorf("duplicated periodic job : %s", p.Name) + //} validPeriodics.Insert(p.Name) if err := p.Base.Validate(PeriodicJob, lh.PodNamespace); err != nil { return fmt.Errorf("invalid periodic job %s: %v", p.Name, err) From e58cd4a02b9ba7940c471408d5161457c24bace9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Thu, 4 Jan 2024 14:33:30 +0100 Subject: [PATCH 12/22] fix: give permission to lighthouse webhook to update cronjobs --- charts/lighthouse/templates/webhooks-role.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/lighthouse/templates/webhooks-role.yaml b/charts/lighthouse/templates/webhooks-role.yaml index 3c87cabd9..9a17f456b 100644 --- a/charts/lighthouse/templates/webhooks-role.yaml +++ b/charts/lighthouse/templates/webhooks-role.yaml @@ -11,6 +11,7 @@ rules: {{- end }} - configmaps - secrets + - cronjobs verbs: - get - update From c097237508fcb3e4ab972dd60b5ee250f69241c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Thu, 4 Jan 2024 14:33:48 +0100 Subject: [PATCH 13/22] fix: release chart for pullrequest need to protect .git-credentials from test that is erasing password --- .lighthouse/jenkins-x/pullrequest.yaml | 23 +++++++++++++++++++++++ .lighthouse/jenkins-x/release.yaml | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.lighthouse/jenkins-x/pullrequest.yaml b/.lighthouse/jenkins-x/pullrequest.yaml index 530c26971..0538f0464 100755 --- a/.lighthouse/jenkins-x/pullrequest.yaml +++ b/.lighthouse/jenkins-x/pullrequest.yaml @@ -22,6 +22,14 @@ spec: - name: jx-variables - name: build-make-linux resources: {} + - name: protect-credentials + securityContext: + runAsUser: 0 + resources: {} + image: ghcr.io/distroless/busybox + script: | + #!/bin/sh + cp -a /tekton/home/.git-credentials /tekton/home/.git-credentials.bak - name: build-make-test resources: {} - name: build-container-build:webhooks @@ -73,6 +81,21 @@ spec: source .jx/variables.sh cp /tekton/creds-secrets/tekton-container-registry-auth/.dockerconfigjson /kaniko/.docker/config.json /kaniko/executor $KANIKO_FLAGS --context=/workspace/source --dockerfile=docker/gc/Dockerfile --destination=ghcr.io/jenkins-x/lighthouse-gc-jobs:$VERSION --build-arg=VERSION=$VERSION + - image: ghcr.io/jenkins-x/jx-boot:3.10.131 + name: release-chart + resources: {} + script: | + #!/usr/bin/env sh + source .jx/variables.sh + if [ -d "charts/$REPO_NAME" ]; then + jx gitops yset -p version -v "$VERSION" -f ./charts/$REPO_NAME/Chart.yaml + jx gitops yset -p appVersion -v "$VERSION" -f ./charts/$REPO_NAME/Chart.yaml + jx gitops yset -p image.tag -v "$VERSION" -f ./charts/$REPO_NAME/values.yaml; + else echo no charts; fi + + mv /tekton/home/.git-credentials.bak /tekton/home/.git-credentials + + jx gitops helm release podTemplate: {} serviceAccountName: tekton-bot timeout: 240h0m0s diff --git a/.lighthouse/jenkins-x/release.yaml b/.lighthouse/jenkins-x/release.yaml index bf35267da..901b81f73 100755 --- a/.lighthouse/jenkins-x/release.yaml +++ b/.lighthouse/jenkins-x/release.yaml @@ -75,7 +75,7 @@ spec: /kaniko/executor $KANIKO_FLAGS --context=/workspace/source --dockerfile=docker/gc/Dockerfile --destination=ghcr.io/jenkins-x/lighthouse-gc-jobs:$VERSION --destination=ghcr.io/jenkins-x/lighthouse-gc-jobs:latest --build-arg=VERSION=$VERSION - name: chart-docs resources: {} - - image: ghcr.io/jenkins-x/jx-boot:3.10.73 + - image: ghcr.io/jenkins-x/jx-boot:3.10.126 name: changelog resources: {} script: | From c4b1dab0c6820b487de060c9f4eb9ff54744540b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Mon, 8 Jan 2024 11:24:06 +0100 Subject: [PATCH 14/22] chore: upgrade jx-boot --- .lighthouse/jenkins-x/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.lighthouse/jenkins-x/release.yaml b/.lighthouse/jenkins-x/release.yaml index 901b81f73..ae7282c6b 100755 --- a/.lighthouse/jenkins-x/release.yaml +++ b/.lighthouse/jenkins-x/release.yaml @@ -75,7 +75,7 @@ spec: /kaniko/executor $KANIKO_FLAGS --context=/workspace/source --dockerfile=docker/gc/Dockerfile --destination=ghcr.io/jenkins-x/lighthouse-gc-jobs:$VERSION --destination=ghcr.io/jenkins-x/lighthouse-gc-jobs:latest --build-arg=VERSION=$VERSION - name: chart-docs resources: {} - - image: ghcr.io/jenkins-x/jx-boot:3.10.126 + - image: ghcr.io/jenkins-x/jx-boot:3.10.131 name: changelog resources: {} script: | From 6470cd71e098d8bb367730457c89de9ee0d4d414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 10 Jan 2024 11:02:19 +0100 Subject: [PATCH 15/22] fix: give permission to lighthouse webhook to update cronjobs --- charts/lighthouse/templates/webhooks-role.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/charts/lighthouse/templates/webhooks-role.yaml b/charts/lighthouse/templates/webhooks-role.yaml index 9a17f456b..a795ea7df 100644 --- a/charts/lighthouse/templates/webhooks-role.yaml +++ b/charts/lighthouse/templates/webhooks-role.yaml @@ -11,7 +11,6 @@ rules: {{- end }} - configmaps - secrets - - cronjobs verbs: - get - update @@ -26,6 +25,16 @@ rules: - get - list - watch +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - get + - update + - create + - list + - watch - apiGroups: - lighthouse.jenkins.io resources: From a54670215aef46c126181075203a4d130c60c3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 10 Jan 2024 12:01:09 +0100 Subject: [PATCH 16/22] fix: give permission to lighthouse webhook to update cronjobs and configmaps --- charts/lighthouse/templates/webhooks-role.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/charts/lighthouse/templates/webhooks-role.yaml b/charts/lighthouse/templates/webhooks-role.yaml index a795ea7df..b05b32819 100644 --- a/charts/lighthouse/templates/webhooks-role.yaml +++ b/charts/lighthouse/templates/webhooks-role.yaml @@ -17,6 +17,7 @@ rules: - create - list - watch + - patch - apiGroups: - "" resources: @@ -35,6 +36,7 @@ rules: - create - list - watch + - patch - apiGroups: - lighthouse.jenkins.io resources: From 664ac329537e48a45dc6471a5e01a62d078ca336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 10 Jan 2024 13:47:43 +0100 Subject: [PATCH 17/22] fix: create configmap --- pkg/plugins/trigger/periodic.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index a9b298229..8ced695ca 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -320,7 +320,7 @@ func (pa *PeriodicAgent) UpdatePeriodicsForRepo( return true } } else { - cm = (&applyv1.ConfigMapApplyConfiguration{}).WithName(resourceName).WithLabels(labels) + cm = applyv1.ConfigMap(resourceName, pa.Namespace).WithLabels(labels) } if cm.Data == nil { cm.Data = make(map[string]string) From 47126acc590917d0aac6ef33ecbb01f12226878d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 10 Jan 2024 15:08:48 +0100 Subject: [PATCH 18/22] fix: create cronjob --- pkg/plugins/trigger/periodic.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index 8ced695ca..71626b50f 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -386,8 +386,7 @@ func (pa *PeriodicAgent) constructCronJob(resourceName, configMapName string, la if !found { serviceAccount = "lighthouse-webhooks" } - return (&applybatchv1.CronJobApplyConfiguration{}). - WithName(resourceName). + return applybatchv1.CronJob(resourceName, pa.Namespace). WithLabels(labels). WithSpec((&applybatchv1.CronJobSpecApplyConfiguration{}). WithJobTemplate((&applybatchv1.JobTemplateSpecApplyConfiguration{}). From 1587b4fda50bd3f8211d4697fe2d22c2c51ff6e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 10 Jan 2024 15:41:02 +0100 Subject: [PATCH 19/22] fix: make lighthousejob more readable --- pkg/plugins/trigger/periodic.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index 71626b50f..3e35e2d21 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -306,12 +306,12 @@ func (pa *PeriodicAgent) UpdatePeriodicsForRepo( } pj := jobutil.NewLighthouseJob(jobutil.PeriodicSpec(l, p, refs), labels, p.Annotations) - lighthouseData, err := json.Marshal(pj) + lighthouseData, err := json.MarshalIndent(pj, "", " ") // Only apply if any value have changed existingCm := getExistingConfigMap(p) - if existingCm == nil || existingCm.Data["lighthousejob.yaml"] != string(lighthouseData) { + if existingCm == nil || existingCm.Data["lighthousejob.json"] != string(lighthouseData) { var cm *applyv1.ConfigMapApplyConfiguration if existingCm != nil { cm, err = applyv1.ExtractConfigMap(existingCm, fieldManager) @@ -325,7 +325,7 @@ func (pa *PeriodicAgent) UpdatePeriodicsForRepo( if cm.Data == nil { cm.Data = make(map[string]string) } - cm.Data["lighthousejob.yaml"] = string(lighthouseData) + cm.Data["lighthousejob.json"] = string(lighthouseData) _, err := cmInterface.Apply(context.TODO(), cm, metav1.ApplyOptions{Force: true, FieldManager: fieldManager}) if err != nil { @@ -405,7 +405,7 @@ func (pa *PeriodicAgent) constructCronJob(resourceName, configMapName string, la WithCommand("/bin/sh"). WithArgs("-c", ` set -o errexit -create_output=$(kubectl create -f /config/lighthousejob.yaml) +create_output=$(kubectl create -f /config/lighthousejob.json) [[ $create_output =~ (.*)\ ]] kubectl patch ${BASH_REMATCH[1]} --type=merge --subresource status --patch 'status: {state: triggered}' `). From e9d395cbaa78036b9f59945265e3e5ed817ae859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 10 Jan 2024 15:53:53 +0100 Subject: [PATCH 20/22] fix: periodics test --- pkg/plugins/trigger/periodic.go | 2 +- pkg/plugins/trigger/periodic_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index 3e35e2d21..379f82de8 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -306,7 +306,7 @@ func (pa *PeriodicAgent) UpdatePeriodicsForRepo( } pj := jobutil.NewLighthouseJob(jobutil.PeriodicSpec(l, p, refs), labels, p.Annotations) - lighthouseData, err := json.MarshalIndent(pj, "", " ") + lighthouseData, err := json.Marshal(pj) // Only apply if any value have changed existingCm := getExistingConfigMap(p) diff --git a/pkg/plugins/trigger/periodic_test.go b/pkg/plugins/trigger/periodic_test.go index 62d327278..4627d044c 100644 --- a/pkg/plugins/trigger/periodic_test.go +++ b/pkg/plugins/trigger/periodic_test.go @@ -68,7 +68,7 @@ func TestUpdatePeriodics(t *testing.T) { List(context.TODO(), metav1.ListOptions{LabelSelector: selector}) require.NoError(t, err, "failed to get ConfigMaps") require.Len(t, cms.Items, 1) - require.Equal(t, lighthouseJob, cms.Items[0].Data["lighthousejob.yaml"]) + require.Equal(t, lighthouseJob, cms.Items[0].Data["lighthousejob.json"]) cjs, err := kubeClient.BatchV1().CronJobs(namespace).List(context.TODO(), metav1.ListOptions{}) require.NoError(t, err, "failed to get CronJobs") @@ -102,7 +102,7 @@ func TestInitializePeriodics(t *testing.T) { List(context.TODO(), metav1.ListOptions{LabelSelector: selector}) require.NoError(t, err, "failed to get ConfigMaps") require.Len(t, cms.Items, 1) - require.Equal(t, lighthouseJob, cms.Items[0].Data["lighthousejob.yaml"]) + require.Equal(t, lighthouseJob, cms.Items[0].Data["lighthousejob.json"]) cjs, err := kubeClient.BatchV1().CronJobs(namespace).List(context.TODO(), metav1.ListOptions{}) require.NoError(t, err, "failed to get CronJobs") From cefb74a2a7e4035be74355aacb353eb95ea9c291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Wed, 10 Jan 2024 17:04:49 +0100 Subject: [PATCH 21/22] fix: periodics job variables --- pkg/apis/lighthouse/v1alpha1/types.go | 4 ---- pkg/config/job/periodic.go | 2 ++ pkg/plugins/trigger/periodic.go | 8 +++++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/apis/lighthouse/v1alpha1/types.go b/pkg/apis/lighthouse/v1alpha1/types.go index 7e5ee4da4..f57e1692f 100644 --- a/pkg/apis/lighthouse/v1alpha1/types.go +++ b/pkg/apis/lighthouse/v1alpha1/types.go @@ -191,10 +191,6 @@ func (s *LighthouseJobSpec) GetEnvVars() map[string]string { env[JobSpecEnv] = fmt.Sprintf("type:%s", s.Type) - if s.Type == job.PeriodicJob { - return env - } - if s.Refs != nil { env[RepoOwnerEnv] = s.Refs.Org env[RepoNameEnv] = s.Refs.Repo diff --git a/pkg/config/job/periodic.go b/pkg/config/job/periodic.go index 471d7d99e..e2f89da11 100644 --- a/pkg/config/job/periodic.go +++ b/pkg/config/job/periodic.go @@ -22,6 +22,8 @@ type Periodic struct { Reporter // Cron representation of job trigger time Cron string `json:"cron"` + // Branch to run job on. If not set default branch for repository is used + Branch string `json:"branch,omitempty"` } // SetDefaults initializes default values diff --git a/pkg/plugins/trigger/periodic.go b/pkg/plugins/trigger/periodic.go index 379f82de8..6df82bc06 100644 --- a/pkg/plugins/trigger/periodic.go +++ b/pkg/plugins/trigger/periodic.go @@ -301,8 +301,10 @@ func (pa *PeriodicAgent) UpdatePeriodicsForRepo( continue } refs := v1alpha1.Refs{ - Org: org, - Repo: repo, + Org: org, + Repo: repo, + BaseRef: p.Branch, + CloneURI: p.CloneURI, } pj := jobutil.NewLighthouseJob(jobutil.PeriodicSpec(l, p, refs), labels, p.Annotations) @@ -402,7 +404,7 @@ func (pa *PeriodicAgent) constructCronJob(resourceName, configMapName string, la WithContainers((&applyv1.ContainerApplyConfiguration{}). WithName("create-lighthousejob"). WithImage("bitnami/kubectl"). - WithCommand("/bin/sh"). + WithCommand("/bin/bash"). WithArgs("-c", ` set -o errexit create_output=$(kubectl create -f /config/lighthousejob.json) From 12192a8a212f50b8b01fce9d5b21c6e8b473ae80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Svantesson?= Date: Thu, 11 Jan 2024 16:39:57 +0100 Subject: [PATCH 22/22] fix: deletion of cm and cronjob --- charts/lighthouse/templates/webhooks-role.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/charts/lighthouse/templates/webhooks-role.yaml b/charts/lighthouse/templates/webhooks-role.yaml index b05b32819..1474f2cec 100644 --- a/charts/lighthouse/templates/webhooks-role.yaml +++ b/charts/lighthouse/templates/webhooks-role.yaml @@ -18,6 +18,7 @@ rules: - list - watch - patch + - delete - apiGroups: - "" resources: @@ -37,6 +38,7 @@ rules: - list - watch - patch + - delete - apiGroups: - lighthouse.jenkins.io resources: