package plugin import ( "context" "fmt" "io/fs" "os" "path/filepath" "strings" "edge-infra.dev/pkg/lib/logging" "github.com/google/go-cmp/cmp" "github.com/google/go-github/v47/github" "gopkg.in/yaml.v2" utilerrors "k8s.io/apimachinery/pkg/util/errors" "edge-infra.dev/pkg/f8n/devinfra/jack/plugin/options" ) var ( allHandlers = map[string]string{} issueHandlers = map[string]IssueHandler{} issueCommentHandlers = map[string]IssueCommentHandler{} pullRequestHandlers = map[string]PullRequestHandler{} pushEventHandlers = map[string]PushEventHandler{} reviewEventHandlers = map[string]ReviewEventHandler{} reviewCommentEventHandlers = map[string]ReviewCommentEventHandler{} statusEventHandlers = map[string]StatusEventHandler{} workflowEventHandlers = map[string]WorkflowEventHandler{} workflowRunEventHandlers = map[string]WorkflowRunEventHandler{} timeEventHandlers = map[string]TimeEventHandler{} ) func init() { // leaving this for future work } type Config struct { Plugins Plugins `yaml:"plugins,omitempty"` PluginOptions options.PluginOptions `yaml:"plugin_options"` } // Plugins maps orgOrRepo to plugins type Plugins map[string]OrgRepoPlugins type ProjectPlugin struct { IDs []int64 `json:"ids,omitempty"` } // type UploadJobBuckets []UploadJobBucket type UploadJobBucket struct { Name string `yaml:"name"` Bucket string `yaml:"bucket"` // ProjectName string `yaml:"project_name"` // ProjectID string `yaml:"project_id"` } type EpicsPlugin struct{} type IssuesPlugin struct{} type PRPlugin struct{} type DraftPRPlugin struct{} type JackPlugin struct{} type SizePlugin struct{} type OrgRepoPlugins struct { Jack JackPlugin `json:"jack,omitempty"` Epics EpicsPlugin `json:"epics,omitempty"` Issues IssuesPlugin `json:"issues,omitempty"` PR PRPlugin `json:"pr,omitempty"` Project ProjectPlugin `json:"project,omitempty"` Uploadjob []UploadJobBucket `json:"uploadjob,omitempty"` DraftPR DraftPRPlugin `json:"draftpr,omitempty"` Size SizePlugin `json:"size,omitempty"` Enabled []string `json:"enabled,omitempty"` Crons Crons `json:"crons,omitempty"` Webhooks Webhooks `json:"webhooks,omitempty"` } // OrgRepo supercedes org/repo string handling type OrgRepo struct { Org string Repo string } type HandlerParams struct { Ctx context.Context Org string Repo string Client GithubClientInterface ClientV4 GithubV4ClientInterface Log logging.EdgeLogger Params OrgRepoPlugins } // create string from org/repo func OrgRepoToString(org, repo string) string { return fmt.Sprintf("%s/%s", org, repo) } // break down org/repo from string func OrgRepoFromString(orgRepo string) (string, string, error) { orgRepoArr := strings.Split(orgRepo, "/") if len(orgRepoArr) != 2 { return "", "", fmt.Errorf("org/repo string is incorrect %s ", orgRepo) } return orgRepoArr[0], orgRepoArr[1], nil } type Crons map[string]Cron type Cron struct { Org string `yaml:"org"` Repo string `yaml:"repo"` Time string `yaml:"time"` } type Webhooks map[string]Webhook type Webhook struct { Endpoint string `yaml:"endpoint"` } type Configuration struct { Plugins Plugins `json:"plugins,omitempty"` // PluginOptions options.PluginOptions `yaml:"plugin_options"` } // ClientAgent contains the various clients that are attached to the Agent. type ClientAgent struct { GitHubClient github.Client } // ConfigAgent contains the agent mutex and the Agent configuration. type ConfigAgent struct { // mut sync.Mutex configuration *Configuration AllPlugins bool } // get all plugins that have registered an init (includes plugins not in your config) func (pa *ConfigAgent) GetRegisteredPlugins() []string { names := []string{} for plugin := range allHandlers { names = append(names, plugin) } return names } func (pa *ConfigAgent) IsOrgRepoInList(owner, repo string) bool { _, ok := pa.configuration.Plugins[OrgRepoToString(owner, repo)] return ok } // getPlugins returns a list of plugins that are enabled on a given (org, repository). func (pa *ConfigAgent) getPlugins(owner, repo string) []string { return pa.configuration.Plugins[OrgRepoToString(owner, repo)].Enabled } // getCrons returns a list of cron jobs func (pa *ConfigAgent) GetPlugins() Plugins { return pa.configuration.Plugins } // Deprecated, plugin options are now constants // // GetPluginOptions returns a list of all plugin options // func (pa *ConfigAgent) GetPluginOptions() options.PluginOptions { // return pa.configuration.PluginOptions // } // GetPluginParams returns a list of a plugins options func (pa *ConfigAgent) GetPluginParams(owner, repo string) OrgRepoPlugins { return pa.configuration.Plugins[OrgRepoToString(owner, repo)] } // IssueHandlers returns a map of plugin names to handlers for the repo. func (pa *ConfigAgent) IssueHandlers(o, r string) map[string]IssueHandler { // pa.mut.Lock() // defer pa.mut.Unlock() hs := map[string]IssueHandler{} for _, p := range pa.getPlugins(o, r) { if h, ok := issueHandlers[p]; ok { hs[p] = h } } return hs } // ExecuteIssueHandlers calls all functions stored in IssueHandlers map func (pa *ConfigAgent) ExecuteIssueHandlers(hp HandlerParams, e github.IssuesEvent) { p := pa.IssueHandlers(hp.Org, hp.Repo) for plugins := range p { p[plugins](hp, e) } } // IssueCommentHandlers returns a map of plugin names to handlers for the repo. func (pa *ConfigAgent) IssueCommentHandlers(o, r string) map[string]IssueCommentHandler { // pa.mut.Lock() // defer pa.mut.Unlock() hs := map[string]IssueCommentHandler{} for _, p := range pa.getPlugins(o, r) { if h, ok := issueCommentHandlers[p]; ok { hs[p] = h } } return hs } // ExecuteIssueCommentHandlers calls all functions stored in issuecomment map func (pa *ConfigAgent) ExecuteIssueCommentHandlers(hp HandlerParams, event github.IssueCommentEvent) { p := pa.IssueCommentHandlers(hp.Org, hp.Repo) for plugins := range p { p[plugins](hp, event) } } // PullRequestHandlers returns a map of plugin names to handlers for the repo. func (pa *ConfigAgent) PullRequestHandlers(owner, repo string) map[string]PullRequestHandler { // pa.mut.Lock() // defer pa.mut.Unlock() hs := map[string]PullRequestHandler{} for _, p := range pa.getPlugins(owner, repo) { if h, ok := pullRequestHandlers[p]; ok { hs[p] = h } } return hs } // ExecutePullRequestHandlers calls all functions stored in PullRequestHandlers map func (pa *ConfigAgent) ExecutePullRequestHandlers(hp HandlerParams, event github.PullRequestEvent) { p := pa.PullRequestHandlers(hp.Org, hp.Repo) for plugins := range p { p[plugins](hp, event) } } // ReviewEventHandlers returns a map of plugin names to handlers for the repo. func (pa *ConfigAgent) ReviewEventHandlers(owner, repo string) map[string]ReviewEventHandler { // pa.mut.Lock() // defer pa.mut.Unlock() hs := map[string]ReviewEventHandler{} for _, p := range pa.getPlugins(owner, repo) { if h, ok := reviewEventHandlers[p]; ok { hs[p] = h } } return hs } // ExecuteReviewEventHandlers calls all functions stored in ReviewEventHandlers map func (pa *ConfigAgent) ExecuteReviewEventHandlers(hp HandlerParams, event github.PullRequestReviewEvent) { p := pa.ReviewEventHandlers(hp.Org, hp.Repo) for plugins := range p { p[plugins](hp, event) } } // ReviewCommentEventHandlers returns a map of plugin names to handlers for the repo. func (pa *ConfigAgent) ReviewCommentEventHandlers(owner, repo string) map[string]ReviewCommentEventHandler { // pa.mut.Lock() // defer pa.mut.Unlock() hs := map[string]ReviewCommentEventHandler{} for _, p := range pa.getPlugins(owner, repo) { if h, ok := reviewCommentEventHandlers[p]; ok { hs[p] = h } } return hs } // ExecuteReviewCommentEventHandlers calls all functions stored in ReviewCommentEventHandler map func (pa *ConfigAgent) ExecuteReviewCommentEventHandlers(hp HandlerParams, event github.PullRequestReviewCommentEvent) { p := pa.ReviewCommentEventHandlers(hp.Org, hp.Repo) for plugins := range p { p[plugins](hp, event) } } // StatusEventHandlers returns a map of plugin names to handlers for the repo. func (pa *ConfigAgent) StatusEventHandlers(owner, repo string) map[string]StatusEventHandler { // pa.mut.Lock() // defer pa.mut.Unlock() hs := map[string]StatusEventHandler{} for _, p := range pa.getPlugins(owner, repo) { if h, ok := statusEventHandlers[p]; ok { hs[p] = h } } return hs } // ExecuteStatusEventHandlers calls all functions stored in StatusEventHandlers map func (pa *ConfigAgent) ExecuteStatusEventHandlers(hp HandlerParams, event github.StatusEvent) { p := pa.StatusEventHandlers(hp.Org, hp.Repo) for plugins := range p { p[plugins](hp, event) } } // WorkflowEventHandlers returns a map of plugin names to handlers for the repo. func (pa *ConfigAgent) WorkflowEventHandlers(owner, repo string) map[string]WorkflowEventHandler { // pa.mut.Lock() // defer pa.mut.Unlock() hs := map[string]WorkflowEventHandler{} for _, p := range pa.getPlugins(owner, repo) { if h, ok := workflowEventHandlers[p]; ok { hs[p] = h } } return hs } // ExecuteWorkflowEventHandlers calls all functions stored in StatusEventHandlers map func (pa *ConfigAgent) ExecuteWorkflowEventHandlers(hp HandlerParams, event github.WorkflowJobEvent) { p := pa.WorkflowEventHandlers(hp.Org, hp.Repo) for plugins := range p { p[plugins](hp, event) } } // WorkflowRunEventHandlers returns a map of plugin names to handlers for the repo. func (pa *ConfigAgent) WorkflowRunEventHandlers(owner, repo string) map[string]WorkflowRunEventHandler { // pa.mut.Lock() // defer pa.mut.Unlock() hs := map[string]WorkflowRunEventHandler{} for _, p := range pa.getPlugins(owner, repo) { if h, ok := workflowRunEventHandlers[p]; ok { hs[p] = h } } return hs } // ExecuteWorkflowEventHandlers calls all functions stored in StatusEventHandlers map func (pa *ConfigAgent) ExecuteWorkflowRunEventHandlers(hp HandlerParams, event github.WorkflowRunEvent) { p := pa.WorkflowRunEventHandlers(hp.Org, hp.Repo) for plugins := range p { p[plugins](hp, event) } } // ExecuteTimeEventHandlers calls all functions stored in timeEventHandlers map func (pa *ConfigAgent) ExecuteTimeEventHandlers(hp HandlerParams, plugin string) { if _, ok := timeEventHandlers[plugin]; ok { timeEventHandlers[plugin](hp) } } // PushEventHandlers returns a map of plugin names to handlers for the repo. func (pa *ConfigAgent) PushEventHandlers(owner, repo string) map[string]PushEventHandler { // pa.mut.Lock() // defer pa.mut.Unlock() hs := map[string]PushEventHandler{} for _, p := range pa.getPlugins(owner, repo) { if h, ok := pushEventHandlers[p]; ok { hs[p] = h } } return hs } // ExecutePushEventHandlers calls all functions stored in PushEventHandlers map func (pa *ConfigAgent) ExecutePushEventHandlers(hp HandlerParams, event github.PushEvent) { p := pa.PushEventHandlers(hp.Org, hp.Repo) for plugins := range p { p[plugins](hp, event) } } // Start starts polling path for plugin config. If the first attempt fails, // then start returns the error. Future errors will halt updates but not stop. // If checkUnknownPlugins is true, unrecognized plugin names will make config // loading fail. func (pa *ConfigAgent) Start(path string, supplementalPluginConfigDirs []string, supplementalPluginConfigFileSuffix string, checkUnknownPlugins bool) error { if err := pa.Load(path, supplementalPluginConfigDirs, supplementalPluginConfigFileSuffix, checkUnknownPlugins); err != nil { return err } // ticker := time.NewTicker(time.Minute) // go func() { // for range ticker.C { // if err := pa.Load(path, supplementalPluginConfigDirs, supplementalPluginConfigFileSuffix, checkUnknownPlugins); err != nil { // // logrus.WithField("path", path).WithError(err).Error("Error loading plugin config.") // } // } // }() return nil } // Load attempts to load config from the path. It returns an error if either // the file can't be read or the configuration is invalid. // If checkUnknownPlugins is true, unrecognized plugin names will make config // loading fail. func (pa *ConfigAgent) Load(path string, supplementalPluginConfigDirs []string, supplementalPluginConfigFileSuffix string, _ bool) error { b, err := os.ReadFile(path) if err != nil { return err } np := &Configuration{} if err := yaml.Unmarshal(b, np); err != nil { return err } var errs []error for _, supplementalPluginConfigDir := range supplementalPluginConfigDirs { if err := filepath.Walk(supplementalPluginConfigDir, func(path string, info fs.FileInfo, err error) error { if err != nil { return err } if info.IsDir() || !strings.HasSuffix(path, supplementalPluginConfigFileSuffix) { return nil } data, err := os.ReadFile(path) if err != nil { errs = append(errs, fmt.Errorf("failed to read %s: %w", path, err)) return nil } cfg := &Configuration{} if err := yaml.Unmarshal(data, cfg); err != nil { errs = append(errs, fmt.Errorf("failed to unmarshal %s: %w", path, err)) return nil } if err := np.mergeFrom(cfg); err != nil { errs = append(errs, fmt.Errorf("failed to merge config from %s into main config: %w", path, err)) } return nil }); err != nil { errs = append(errs, fmt.Errorf("failed to walk %s: %w", supplementalPluginConfigDir, err)) } } if pa.AllPlugins { pa.setupTesting(np) } pa.Set(np) return nil } // enables all plugins and sets the testing bucket if no other is set func (pa *ConfigAgent) setupTesting(np *Configuration) { for orgRepo := range np.Plugins { orgRepoPlugin := np.Plugins[orgRepo] // set all plugins as enabled orgRepoPlugin.Enabled = pa.GetRegisteredPlugins() // revisit adding default bucket // if no upload job bucket is set use the default // if len(orgRepoPlugin.Uploadjob) == 0 { // orgRepoPlugin.Uploadjob = append(orgRepoPlugin.Uploadjob, UploadJobBucket{ // Name: "testing", // Bucket: storage.DefaultTestingBucket, // }) // } // if orgRepoPlugin.Uploadjob.Bucket == "" { // orgRepoPlugin.Uploadjob.Bucket = storage.DefaultTestingBucket // } np.Plugins[orgRepo] = orgRepoPlugin } } func (c *Configuration) mergeFrom(other *Configuration) error { var errs []error if diff := cmp.Diff(other, &Configuration{Plugins: other.Plugins}); diff != "" { errs = append(errs, fmt.Errorf("supplemental plugin configuration has config that doesn't support merging: %s", diff)) } if c.Plugins == nil { c.Plugins = Plugins{} } if err := c.Plugins.mergeFrom(&other.Plugins); err != nil { errs = append(errs, fmt.Errorf("failed to merge .plugins from supplemental config: %w", err)) } return utilerrors.NewAggregate(errs) } func (p *Plugins) mergeFrom(other *Plugins) error { if other == nil { return nil } if len(*p) == 0 { *p = *other return nil } var errs []error for orgOrRepo, config := range *other { if _, ok := (*p)[orgOrRepo]; ok { errs = append(errs, fmt.Errorf("found duplicate config for plugins.%s", orgOrRepo)) continue } (*p)[orgOrRepo] = config } return utilerrors.NewAggregate(errs) } // Set attempts to set the plugins that are enabled on repos. Plugins are listed // as a map from repositories to the list of plugins that are enabled on them. // Specifying simply an org name will also work, and will enable the plugin on // all repos in the org. func (pa *ConfigAgent) Set(pc *Configuration) { // pa.mut.Lock() // defer pa.mut.Unlock() pa.configuration = pc } // IssueHandler defines the function contract for a github.IssueEvent handler. type IssueHandler func(HandlerParams, github.IssuesEvent) // RegisterIssueHandler registers a plugin's github.IssueEvent handler. func RegisterIssueHandler(name string, fn IssueHandler) { allHandlers[name] = name issueHandlers[name] = fn } // IssueCommentHandler defines the function contract for a github.IssueCommentEvent handler. type IssueCommentHandler func(HandlerParams, github.IssueCommentEvent) // RegisterIssueCommentHandler registers a plugin's github.IssueCommentEvent handler. func RegisterIssueCommentHandler(name string, fn IssueCommentHandler) { allHandlers[name] = name issueCommentHandlers[name] = fn } // PullRequestHandler defines the function contract for a github.PullRequestEvent handler. type PullRequestHandler func(HandlerParams, github.PullRequestEvent) // RegisterPullRequestHandler registers a plugin's github.PullRequestEvent handler. func RegisterPullRequestHandler(name string, fn PullRequestHandler) { allHandlers[name] = name pullRequestHandlers[name] = fn } // StatusEventHandler defines the function contract for a github.StatusEvent handler. type StatusEventHandler func(HandlerParams, github.StatusEvent) // RegisterStatusEventHandler registers a plugin's github.StatusEvent handler. func RegisterStatusEventHandler(name string, fn StatusEventHandler) { allHandlers[name] = name statusEventHandlers[name] = fn } // PushEventHandler defines the function contract for a github.PushEvent handler. type PushEventHandler func(HandlerParams, github.PushEvent) // RegisterPushEventHandler registers a plugin's github.PushEvent handler. func RegisterPushEventHandler(name string, fn PushEventHandler) { allHandlers[name] = name pushEventHandlers[name] = fn } // ReviewEventHandler defines the function contract for a github.ReviewEvent handler. type ReviewEventHandler func(HandlerParams, github.PullRequestReviewEvent) // RegisterReviewEventHandler registers a plugin's github.ReviewEvent handler. func RegisterReviewEventHandler(name string, fn ReviewEventHandler) { allHandlers[name] = name reviewEventHandlers[name] = fn } // ReviewCommentEventHandler defines the function contract for a github.ReviewCommentEvent handler. type ReviewCommentEventHandler func(HandlerParams, github.PullRequestReviewCommentEvent) // RegisterReviewCommentEventHandler registers a plugin's github.ReviewCommentEvent handler. func RegisterReviewCommentEventHandler(name string, fn ReviewCommentEventHandler) { allHandlers[name] = name reviewCommentEventHandlers[name] = fn } // ReviewCommentEventHandler defines the function contract for a github.ReviewCommentEvent handler. type WorkflowEventHandler func(HandlerParams, github.WorkflowJobEvent) // RegisterReviewCommentEventHandler registers a plugin's github.ReviewCommentEvent handler. func RegisterWorkflowEventHandler(name string, fn WorkflowEventHandler) { allHandlers[name] = name workflowEventHandlers[name] = fn } // ReviewCommentEventHandler defines the function contract for a github.ReviewCommentEvent handler. type WorkflowRunEventHandler func(HandlerParams, github.WorkflowRunEvent) // RegisterReviewCommentEventHandler registers a plugin's github.ReviewCommentEvent handler. func RegisterWorkflowRunEventHandler(name string, fn WorkflowRunEventHandler) { allHandlers[name] = name workflowRunEventHandlers[name] = fn } // ReviewCommentEventHandler defines the function contract for a github.ReviewCommentEvent handler. type TimeEventHandler func(HandlerParams) // RegisterReviewCommentEventHandler registers a plugin's github.ReviewCommentEvent handler. func RegisterTimeEventHandler(name string, fn TimeEventHandler) { timeEventHandlers[name] = fn }