Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions api/v1alpha1/taskspawner_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,10 @@ type GitHubWebhook struct {
// If empty, all events in the Events list trigger tasks.
// +optional
Filters []GitHubWebhookFilter `json:"filters,omitempty"`

// Reporting configures status reporting back to the originating GitHub issue or PR.
// +optional
Reporting *GitHubReporting `json:"reporting,omitempty"`
}

// GitHubWebhookFilter defines filtering criteria for GitHub webhook events.
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion cmd/kelos-spawner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,8 @@ func sourceAnnotations(ts *kelosv1alpha1.TaskSpawner, item source.WorkItem) map[
}

// reportingEnabled returns true when GitHub reporting is configured and enabled
// on the TaskSpawner.
// on the TaskSpawner. This only covers polling-based sources (Issues, PRs);
// webhook-based reporting is handled by the webhook server and its handler.
func reportingEnabled(ts *kelosv1alpha1.TaskSpawner) bool {
if ts.Spec.When.GitHubIssues != nil && ts.Spec.When.GitHubIssues.Reporting != nil {
return ts.Spec.When.GitHubIssues.Reporting.Enabled
Expand Down
26 changes: 26 additions & 0 deletions cmd/kelos-webhook-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,21 @@ func main() {
probeAddr string
webhookAddr string
enableLeaderElection bool
githubOwner string
githubRepo string
githubTokenFile string
githubAPIBaseURL string
)

flag.StringVar(&source, "source", "", "Webhook source type (github or linear)")
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.StringVar(&webhookAddr, "webhook-bind-address", ":8443", "The address the webhook endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager.")
flag.StringVar(&githubOwner, "github-owner", "", "GitHub repository owner for reporting.")
flag.StringVar(&githubRepo, "github-repo", "", "GitHub repository name for reporting.")
flag.StringVar(&githubTokenFile, "github-token-file", "", "Path to file containing GitHub token for reporting.")
flag.StringVar(&githubAPIBaseURL, "github-api-base-url", "", "GitHub API base URL for reporting (defaults to https://api.github.com).")

opts, applyVerbosity := logging.SetupZapOptions(flag.CommandLine)
flag.Parse()
Expand Down Expand Up @@ -120,6 +128,24 @@ func main() {
}
}()

// Set up reporting reconciler when GitHub credentials are provided
if githubOwner != "" && githubRepo != "" {
reportingReconciler := &reportingReconciler{
Client: mgr.GetClient(),
config: reportingConfig{
GitHubOwner: githubOwner,
GitHubRepo: githubRepo,
GitHubTokenFile: githubTokenFile,
GitHubAPIBaseURL: githubAPIBaseURL,
},
}
if err := reportingReconciler.SetupWithManager(mgr); err != nil {
setupLog.Error(err, "Unable to create reporting controller")
os.Exit(1)
}
setupLog.Info("Reporting controller enabled", "owner", githubOwner, "repo", githubRepo)
}

// Add health checks
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "Unable to set up health check")
Expand Down
120 changes: 120 additions & 0 deletions cmd/kelos-webhook-server/reporting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package main

import (
"context"
"fmt"
"os"
"strings"

ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/event"

kelosv1alpha1 "github.com/kelos-dev/kelos/api/v1alpha1"
"github.com/kelos-dev/kelos/internal/reporting"
)

// reportingConfig holds the configuration for the reporting reconciler.
type reportingConfig struct {
GitHubOwner string
GitHubRepo string
GitHubTokenFile string
GitHubAPIBaseURL string
}

// reportingReconciler watches Tasks with GitHub reporting annotations
// and reports their status back to GitHub.
type reportingReconciler struct {
client.Client
config reportingConfig
}

func (r *reportingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := ctrl.Log.WithName("reporting")

var task kelosv1alpha1.Task
if err := r.Get(ctx, req.NamespacedName, &task); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

// Only process tasks with GitHub reporting enabled
if task.Annotations == nil || task.Annotations[reporting.AnnotationGitHubReporting] != "enabled" {
return ctrl.Result{}, nil
}

token, err := readGitHubToken(r.config.GitHubTokenFile)
if err != nil {
return ctrl.Result{}, fmt.Errorf("reading GitHub token for reporting: %w", err)
}

reporter := &reporting.TaskReporter{
Client: r.Client,
Reporter: &reporting.GitHubReporter{
Owner: r.config.GitHubOwner,
Repo: r.config.GitHubRepo,
Token: token,
TokenFile: r.config.GitHubTokenFile,
BaseURL: r.config.GitHubAPIBaseURL,
},
}

if err := reporter.ReportTaskStatus(ctx, &task); err != nil {
log.Error(err, "Reporting task status", "task", task.Name)
return ctrl.Result{}, err
}

return ctrl.Result{}, nil
}

func (r *reportingReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Named("webhook-reporting").
WithOptions(controller.Options{MaxConcurrentReconciles: 1}).
For(&kelosv1alpha1.Task{}, builder.WithPredicates(
reportingAnnotationPredicate{},
)).
Complete(r)
}

// reportingAnnotationPredicate triggers reconciliation when a Task's status
// phase changes. Status sub-resource updates do not bump metadata.generation,
// so GenerationChangedPredicate alone would miss them.
type reportingAnnotationPredicate struct{}

func (reportingAnnotationPredicate) Create(_ event.CreateEvent) bool { return true }
func (reportingAnnotationPredicate) Delete(_ event.DeleteEvent) bool { return false }
func (reportingAnnotationPredicate) Generic(_ event.GenericEvent) bool { return false }

func (reportingAnnotationPredicate) Update(e event.UpdateEvent) bool {
oldTask, ok1 := e.ObjectOld.(*kelosv1alpha1.Task)
newTask, ok2 := e.ObjectNew.(*kelosv1alpha1.Task)
if !ok1 || !ok2 {
return true
}
// Reconcile when the Task phase changes
return oldTask.Status.Phase != newTask.Status.Phase
}

// readGitHubToken reads the GitHub token from a file or environment variable.
func readGitHubToken(tokenFile string) (string, error) {
token := os.Getenv("GITHUB_TOKEN")
if tokenFile == "" {
return token, nil
}

data, err := os.ReadFile(tokenFile)
if err != nil {
if os.IsNotExist(err) {
ctrl.Log.WithName("reporting").Info("Token file not yet available, proceeding without token", "path", tokenFile)
return token, nil
}
return "", fmt.Errorf("reading token file: %w", err)
}

if t := strings.TrimSpace(string(data)); t != "" {
return t, nil
}
return token, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,15 @@ spec:
- event
type: object
type: array
reporting:
description: Reporting configures status reporting back to
the originating GitHub issue or PR.
properties:
enabled:
description: Enabled posts standard status comments back
to the originating GitHub issue or PR.
type: boolean
type: object
repository:
description: |-
Repository restricts webhooks to a specific repository (owner/repo format).
Expand Down
9 changes: 9 additions & 0 deletions internal/manifests/install-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1583,6 +1583,15 @@ spec:
- event
type: object
type: array
reporting:
description: Reporting configures status reporting back to
the originating GitHub issue or PR.
properties:
enabled:
description: Enabled posts standard status comments back
to the originating GitHub issue or PR.
type: boolean
type: object
repository:
description: |-
Repository restricts webhooks to a specific repository (owner/repo format).
Expand Down
30 changes: 30 additions & 0 deletions internal/webhook/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
Expand All @@ -16,6 +17,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/kelos-dev/kelos/api/v1alpha1"
"github.com/kelos-dev/kelos/internal/reporting"
"github.com/kelos-dev/kelos/internal/taskbuilder"
)

Expand Down Expand Up @@ -475,9 +477,37 @@ func (h *WebhookHandler) createTask(ctx context.Context, spawner *v1alpha1.TaskS
return fmt.Errorf("failed to build task: %w", err)
}

// Stamp reporting annotations for GitHub webhook sources when reporting is enabled.
if h.source == GitHubSource && parsed.GitHub != nil && parsed.GitHub.Number > 0 &&
spawner.Spec.When.GitHubWebhook != nil &&
spawner.Spec.When.GitHubWebhook.Reporting != nil &&
spawner.Spec.When.GitHubWebhook.Reporting.Enabled {
if task.Annotations == nil {
task.Annotations = make(map[string]string)
}
task.Annotations[reporting.AnnotationGitHubReporting] = "enabled"
task.Annotations[reporting.AnnotationSourceKind] = webhookSourceKind(eventType, parsed.GitHub)
task.Annotations[reporting.AnnotationSourceNumber] = strconv.Itoa(parsed.GitHub.Number)
}

if err := h.client.Create(ctx, task); err != nil {
return fmt.Errorf("failed to create task: %w", err)
}

return nil
}

// webhookSourceKind determines the reporting source kind from a GitHub webhook event.
func webhookSourceKind(eventType string, eventData *GitHubEventData) string {
switch eventType {
case "pull_request", "pull_request_review", "pull_request_review_comment", "pull_request_target":
return "pull-request"
case "issue_comment":
if eventData.PullRequestAPIURL != "" {
return "pull-request"
}
return "issue"
default:
return "issue"
}
}
Loading
Loading