Add POC of GitHub Webhook Delivery Forwarder (#682)
* Add POC of GitHub Webhook Delivery Forwarder * multi-forwarder and ctrl-c existing and fix for non-woring http post * Rename source files * Extract signal handling into a dedicated source file * Faster ctrl-c handling * Enable automatic creation of repo hook on startup * Add support for forwarding org hook deliveries * Set hook secret on hook creation via envvar (HOOK_SECRET) * Fix org hook support * Fix HOOK_SECRET for consistency * Refactor to prepare for custom log position provider * Refactor to extract inmemory log position provider * Add configmap-based log position provider * Rename githubwebhookdeliveryforwarder to hookdeliveryforwarder * Refactor to rename LogPositionProvider to Checkpointer and extract ConfigMap checkpointer into a dedicated pkg * Refactor to extract logger initialization * Add hookdeliveryforwarder README and bump go-github to unreleased ver
This commit is contained in:
parent
6f130c2db5
commit
f858e2e432
|
|
@ -29,7 +29,7 @@ import (
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
gogithub "github.com/google/go-github/v36/github"
|
gogithub "github.com/google/go-github/v37/github"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/client-go/tools/record"
|
"k8s.io/client-go/tools/record"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package controllers
|
||||||
import (
|
import (
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/pkg/actionsglob"
|
"github.com/actions-runner-controller/actions-runner-controller/pkg/actionsglob"
|
||||||
"github.com/google/go-github/v36/github"
|
"github.com/google/go-github/v37/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchCheckRunEvent(event *github.CheckRunEvent) func(scaleUpTrigger v1alpha1.ScaleUpTrigger) bool {
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchCheckRunEvent(event *github.CheckRunEvent) func(scaleUpTrigger v1alpha1.ScaleUpTrigger) bool {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||||
"github.com/google/go-github/v36/github"
|
"github.com/google/go-github/v37/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchPullRequestEvent(event *github.PullRequestEvent) func(scaleUpTrigger v1alpha1.ScaleUpTrigger) bool {
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchPullRequestEvent(event *github.PullRequestEvent) func(scaleUpTrigger v1alpha1.ScaleUpTrigger) bool {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||||
"github.com/google/go-github/v36/github"
|
"github.com/google/go-github/v37/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchPushEvent(event *github.PushEvent) func(scaleUpTrigger v1alpha1.ScaleUpTrigger) bool {
|
func (autoscaler *HorizontalRunnerAutoscalerGitHubWebhook) MatchPushEvent(event *github.PushEvent) func(scaleUpTrigger v1alpha1.ScaleUpTrigger) bool {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
|
|
||||||
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
actionsv1alpha1 "github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
"github.com/google/go-github/v36/github"
|
"github.com/google/go-github/v37/github"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
github2 "github.com/actions-runner-controller/actions-runner-controller/github"
|
github2 "github.com/actions-runner-controller/actions-runner-controller/github"
|
||||||
"github.com/google/go-github/v36/github"
|
"github.com/google/go-github/v37/github"
|
||||||
|
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/github/fake"
|
"github.com/actions-runner-controller/actions-runner-controller/github/fake"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/hash"
|
"github.com/actions-runner-controller/actions-runner-controller/hash"
|
||||||
gogithub "github.com/google/go-github/v36/github"
|
gogithub "github.com/google/go-github/v37/github"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gogithub "github.com/google/go-github/v36/github"
|
gogithub "github.com/google/go-github/v37/github"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gogithub "github.com/google/go-github/v36/github"
|
gogithub "github.com/google/go-github/v37/github"
|
||||||
|
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
"github.com/actions-runner-controller/actions-runner-controller/api/v1alpha1"
|
||||||
|
|
||||||
"github.com/google/go-github/v36/github"
|
"github.com/google/go-github/v37/github"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/github/metrics"
|
"github.com/actions-runner-controller/actions-runner-controller/github/metrics"
|
||||||
"github.com/bradleyfalzon/ghinstallation"
|
"github.com/bradleyfalzon/ghinstallation"
|
||||||
"github.com/google/go-github/v36/github"
|
"github.com/google/go-github/v37/github"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/actions-runner-controller/actions-runner-controller/github/fake"
|
"github.com/actions-runner-controller/actions-runner-controller/github/fake"
|
||||||
"github.com/google/go-github/v36/github"
|
"github.com/google/go-github/v37/github"
|
||||||
)
|
)
|
||||||
|
|
||||||
var server *httptest.Server
|
var server *httptest.Server
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -7,7 +7,7 @@ require (
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/go-logr/logr v0.4.0
|
github.com/go-logr/logr v0.4.0
|
||||||
github.com/google/go-cmp v0.5.6
|
github.com/google/go-cmp v0.5.6
|
||||||
github.com/google/go-github/v36 v36.0.0
|
github.com/google/go-github/v37 v37.0.0
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/kelseyhightower/envconfig v1.4.0
|
github.com/kelseyhightower/envconfig v1.4.0
|
||||||
github.com/onsi/ginkgo v1.16.4
|
github.com/onsi/ginkgo v1.16.4
|
||||||
|
|
@ -23,3 +23,5 @@ require (
|
||||||
sigs.k8s.io/controller-runtime v0.9.0
|
sigs.k8s.io/controller-runtime v0.9.0
|
||||||
sigs.k8s.io/yaml v1.2.0
|
sigs.k8s.io/yaml v1.2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/google/go-github/v37 => github.com/google/go-github/v37 v37.0.1-0.20210713230028-465df60a8ec3
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -174,8 +174,8 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts=
|
github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts=
|
||||||
github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
|
github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
|
||||||
github.com/google/go-github/v36 v36.0.0 h1:ndCzM616/oijwufI7nBRa+5eZHLldT+4yIB68ib5ogs=
|
github.com/google/go-github/v37 v37.0.1-0.20210713230028-465df60a8ec3 h1:YVfdOQRQ95EjQz0qpGdw9LIzJUflL4FV0EEX3fZ7fH8=
|
||||||
github.com/google/go-github/v36 v36.0.0/go.mod h1:LFlKC047IOqiglRGNqNb9s/iAPTnnjtlshm+bxp+kwk=
|
github.com/google/go-github/v37 v37.0.1-0.20210713230028-465df60a8ec3/go.mod h1:LM7in3NmXDrX58GbEHy7FtNLbI2JijX93RnMKvWG3m4=
|
||||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
`hookdeliveryforwarder` is currently in a Proof-of-Concept phase, not complete, and not supported.
|
||||||
|
That being said, we are likely accept bug reports with concrete reproduction steps, but usage/support issues.
|
||||||
|
|
||||||
|
To use this, you need to write some Kubernetes manifest and a container image for deployment.
|
||||||
|
|
||||||
|
For other information, please see the original pull request introduced it.
|
||||||
|
|
||||||
|
https://github.com/actions-runner-controller/actions-runner-controller/pull/682
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package hookdeliveryforwarder
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Checkpointer interface {
|
||||||
|
GetOrCreate(hookID int64) (*State, error)
|
||||||
|
Update(hookID int64, pos *State) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type InMemoryCheckpointer struct {
|
||||||
|
t time.Time
|
||||||
|
id int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *InMemoryCheckpointer) GetOrCreate(hookID int64) (*State, error) {
|
||||||
|
return &State{DeliveredAt: p.t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *InMemoryCheckpointer) Update(hookID int64, pos *State) error {
|
||||||
|
p.t = pos.DeliveredAt
|
||||||
|
p.id = pos.ID
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInMemoryLogPositionProvider() Checkpointer {
|
||||||
|
return &InMemoryCheckpointer{
|
||||||
|
t: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/actions-runner-controller/actions-runner-controller/pkg/hookdeliveryforwarder"
|
||||||
|
"github.com/actions-runner-controller/actions-runner-controller/pkg/hookdeliveryforwarder/configmap"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
zaplib "go.uber.org/zap"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||||
|
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
logLevelDebug = "debug"
|
||||||
|
logLevelInfo = "info"
|
||||||
|
logLevelWarn = "warn"
|
||||||
|
logLevelError = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scheme = runtime.NewScheme()
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
_ = clientgoscheme.AddToScheme(scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
logLevel string
|
||||||
|
|
||||||
|
checkpointerConfig configmap.Config
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.StringVar(&logLevel, "log-level", logLevelDebug, `The verbosity of the logging. Valid values are "debug", "info", "warn", "error". Defaults to "debug".`)
|
||||||
|
|
||||||
|
checkpointerConfig.InitFlags(flag.CommandLine)
|
||||||
|
|
||||||
|
config := &hookdeliveryforwarder.Config{}
|
||||||
|
|
||||||
|
config.InitFlags((flag.CommandLine))
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
logger := newZapLogger(logLevel)
|
||||||
|
|
||||||
|
checkpointerConfig.Scheme = scheme
|
||||||
|
checkpointerConfig.Logger = logger
|
||||||
|
|
||||||
|
p, mgr, err := configmap.New(&checkpointerConfig)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Set to something that is backed by a CRD so that
|
||||||
|
// restarting the forwarder doesn't result in missing deliveries.
|
||||||
|
config.Checkpointer = p
|
||||||
|
|
||||||
|
ctx := hookdeliveryforwarder.SetupSignalHandler()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := mgr.Start(ctx); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "problem running manager: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
hookdeliveryforwarder.Run(ctx, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newZapLogger(logLevel string) logr.Logger {
|
||||||
|
return zap.New(func(o *zap.Options) {
|
||||||
|
switch logLevel {
|
||||||
|
case logLevelDebug:
|
||||||
|
o.Development = true
|
||||||
|
case logLevelInfo:
|
||||||
|
lvl := zaplib.NewAtomicLevelAt(zaplib.InfoLevel)
|
||||||
|
o.Level = &lvl
|
||||||
|
case logLevelWarn:
|
||||||
|
lvl := zaplib.NewAtomicLevelAt(zaplib.WarnLevel)
|
||||||
|
o.Level = &lvl
|
||||||
|
case logLevelError:
|
||||||
|
lvl := zaplib.NewAtomicLevelAt(zaplib.ErrorLevel)
|
||||||
|
o.Level = &lvl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package hookdeliveryforwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Rules StringSlice
|
||||||
|
MetricsAddr string
|
||||||
|
GitHubConfig github.Config
|
||||||
|
Checkpointer Checkpointer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *Config) InitFlags(fs *flag.FlagSet) {
|
||||||
|
if err := envconfig.Process("github", &config.GitHubConfig); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: Environment variable read failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.StringVar(&config.MetricsAddr, "metrics-addr", ":8000", "The address the metric endpoint binds to.")
|
||||||
|
flag.Var(&config.Rules, "rule", "The rule denotes from where webhook deliveries forwarded and to where they are forwarded. Must be formatted REPO=TARGET where REPO can be just the organization name for a repostory hook or \"owner/repo\" for a repository hook.")
|
||||||
|
flag.StringVar(&config.GitHubConfig.Token, "github-token", config.GitHubConfig.Token, "The personal access token of GitHub.")
|
||||||
|
flag.Int64Var(&config.GitHubConfig.AppID, "github-app-id", config.GitHubConfig.AppID, "The application ID of GitHub App.")
|
||||||
|
flag.Int64Var(&config.GitHubConfig.AppInstallationID, "github-app-installation-id", config.GitHubConfig.AppInstallationID, "The installation ID of GitHub App.")
|
||||||
|
flag.StringVar(&config.GitHubConfig.AppPrivateKey, "github-app-private-key", config.GitHubConfig.AppPrivateKey, "The path of a private key file to authenticate as a GitHub App")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(ctx context.Context, config *Config) {
|
||||||
|
c := config.GitHubConfig
|
||||||
|
|
||||||
|
ghClient, err := c.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: Client creation failed.", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
fwd, err := New(ghClient, []string(config.Rules))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "problem initializing forwarder: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Checkpointer != nil {
|
||||||
|
fwd.Checkpointer = config.Checkpointer
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/readyz", fwd.HandleReadyz)
|
||||||
|
|
||||||
|
srv := http.Server{
|
||||||
|
Addr: config.MetricsAddr,
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer cancel()
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
if err := fwd.Run(ctx); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "problem running forwarder: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer cancel()
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
srv.Shutdown(context.Background())
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := srv.ListenAndServe(); err != nil {
|
||||||
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
fmt.Fprintf(os.Stderr, "problem running http server: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringSlice []string
|
||||||
|
|
||||||
|
func (s *StringSlice) String() string {
|
||||||
|
if s == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%+v", []string(*s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StringSlice) Set(value string) error {
|
||||||
|
*s = append(*s, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package configmap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
||||||
|
"github.com/actions-runner-controller/actions-runner-controller/pkg/hookdeliveryforwarder"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigMapCheckpointer struct {
|
||||||
|
Name string
|
||||||
|
NS string
|
||||||
|
Client client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
DeliveredAt time.Time `json:"delivered_at"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConfigMapCheckpointer) GetOrCreate(hookID int64) (*hookdeliveryforwarder.State, error) {
|
||||||
|
var cm corev1.ConfigMap
|
||||||
|
|
||||||
|
if err := p.Client.Get(context.Background(), types.NamespacedName{Namespace: p.NS, Name: p.Name}, &cm); err != nil {
|
||||||
|
if !kerrors.IsNotFound(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Name = p.Name
|
||||||
|
cm.Namespace = p.NS
|
||||||
|
|
||||||
|
if err := p.Client.Create(context.Background(), &cm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idStr := fmt.Sprintf("hook_%d", hookID)
|
||||||
|
|
||||||
|
var unmarshalled state
|
||||||
|
|
||||||
|
data, ok := cm.Data[idStr]
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
if err := json.Unmarshal([]byte(data), &unmarshalled); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos := &hookdeliveryforwarder.State{
|
||||||
|
DeliveredAt: unmarshalled.DeliveredAt,
|
||||||
|
ID: unmarshalled.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pos.DeliveredAt.IsZero() {
|
||||||
|
pos.DeliveredAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConfigMapCheckpointer) Update(hookID int64, pos *hookdeliveryforwarder.State) error {
|
||||||
|
var cm corev1.ConfigMap
|
||||||
|
|
||||||
|
if err := p.Client.Get(context.Background(), types.NamespacedName{Namespace: p.NS, Name: p.Name}, &cm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var posData state
|
||||||
|
|
||||||
|
posData.DeliveredAt = pos.DeliveredAt
|
||||||
|
posData.ID = pos.ID
|
||||||
|
|
||||||
|
idStr := fmt.Sprintf("hook_%d", hookID)
|
||||||
|
|
||||||
|
data, err := json.Marshal(posData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
copy := cm.DeepCopy()
|
||||||
|
|
||||||
|
if copy.Data == nil {
|
||||||
|
copy.Data = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
copy.Data[idStr] = string(data)
|
||||||
|
|
||||||
|
if err := p.Client.Patch(context.Background(), copy, client.MergeFrom(&cm)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package configmap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Name string
|
||||||
|
Namespace string
|
||||||
|
Logger logr.Logger
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) InitFlags(fs *flag.FlagSet) {
|
||||||
|
fs.StringVar(&c.Name, "configmap-name", "gh-webhook-forwarder", `The name of the Kubernetes ConfigMap to which store state for check-pointing.`)
|
||||||
|
fs.StringVar(&c.Namespace, "namespace", "default", `The Kubernetes namespace to store configmap for check-pointing.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(checkpointerConfig *Config) (*ConfigMapCheckpointer, manager.Manager, error) {
|
||||||
|
ctrl.SetLogger(checkpointerConfig.Logger)
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
|
Scheme: checkpointerConfig.Scheme,
|
||||||
|
LeaderElectionID: "hookdeliveryforwarder",
|
||||||
|
Port: 9443,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("unable to start manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ConfigMapCheckpointer{
|
||||||
|
Client: mgr.GetClient(),
|
||||||
|
Name: checkpointerConfig.Name,
|
||||||
|
NS: checkpointerConfig.Namespace,
|
||||||
|
}, mgr, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
package hookdeliveryforwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||||
|
gogithub "github.com/google/go-github/v37/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Forwarder struct {
|
||||||
|
Repo string
|
||||||
|
Target string
|
||||||
|
|
||||||
|
Hook gogithub.Hook
|
||||||
|
|
||||||
|
PollingDelay time.Duration
|
||||||
|
|
||||||
|
Client *github.Client
|
||||||
|
|
||||||
|
Checkpointer Checkpointer
|
||||||
|
|
||||||
|
logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type persistentError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e persistentError) Error() string {
|
||||||
|
return fmt.Sprintf("%v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forwarder) Run(ctx context.Context) error {
|
||||||
|
pollingDelay := 10 * time.Second
|
||||||
|
if f.PollingDelay > 0 {
|
||||||
|
pollingDelay = f.PollingDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
segments := strings.Split(f.Repo, "/")
|
||||||
|
|
||||||
|
owner := segments[0]
|
||||||
|
|
||||||
|
var repo string
|
||||||
|
|
||||||
|
if len(segments) > 1 {
|
||||||
|
repo = segments[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
hooksAPI := newHooksAPI(f.Client.Client, owner, repo)
|
||||||
|
|
||||||
|
hooks, _, err := hooksAPI.ListHooks(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
f.Errorf("Failed listing hooks: %v", err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var hook *gogithub.Hook
|
||||||
|
|
||||||
|
for i := range hooks {
|
||||||
|
hook = hooks[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if hook == nil {
|
||||||
|
hookConfig := &f.Hook
|
||||||
|
|
||||||
|
if _, ok := hookConfig.Config["url"]; !ok {
|
||||||
|
return persistentError{Err: fmt.Errorf("config.url is missing in the hook config")}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := hookConfig.Config["content_type"]; !ok {
|
||||||
|
hookConfig.Config["content_type"] = "json"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := hookConfig.Config["insecure_ssl"]; !ok {
|
||||||
|
hookConfig.Config["insecure_ssl"] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := hookConfig.Config["secret"]; !ok {
|
||||||
|
hookConfig.Config["secret"] = os.Getenv("GITHUB_HOOK_SECRET")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hookConfig.Events) == 0 {
|
||||||
|
hookConfig.Events = []string{"check_run", "push"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hookConfig.Active == nil {
|
||||||
|
hookConfig.Active = gogithub.Bool(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
h, _, err := hooksAPI.CreateHook(ctx, hookConfig)
|
||||||
|
if err != nil {
|
||||||
|
f.Errorf("Failed creating hook: %v", err)
|
||||||
|
|
||||||
|
return persistentError{Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
hook = h
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Logf("Using this hook for receiving deliveries to be forwarded: %+v", *hook)
|
||||||
|
|
||||||
|
hookDeliveries := newHookDeliveriesAPI(f.Client.Client, owner, repo, hook.GetID())
|
||||||
|
|
||||||
|
cur, err := f.Checkpointer.GetOrCreate(hook.GetID())
|
||||||
|
if err != nil {
|
||||||
|
f.Errorf("Failed to get or create log position: %v", err)
|
||||||
|
|
||||||
|
return persistentError{Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOOP:
|
||||||
|
for {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
payloads [][]byte
|
||||||
|
)
|
||||||
|
|
||||||
|
payloads, cur, err = f.getUnprocessedDeliveries(ctx, hookDeliveries, *cur)
|
||||||
|
if err != nil {
|
||||||
|
f.Errorf("failed getting unprocessed deliveries: %v", err)
|
||||||
|
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range payloads {
|
||||||
|
if _, err := http.Post(f.Target, "application/json", bytes.NewReader(p)); err != nil {
|
||||||
|
f.Errorf("failed forwarding delivery: %v", err)
|
||||||
|
|
||||||
|
retryDelay := 5 * time.Second
|
||||||
|
t := time.NewTimer(retryDelay)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
t.Stop()
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Stop()
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
continue LOOP
|
||||||
|
} else {
|
||||||
|
f.Logf("Successfully POSTed the payload to %s", f.Target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Checkpointer.Update(hook.GetID(), cur); err != nil {
|
||||||
|
return fmt.Errorf("failed updating checkpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.NewTimer(pollingDelay)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
t.Stop()
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Stop()
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
DeliveredAt time.Time
|
||||||
|
ID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forwarder) getUnprocessedDeliveries(ctx context.Context, hookDeliveries *hookDeliveriesAPI, pos State) ([][]byte, *State, error) {
|
||||||
|
var (
|
||||||
|
opts gogithub.ListCursorOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
opts.PerPage = 2
|
||||||
|
|
||||||
|
var deliveries []*gogithub.HookDelivery
|
||||||
|
|
||||||
|
OUTER:
|
||||||
|
for {
|
||||||
|
ds, resp, err := hookDeliveries.ListHookDeliveries(ctx, &opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Cursor = resp.Cursor
|
||||||
|
|
||||||
|
for _, d := range ds {
|
||||||
|
d, _, err := hookDeliveries.GetHookDelivery(ctx, d.GetID())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := d.ParseRequestPayload()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id := d.GetID()
|
||||||
|
deliveredAt := d.GetDeliveredAt()
|
||||||
|
|
||||||
|
if !pos.DeliveredAt.IsZero() && deliveredAt.Before(pos.DeliveredAt) {
|
||||||
|
f.Logf("%s is before %s so skipping all the remaining deliveries", deliveredAt, pos.DeliveredAt)
|
||||||
|
break OUTER
|
||||||
|
}
|
||||||
|
|
||||||
|
if pos.ID != 0 && id <= pos.ID {
|
||||||
|
break OUTER
|
||||||
|
}
|
||||||
|
|
||||||
|
deliveries = append(deliveries, d)
|
||||||
|
|
||||||
|
f.Logf("Received %T at %s: %v", payload, deliveredAt, payload)
|
||||||
|
|
||||||
|
if deliveredAt.After(pos.DeliveredAt) {
|
||||||
|
pos.DeliveredAt = deliveredAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
if id > pos.ID {
|
||||||
|
pos.ID = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Cursor == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(deliveries, func(a, b int) bool {
|
||||||
|
return deliveries[b].GetDeliveredAt().After(deliveries[a].GetDeliveredAt().Time)
|
||||||
|
})
|
||||||
|
|
||||||
|
var payloads [][]byte
|
||||||
|
|
||||||
|
for _, d := range deliveries {
|
||||||
|
payloads = append(payloads, *d.Request.RawPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payloads, &pos, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package hookdeliveryforwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
gogithub "github.com/google/go-github/v37/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hooksAPI struct {
|
||||||
|
ListHooks func(ctx context.Context, opts *gogithub.ListOptions) ([]*gogithub.Hook, *gogithub.Response, error)
|
||||||
|
CreateHook func(ctx context.Context, hook *gogithub.Hook) (*gogithub.Hook, *gogithub.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHooksAPI(client *gogithub.Client, org, repo string) *hooksAPI {
|
||||||
|
var hooksAPI *hooksAPI
|
||||||
|
|
||||||
|
if repo != "" {
|
||||||
|
hooksAPI = repoHooksAPI(client.Repositories, org, repo)
|
||||||
|
} else {
|
||||||
|
hooksAPI = orgHooksAPI(client.Organizations, org)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hooksAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
func repoHooksAPI(svc *gogithub.RepositoriesService, org, repo string) *hooksAPI {
|
||||||
|
return &hooksAPI{
|
||||||
|
ListHooks: func(ctx context.Context, opts *gogithub.ListOptions) ([]*gogithub.Hook, *gogithub.Response, error) {
|
||||||
|
return svc.ListHooks(ctx, org, repo, opts)
|
||||||
|
},
|
||||||
|
CreateHook: func(ctx context.Context, hook *gogithub.Hook) (*gogithub.Hook, *gogithub.Response, error) {
|
||||||
|
return svc.CreateHook(ctx, org, repo, hook)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func orgHooksAPI(svc *gogithub.OrganizationsService, org string) *hooksAPI {
|
||||||
|
return &hooksAPI{
|
||||||
|
ListHooks: func(ctx context.Context, opts *gogithub.ListOptions) ([]*gogithub.Hook, *gogithub.Response, error) {
|
||||||
|
return svc.ListHooks(ctx, org, opts)
|
||||||
|
},
|
||||||
|
CreateHook: func(ctx context.Context, hook *gogithub.Hook) (*gogithub.Hook, *gogithub.Response, error) {
|
||||||
|
return svc.CreateHook(ctx, org, hook)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package hookdeliveryforwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
gogithub "github.com/google/go-github/v37/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hookDeliveriesAPI struct {
|
||||||
|
GetHookDelivery func(ctx context.Context, id int64) (*gogithub.HookDelivery, *gogithub.Response, error)
|
||||||
|
ListHookDeliveries func(ctx context.Context, opts *gogithub.ListCursorOptions) ([]*gogithub.HookDelivery, *gogithub.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHookDeliveriesAPI(client *gogithub.Client, org, repo string, hookID int64) *hookDeliveriesAPI {
|
||||||
|
var hookDeliveries *hookDeliveriesAPI
|
||||||
|
|
||||||
|
if repo != "" {
|
||||||
|
hookDeliveries = repoHookDeliveriesAPI(client.Repositories, org, repo, hookID)
|
||||||
|
} else {
|
||||||
|
hookDeliveries = orgHookDeliveriesAPI(client.Organizations, org, hookID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hookDeliveries
|
||||||
|
}
|
||||||
|
|
||||||
|
func repoHookDeliveriesAPI(svc *gogithub.RepositoriesService, org, repo string, hookID int64) *hookDeliveriesAPI {
|
||||||
|
return &hookDeliveriesAPI{
|
||||||
|
GetHookDelivery: func(ctx context.Context, id int64) (*gogithub.HookDelivery, *gogithub.Response, error) {
|
||||||
|
return svc.GetHookDelivery(ctx, org, repo, hookID, id)
|
||||||
|
},
|
||||||
|
ListHookDeliveries: func(ctx context.Context, opts *gogithub.ListCursorOptions) ([]*gogithub.HookDelivery, *gogithub.Response, error) {
|
||||||
|
return svc.ListHookDeliveries(ctx, org, repo, hookID, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func orgHookDeliveriesAPI(svc *gogithub.OrganizationsService, org string, hookID int64) *hookDeliveriesAPI {
|
||||||
|
return &hookDeliveriesAPI{
|
||||||
|
GetHookDelivery: func(ctx context.Context, id int64) (*gogithub.HookDelivery, *gogithub.Response, error) {
|
||||||
|
return svc.GetHookDelivery(ctx, org, hookID, id)
|
||||||
|
},
|
||||||
|
ListHookDeliveries: func(ctx context.Context, opts *gogithub.ListCursorOptions) ([]*gogithub.HookDelivery, *gogithub.Response, error) {
|
||||||
|
return svc.ListHookDeliveries(ctx, org, hookID, opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package hookdeliveryforwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logger struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f logger) Logf(format string, args ...interface{}) {
|
||||||
|
fmt.Fprintf(os.Stdout, format+"\n", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f logger) Errorf(format string, args ...interface{}) {
|
||||||
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
package hookdeliveryforwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/actions-runner-controller/actions-runner-controller/github"
|
||||||
|
gogithub "github.com/google/go-github/v37/github"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiForwarder struct {
|
||||||
|
client *github.Client
|
||||||
|
|
||||||
|
Rules []Rule
|
||||||
|
|
||||||
|
Checkpointer Checkpointer
|
||||||
|
|
||||||
|
logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleConfig struct {
|
||||||
|
Repo []string `json:"from"`
|
||||||
|
Target string `json:"to"`
|
||||||
|
Hook gogithub.Hook `json:"hook"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
Repo string
|
||||||
|
Target string
|
||||||
|
Hook gogithub.Hook
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(client *github.Client, rules []string) (*MultiForwarder, error) {
|
||||||
|
var srv MultiForwarder
|
||||||
|
|
||||||
|
for _, r := range rules {
|
||||||
|
var rule RuleConfig
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(r), &rule); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed unmarshalling %s: %w", r, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rule.Repo) == 0 {
|
||||||
|
return nil, fmt.Errorf("there must be one or more sources configured via `--repo \"from=SOURCE1,SOURCE2,... to=DEST1,DEST2,...\". got %q", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Target == "" {
|
||||||
|
return nil, fmt.Errorf("there must be one destination configured via `--repo \"from=SOURCE to=DEST1,DEST2,...\". got %q", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, repo := range rule.Repo {
|
||||||
|
srv.Rules = append(srv.Rules, Rule{
|
||||||
|
Repo: repo,
|
||||||
|
Target: rule.Target,
|
||||||
|
Hook: rule.Hook,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.client = client
|
||||||
|
srv.Checkpointer = NewInMemoryLogPositionProvider()
|
||||||
|
|
||||||
|
return &srv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MultiForwarder) Run(ctx context.Context) error {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
errs := make(chan error, len(f.Rules))
|
||||||
|
|
||||||
|
for _, r := range f.Rules {
|
||||||
|
r := r
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
errs <- f.run(ctx, r)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errs:
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MultiForwarder) run(ctx context.Context, rule Rule) error {
|
||||||
|
i := &Forwarder{
|
||||||
|
Repo: rule.Repo,
|
||||||
|
Target: rule.Target,
|
||||||
|
Hook: rule.Hook,
|
||||||
|
Client: f.client,
|
||||||
|
Checkpointer: f.Checkpointer,
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.Run(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MultiForwarder) HandleReadyz(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ok bool
|
||||||
|
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
msg := err.Error()
|
||||||
|
if _, err := w.Write([]byte(msg)); err != nil {
|
||||||
|
f.Errorf("failed writing http error response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r.Body != nil {
|
||||||
|
r.Body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// respond ok to GET / e.g. for health check
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
fmt.Fprintln(w, "webhook server is running")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
if _, err := w.Write([]byte("ok")); err != nil {
|
||||||
|
f.Errorf("failed writing http response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package hookdeliveryforwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var onlyOneSignalHandler = make(chan struct{})
|
||||||
|
|
||||||
|
var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM}
|
||||||
|
|
||||||
|
// SetupSignalHandler registers for SIGTERM and SIGINT. A stop channel is returned
|
||||||
|
// which is closed on one of these signals. If a second signal is caught, the program
|
||||||
|
// is terminated with exit code 1.
|
||||||
|
func SetupSignalHandler() context.Context {
|
||||||
|
close(onlyOneSignalHandler) // panics when called twice
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 2)
|
||||||
|
signal.Notify(c, shutdownSignals...)
|
||||||
|
go func() {
|
||||||
|
<-c
|
||||||
|
cancel()
|
||||||
|
<-c
|
||||||
|
os.Exit(1) // second signal. Exit directly.
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue