Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat allow pipelinerun rerun to have github status update too #1613

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ spec:
- --namespace={{ .Release.Namespace }}
- --dashboard-url={{ .Values.tektoncontroller.dashboardURL }}
- --dashboard-template={{ .Values.tektoncontroller.dashboardTemplate }}
- --rerun-pr-reconciler={{ .Values.tektoncontroller.rerunPrReconciler | default false }}
ports:
- name: metrics
containerPort: 8080
Expand Down
6 changes: 6 additions & 0 deletions charts/lighthouse/templates/tekton-controller-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ rules:
- list
- get
- watch
{{- if .Values.tektoncontroller.rerunPrReconciler }}
- update
{{- end }}
- apiGroups:
- lighthouse.jenkins.io
resources:
- lighthousebreakpoints
- lighthousejobs
verbs:
{{- if .Values.tektoncontroller.rerunPrReconciler }}
- create
{{- end }}
- get
- update
- list
Expand Down
3 changes: 3 additions & 0 deletions charts/lighthouse/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ tektoncontroller:
# tektoncontroller.terminationGracePeriodSeconds -- Termination grace period for tekton controller pods
terminationGracePeriodSeconds: 180

# tektoncontroller.rerunPrReconciler -- Enables the rerun pipelinerun reconciler to run and update rerun pipelinerun status to github
JordanGoasdoue marked this conversation as resolved.
Show resolved Hide resolved
rerunPrReconciler: false

image:
# tektoncontroller.image.repository -- Template for computing the tekton controller docker image repository
repository: "{{ .Values.image.parentRepository }}/lighthouse-tekton-controller"
Expand Down
19 changes: 14 additions & 5 deletions cmd/tektoncontroller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import (
)

type options struct {
namespace string
dashboardURL string
dashboardTemplate string
namespace string
dashboardURL string
dashboardTemplate string
rerunPipelineRunReconciler bool
}

