| /* |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You 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 impl |
| |
| import ( |
| "context" |
| "fmt" |
| "net/http" |
| "net/url" |
| "reflect" |
| "strings" |
| "time" |
| |
| "github.com/apache/incubator-devlake/core/models/domainlayer/devops" |
| |
| "github.com/apache/incubator-devlake/core/dal" |
| "github.com/apache/incubator-devlake/core/errors" |
| "github.com/apache/incubator-devlake/core/log" |
| "github.com/apache/incubator-devlake/core/plugin" |
| helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api" |
| githubImpl "github.com/apache/incubator-devlake/plugins/github/impl" |
| "github.com/apache/incubator-devlake/plugins/github/models" |
| githubTasks "github.com/apache/incubator-devlake/plugins/github/tasks" |
| "github.com/apache/incubator-devlake/plugins/github_graphql/model/migrationscripts" |
| "github.com/apache/incubator-devlake/plugins/github_graphql/tasks" |
| "github.com/merico-dev/graphql" |
| "golang.org/x/oauth2" |
| ) |
| |
| // make sure interface is implemented |
| var _ interface { |
| plugin.PluginMeta |
| plugin.PluginTask |
| plugin.PluginApi |
| plugin.PluginModel |
| plugin.PluginSource |
| plugin.CloseablePluginTask |
| plugin.PluginMigration |
| } = (*GithubGraphql)(nil) |
| |
| type GithubGraphql struct{} |
| |
| func (p GithubGraphql) Connection() dal.Tabler { |
| return &models.GithubConnection{} |
| } |
| |
| func (p GithubGraphql) Scope() plugin.ToolLayerScope { |
| return &models.GithubRepo{} |
| } |
| |
| func (p GithubGraphql) ScopeConfig() dal.Tabler { |
| return &models.GithubScopeConfig{} |
| } |
| |
| func (p GithubGraphql) Description() string { |
| return "collect some GithubGraphql data" |
| } |
| |
| func (p GithubGraphql) Name() string { |
| return "github_graphql" |
| } |
| |
| func (p GithubGraphql) GetTablesInfo() []dal.Tabler { |
| return []dal.Tabler{ |
| &models.GithubDeployment{}, |
| } |
| } |
| |
| func (p GithubGraphql) SubTaskMetas() []plugin.SubTaskMeta { |
| return []plugin.SubTaskMeta{ |
| // collect millstones |
| githubTasks.CollectMilestonesMeta, |
| githubTasks.ExtractMilestonesMeta, |
| |
| // collect issue & pr, deps on millstone |
| tasks.CollectIssuesMeta, |
| tasks.ExtractIssuesMeta, |
| tasks.CollectPrsMeta, |
| tasks.ExtractPrsMeta, |
| |
| // collect workflow run & job |
| githubTasks.CollectRunsMeta, |
| githubTasks.ExtractRunsMeta, |
| tasks.CollectJobsMeta, |
| tasks.ExtractJobsMeta, |
| |
| // collect others |
| githubTasks.CollectApiCommentsMeta, |
| githubTasks.ExtractApiCommentsMeta, |
| githubTasks.CollectApiEventsMeta, |
| githubTasks.ExtractApiEventsMeta, |
| githubTasks.CollectApiPrReviewCommentsMeta, |
| githubTasks.ExtractApiPrReviewCommentsMeta, |
| |
| // collect account, deps on all before |
| tasks.CollectAccountMeta, |
| tasks.ExtractAccountsMeta, |
| |
| // convert to domain layer |
| githubTasks.ConvertRunsMeta, |
| githubTasks.ConvertJobsMeta, |
| githubTasks.EnrichPullRequestIssuesMeta, |
| githubTasks.ConvertRepoMeta, |
| githubTasks.ConvertIssuesMeta, |
| githubTasks.ConvertCommitsMeta, |
| githubTasks.ConvertIssueLabelsMeta, |
| githubTasks.ConvertPullRequestCommitsMeta, |
| githubTasks.ConvertPullRequestsMeta, |
| githubTasks.ConvertPullRequestReviewsMeta, |
| githubTasks.ConvertPullRequestLabelsMeta, |
| githubTasks.ConvertPullRequestIssuesMeta, |
| githubTasks.ConvertIssueAssigneeMeta, |
| githubTasks.ConvertIssueCommentsMeta, |
| githubTasks.ConvertPullRequestCommentsMeta, |
| githubTasks.ConvertReviewsMeta, |
| githubTasks.ConvertMilestonesMeta, |
| githubTasks.ConvertAccountsMeta, |
| |
| // deployment |
| tasks.CollectDeploymentsMeta, |
| tasks.ExtractDeploymentsMeta, |
| tasks.ConvertDeploymentsMeta, |
| |
| // releases |
| tasks.CollectReleaseMeta, |
| tasks.ExtractReleasesMeta, |
| githubTasks.ConvertReleasesMeta, |
| } |
| } |
| |
| type GraphQueryRateLimit struct { |
| RateLimit *struct { |
| Limit graphql.Int |
| Remaining graphql.Int |
| ResetAt time.Time |
| } |
| } |
| |
| func (p GithubGraphql) PrepareTaskData(taskCtx plugin.TaskContext, options map[string]interface{}) (interface{}, errors.Error) { |
| logger := taskCtx.GetLogger() |
| logger.Debug("%v", options) |
| var op githubTasks.GithubOptions |
| err := helper.Decode(options, &op, nil) |
| if err != nil { |
| return nil, err |
| } |
| connectionHelper := helper.NewConnectionHelper( |
| taskCtx, |
| nil, |
| p.Name(), |
| ) |
| connection := &models.GithubConnection{} |
| err = connectionHelper.FirstById(connection, op.ConnectionId) |
| if err != nil { |
| return nil, errors.Default.Wrap(err, "unable to get github connection by the given connection ID: %v") |
| } |
| |
| apiClient, err := githubTasks.CreateApiClient(taskCtx, connection) |
| if err != nil { |
| return nil, errors.Default.Wrap(err, "unable to get github API client instance") |
| } |
| |
| err = githubImpl.EnrichOptions(taskCtx, &op, apiClient.ApiClient) |
| if err != nil { |
| return nil, err |
| } |
| |
| tokens := strings.Split(connection.Token, ",") |
| src := oauth2.StaticTokenSource( |
| &oauth2.Token{AccessToken: tokens[0]}, |
| ) |
| oauthContext := taskCtx.GetContext() |
| proxy := connection.GetProxy() |
| if proxy != "" { |
| pu, err := url.Parse(proxy) |
| if err != nil { |
| return nil, errors.Convert(err) |
| } |
| if pu.Scheme == "http" || pu.Scheme == "socks5" { |
| proxyClient := &http.Client{ |
| Transport: &http.Transport{Proxy: http.ProxyURL(pu)}, |
| } |
| oauthContext = context.WithValue( |
| taskCtx.GetContext(), |
| oauth2.HTTPClient, |
| proxyClient, |
| ) |
| logger.Debug("Proxy set in oauthContext to %s", proxy) |
| } else { |
| return nil, errors.BadInput.New("Unsupported scheme set in proxy") |
| } |
| } |
| |
| httpClient := oauth2.NewClient(oauthContext, src) |
| endpoint, err := errors.Convert01(url.Parse(connection.Endpoint)) |
| if err != nil { |
| return nil, errors.BadInput.Wrap(err, fmt.Sprintf("malformed connection endpoint supplied: %s", connection.Endpoint)) |
| } |
| |
| // github.com and github enterprise have different graphql endpoints |
| endpoint.Path = "/graphql" // see https://docs.github.com/en/graphql/guides/forming-calls-with-graphql |
| if endpoint.Hostname() != "api.github.com" { |
| // see https://docs.github.com/en/enterprise-server@3.11/graphql/guides/forming-calls-with-graphql |
| endpoint.Path = "/api/graphql" |
| } |
| client := graphql.NewClient(endpoint.String(), httpClient) |
| graphqlClient, err := helper.CreateAsyncGraphqlClient(taskCtx, client, taskCtx.GetLogger(), |
| func(ctx context.Context, client *graphql.Client, logger log.Logger) (rateRemaining int, resetAt *time.Time, err errors.Error) { |
| var query GraphQueryRateLimit |
| dataErrors, err := errors.Convert01(client.Query(taskCtx.GetContext(), &query, nil)) |
| if err != nil { |
| return 0, nil, err |
| } |
| if len(dataErrors) > 0 { |
| return 0, nil, errors.Default.Wrap(dataErrors[0], `query rate limit fail`) |
| } |
| if query.RateLimit == nil { |
| logger.Info(`github graphql rate limit are disabled, fallback to 5000req/hour`) |
| return 5000, nil, nil |
| } |
| logger.Info(`github graphql init success with remaining %d/%d and will reset at %s`, |
| query.RateLimit.Remaining, query.RateLimit.Limit, query.RateLimit.ResetAt) |
| return int(query.RateLimit.Remaining), &query.RateLimit.ResetAt, nil |
| }) |
| if err != nil { |
| return nil, err |
| } |
| |
| graphqlClient.SetGetRateCost(func(q interface{}) int { |
| v := reflect.ValueOf(q) |
| return int(v.Elem().FieldByName(`RateLimit`).FieldByName(`Cost`).Int()) |
| }) |
| |
| regexEnricher := helper.NewRegexEnricher() |
| if err = regexEnricher.TryAdd(devops.DEPLOYMENT, op.ScopeConfig.DeploymentPattern); err != nil { |
| return nil, errors.BadInput.Wrap(err, "invalid value for `deploymentPattern`") |
| } |
| if err = regexEnricher.TryAdd(devops.PRODUCTION, op.ScopeConfig.ProductionPattern); err != nil { |
| return nil, errors.BadInput.Wrap(err, "invalid value for `productionPattern`") |
| } |
| if err = regexEnricher.TryAdd(devops.ENV_NAME_PATTERN, op.ScopeConfig.EnvNamePattern); err != nil { |
| return nil, errors.BadInput.Wrap(err, "invalid value for `envNamePattern`") |
| } |
| |
| taskData := &githubTasks.GithubTaskData{ |
| Options: &op, |
| ApiClient: apiClient, |
| GraphqlClient: graphqlClient, |
| RegexEnricher: regexEnricher, |
| } |
| |
| return taskData, nil |
| } |
| |
| // RootPkgPath information lost when compiled as plugin(.so) |
| func (p GithubGraphql) RootPkgPath() string { |
| return "github.com/apache/incubator-devlake/plugins/githubGraphql" |
| } |
| |
| func (p GithubGraphql) ApiResources() map[string]map[string]plugin.ApiResourceHandler { |
| return nil |
| } |
| |
| func (p GithubGraphql) Close(taskCtx plugin.TaskContext) errors.Error { |
| data, ok := taskCtx.GetData().(*githubTasks.GithubTaskData) |
| if !ok { |
| return errors.Default.New(fmt.Sprintf("GetData failed when try to close %+v", taskCtx)) |
| } |
| data.ApiClient.Release() |
| data.GraphqlClient.Release() |
| return nil |
| } |
| |
| func (p GithubGraphql) MigrationScripts() []plugin.MigrationScript { |
| return migrationscripts.All() |
| } |