func (o *options) Validate() error {
Expand All @@ -30,6 +31,7 @@ func gatherOptions(fs *flag.FlagSet, args ...string) options {
fs.StringVar(&o.namespace, "namespace", "", "The namespace to listen in")
fs.StringVar(&o.dashboardURL, "dashboard-url", "", "The base URL for the Tekton Dashboard to link to for build reports")
fs.StringVar(&o.dashboardTemplate, "dashboard-template", "", "The template expression for generating the URL to the build report based on the PipelineRun parameters. If not specified defaults to $LIGHTHOUSE_DASHBOARD_TEMPLATE")
fs.BoolVar(&o.rerunPipelineRunReconciler, "rerun-pr-reconciler", false, "Enables the rerun pipelinerun reconciler to run and update rerun pipelinerun status to github")
JordanGoasdoue marked this conversation as resolved.
Show resolved Hide resolved
err := fs.Parse(args)
if err != nil {
logrus.WithError(err).Fatal("Invalid options")
Expand Down Expand Up @@ -64,11 +66,18 @@ func main() {
logrus.WithError(err).Fatal("Unable to start manager")
}

reconciler := tektonengine.NewLighthouseJobReconciler(mgr.GetClient(), mgr.GetAPIReader(), mgr.GetScheme(), o.dashboardURL, o.dashboardTemplate, o.namespace)
if err = reconciler.SetupWithManager(mgr); err != nil {
lhJobReconciler := tektonengine.NewLighthouseJobReconciler(mgr.GetClient(), mgr.GetAPIReader(), mgr.GetScheme(), o.dashboardURL, o.dashboardTemplate, o.namespace)
if err = lhJobReconciler.SetupWithManager(mgr); err != nil {
logrus.WithError(err).Fatal("Unable to create controller")
}

if o.rerunPipelineRunReconciler {
rerunPipelineRunReconciler := tektonengine.NewRerunPipelineRunReconciler(mgr.GetClient(), mgr.GetScheme())
if err = rerunPipelineRunReconciler.SetupWithManager(mgr); err != nil {
logrus.WithError(err).Fatal("Unable to create RerunPipelineRun controller")
}
}

defer interrupts.WaitForGracefulShutdown()
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
logrus.WithError(err).Fatal("Problem running manager")
Expand Down
176 changes: 176 additions & 0 deletions pkg/engines/tekton/pipelinerun_rerun_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package tekton

import (
"context"
"fmt"
"regexp"

"github.com/google/uuid"
lighthousev1alpha1 "github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
"github.com/jenkins-x/lighthouse/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// RerunPipelineRunReconciler reconciles PipelineRun objects with the rerun label
type RerunPipelineRunReconciler struct {
client client.Client
logger *logrus.Entry
scheme *runtime.Scheme
}

// NewRerunPipelineRunReconciler creates a new RerunPipelineRunReconciler
func NewRerunPipelineRunReconciler(client client.Client, scheme *runtime.Scheme) *RerunPipelineRunReconciler {
return &RerunPipelineRunReconciler{
client: client,
logger: logrus.NewEntry(logrus.StandardLogger()).WithField("controller", "RerunPipelineRunController"),
scheme: scheme,
}
}

// SetupWithManager sets up the controller with the Manager.
func (r *RerunPipelineRunReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&pipelinev1beta1.PipelineRun{}).
WithEventFilter(predicate.NewPredicateFuncs(func(object client.Object) bool {
labels := object.GetLabels()
_, exists := labels[util.DashboardTektonRerun]
return exists
})).
Complete(r)
}

// Reconcile handles the reconciliation logic for rerun PipelineRuns
func (r *RerunPipelineRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
r.logger.Infof("Reconciling rerun PipelineRun %s", req.NamespacedName)

// Fetch the Rerun PipelineRun instance
var rerunPipelineRun pipelinev1beta1.PipelineRun
if err := r.client.Get(ctx, req.NamespacedName, &rerunPipelineRun); err != nil {
r.logger.Errorf("Failed to get rerun PipelineRun: %s", err)
return ctrl.Result{}, client.IgnoreNotFound(err)
}

// Check if the Rerun PipelineRun already has an ownerReference set
if len(rerunPipelineRun.OwnerReferences) > 0 {
r.logger.Infof("PipelineRun %s already has an ownerReference set, skipping.", req.NamespacedName)
return ctrl.Result{}, nil
}

// Extract Rerun PipelineRun Parent Name
rerunPipelineRunParentName, ok := rerunPipelineRun.Labels[util.DashboardTektonRerun]
if !ok {
return ctrl.Result{}, nil
}

// Get Rerun PipelineRun parent PipelineRun
var rerunPipelineRunParent pipelinev1beta1.PipelineRun
if err := r.client.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: rerunPipelineRunParentName}, &rerunPipelineRunParent); err != nil {
r.logger.Warningf("Unable to get Rerun Parent PipelineRun %s: %v", rerunPipelineRunParentName, err)
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (we'll need to wait for a new notification), and we can get them
// on deleted requests.
return ctrl.Result{}, client.IgnoreNotFound(err)
}

// Check if the Rerun Parent PipelineRun doesn't already have an ownerReference set
if len(rerunPipelineRunParent.OwnerReferences) == 0 {
r.logger.Infof("Parent PipelineRun %s doesn't already have an ownerReference set, skipping.", rerunPipelineRunParentName)
return ctrl.Result{}, nil
}

// get rerun pipelinerun parent pipelinerun parent lighthousejob
var parentPipelineRunParentLighthouseJob lighthousev1alpha1.LighthouseJob
parentPipelineRunRef := rerunPipelineRunParent.OwnerReferences[0]
if err := r.client.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: parentPipelineRunRef.Name}, &parentPipelineRunParentLighthouseJob); err != nil {
r.logger.Warningf("Unable to get Rerun Parent PipelineRun Parent LighthouseJob %s: %v", parentPipelineRunRef.Name, err)
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (we'll need to wait for a new notification), and we can get them
// on deleted requests.
return ctrl.Result{}, client.IgnoreNotFound(err)
}

// Clone the LighthouseJob
rerunLhJob := parentPipelineRunParentLighthouseJob.DeepCopy()
rerunLhJob.APIVersion = parentPipelineRunParentLighthouseJob.APIVersion
rerunLhJob.Kind = parentPipelineRunParentLighthouseJob.Kind

// Trim existing r-xxxxx suffix and append a new one
re := regexp.MustCompile(`-r-[a-f0-9]{5}$`)
baseName := re.ReplaceAllString(parentPipelineRunParentLighthouseJob.Name, "")
rerunLhJob.Name = fmt.Sprintf("%s-%s", baseName, fmt.Sprintf("r-%s", uuid.NewString()[:5]))

rerunLhJob.ResourceVersion = ""
rerunLhJob.UID = ""

// Create the new LighthouseJob
if err := r.client.Create(ctx, rerunLhJob); err != nil {
r.logger.Errorf("Failed to create new LighthouseJob: %s", err)
return ctrl.Result{}, err
}

// Prepare the ownerReference
ownerReference := metav1.OwnerReference{
APIVersion: parentPipelineRunParentLighthouseJob.APIVersion,
Kind: parentPipelineRunParentLighthouseJob.Kind,
Name: rerunLhJob.Name,
UID: rerunLhJob.UID,
Controller: ptr.To(true),
}

// Set the ownerReference on the PipelineRun
rerunPipelineRun.OwnerReferences = append(rerunPipelineRun.OwnerReferences, ownerReference)

// update ownerReference of rerun PipelineRun
f := func(job *pipelinev1beta1.PipelineRun) error {
// Patch the PipelineRun with the new ownerReference
if err := r.client.Update(ctx, &rerunPipelineRun); err != nil {
return errors.Wrapf(err, "failed to update PipelineRun with ownerReference")
}
return nil
}
err := r.retryModifyPipelineRun(ctx, req.NamespacedName, &rerunPipelineRun, f)
if err != nil {
return ctrl.Result{}, err
}

r.logger.Infof("Successfully patched PipelineRun %s with new ownerReference to LighthouseJob %s", req.NamespacedName, rerunLhJob.Name)

return ctrl.Result{}, nil
}

// retryModifyPipelineRun tries to modify the PipelineRun, retrying if it fails
func (r *RerunPipelineRunReconciler) retryModifyPipelineRun(ctx context.Context, ns client.ObjectKey, pipelineRun *pipelinev1beta1.PipelineRun, f func(pipelineRun *pipelinev1beta1.PipelineRun) error) error {
const retryCount = 5

i := 0
for {
i++
err := f(pipelineRun)
if err == nil {
if i > 1 {
r.logger.Infof("Took %d attempts to update PipelineRun %s", i, pipelineRun.Name)
}
return nil
}
if i >= retryCount {
return fmt.Errorf("failed to update PipelineRun %s after %d attempts: %w", pipelineRun.Name, retryCount, err)
}

if err := r.client.Get(ctx, ns, pipelineRun); err != nil {
r.logger.Warningf("Unable to get PipelineRun %s due to: %s", pipelineRun.Name, err)
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (we'll need to wait for a new notification), and we can get them
// on deleted requests.
return client.IgnoreNotFound(err)
}
}
}
3 changes: 3 additions & 0 deletions pkg/util/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,7 @@ const (

// LighthousePayloadTypeActivity is the activity type
LighthousePayloadTypeActivity = "activity"

// DashboardTektonRerun is added by Tekton when clicking on the Action > Rerun button
DashboardTektonRerun = "dashboard.tekton.dev/rerunOf"
)
Loading