From e3d07b81fa5165881698b14b3f806c12b9781c07 Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Wed, 15 Mar 2023 13:27:58 -0400 Subject: [PATCH] Fix: no longer require SRC_GITHUB_TOKEN to be set --- cmd/src/validate_install.go | 10 +- internal/validate/install/config.go | 132 ++++++++++++ internal/validate/install/github.go | 95 ++++++++ internal/validate/install/insight.go | 101 +++++++++ internal/validate/install/install.go | 311 +-------------------------- 5 files changed, 340 insertions(+), 309 deletions(-) create mode 100644 internal/validate/install/config.go create mode 100644 internal/validate/install/github.go create mode 100644 internal/validate/install/insight.go diff --git a/cmd/src/validate_install.go b/cmd/src/validate_install.go index 02a0a26f99..b298543f3d 100644 --- a/cmd/src/validate_install.go +++ b/cmd/src/validate_install.go @@ -11,7 +11,6 @@ import ( "github.com/mattn/go-isatty" "github.com/sourcegraph/src-cli/internal/api" - "github.com/sourcegraph/src-cli/internal/validate" "github.com/sourcegraph/src-cli/internal/validate/install" "github.com/sourcegraph/sourcegraph/lib/errors" @@ -97,14 +96,7 @@ Environmental variables validationSpec = install.DefaultConfig() } - envGithubToken := os.Getenv("SRC_GITHUB_TOKEN") - - // will work for now with only GitHub supported but will need to be revisited when other code hosts are supported - if envGithubToken == "" { - return errors.Newf("%s failed to read `SRC_GITHUB_TOKEN` environment variable", validate.WarningSign) - } - - validationSpec.ExternalService.Config.GitHub.Token = envGithubToken + validationSpec.ExternalService.Config.GitHub.Token = os.Getenv("SRC_GITHUB_TOKEN") return install.Validate(context.Background(), client, validationSpec) } diff --git a/internal/validate/install/config.go b/internal/validate/install/config.go new file mode 100644 index 0000000000..39695dbb25 --- /dev/null +++ b/internal/validate/install/config.go @@ -0,0 +1,132 @@ +package install + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +type ExternalService struct { + // Type of code host, e.g. GITHUB. + Kind string `yaml:"kind"` + + // Display name of external service, e.g. sourcegraph-test. + DisplayName string `yaml:"displayName"` + + // Configuration for code host. + Config Config `yaml:"config"` + + // Maximum retry attempts when cloning test repositories. Defaults to 5 retries. + MaxRetries int `yaml:"maxRetries"` + + // Retry timeout in seconds. Defaults to 5 seconds + RetryTimeoutSeconds int `yaml:"retryTimeoutSeconds"` + + // Delete code host when test is done. Defaults to true. + DeleteWhenDone bool `yaml:"deleteWhenDone"` +} + +// Config for different types of code hosts. +type Config struct { + GitHub GitHub `yaml:"gitHub"` +} + +// GitHub configuration parameters. +type GitHub struct { + // URL used to access your GitHub instance, e.g. https://github.com. + URL string `yaml:"url" json:"url"` + + // Auth token used to authenticate to GitHub instance. This should be provided via env var SRC_GITHUB_TOKEN. + Token string `yaml:"token" json:"token"` + + // List of organizations. + Orgs []string `yaml:"orgs" json:"orgs"` + + // List of repositories to pull. + Repos []string `yaml:"repos" json:"repos"` +} + +type Insight struct { + Title string `yaml:"title"` + DataSeries []map[string]any `yaml:"dataSeries"` + DeleteWhenDone bool `yaml:"deleteWhenDone"` +} + +type ValidationSpec struct { + // Search queries used for validation testing, e.g. "repo:^github\\.com/gorilla/mux$ Router". + SearchQuery []string `yaml:"searchQuery"` + + // External Service configuration. + ExternalService ExternalService `yaml:"externalService"` + + // Insight used for validation testing. + Insight Insight `yaml:"insight"` +} + +// DefaultConfig returns a default configuration to be used for testing. +func DefaultConfig() *ValidationSpec { + return &ValidationSpec{ + SearchQuery: []string{ + "repo:^github.com/sourcegraph/src-cli$ config", + "repo:^github.com/sourcegraph/src-cli$@4.0.0 config", + "repo:^github.com/sourcegraph/src-cli$ type:symbol config", + }, + ExternalService: ExternalService{ + Kind: "GITHUB", + DisplayName: "sourcegraph-test", + Config: Config{ + GitHub: GitHub{ + URL: "https://github.com", + Token: "", + Orgs: []string{}, + Repos: []string{"sourcegraph/src-cli"}, + }, + }, + MaxRetries: 5, + RetryTimeoutSeconds: 5, + DeleteWhenDone: true, + }, + Insight: Insight{ + Title: "test insight", + DataSeries: []map[string]any{ + { + "query": "lang:javascript", + "label": "javascript", + "repositoryScope": "", + "lineColor": "#6495ED", + "timeScopeUnit": "MONTH", + "timeScopeValue": 1, + }, + { + "query": "lang:typescript", + "label": "typescript", + "lineColor": "#DE3163", + "repositoryScope": "", + "timeScopeUnit": "MONTH", + "timeScopeValue": 1, + }, + }, + DeleteWhenDone: true, + }, + } +} + +// LoadYamlConfig will unmarshal a YAML configuration file into a ValidationSpec. +func LoadYamlConfig(userConfig []byte) (*ValidationSpec, error) { + var config ValidationSpec + if err := yaml.Unmarshal(userConfig, &config); err != nil { + return nil, err + } + + return &config, nil +} + +// LoadJsonConfig will unmarshal a JSON configuration file into a ValidationSpec. +func LoadJsonConfig(userConfig []byte) (*ValidationSpec, error) { + var config ValidationSpec + if err := json.Unmarshal(userConfig, &config); err != nil { + return nil, err + } + + return &config, nil +} diff --git a/internal/validate/install/github.go b/internal/validate/install/github.go new file mode 100644 index 0000000000..ec506308c2 --- /dev/null +++ b/internal/validate/install/github.go @@ -0,0 +1,95 @@ +package install + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/validate" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +const GITHUB = "GITHUB" + +func validateGithub(ctx context.Context, client api.Client, config *ValidationSpec) (func(), error) { + // validate external service + log.Printf("%s validating external service", validate.EmojiFingerPointRight) + + srvId, err := addGithubExternalService(ctx, client, config.ExternalService) + if err != nil { + return nil, err + } + + log.Printf("%s external service %s is being added", validate.HourglassEmoji, config.ExternalService.DisplayName) + + cleanupFunc := func() { + if srvId != "" && config.ExternalService.DeleteWhenDone { + _ = removeExternalService(ctx, client, srvId) + log.Printf("%s external service %s has been removed", validate.SuccessEmoji, config.ExternalService.DisplayName) + } + } + + log.Printf("%s cloning repository", validate.HourglassEmoji) + + repo := fmt.Sprintf("github.com/%s", config.ExternalService.Config.GitHub.Repos[0]) + cloned, err := repoCloneTimeout(ctx, client, repo, config.ExternalService) + if err != nil { + return nil, err + } + if !cloned { + return nil, errors.Newf("%s validate failed, repo did not clone\n", validate.FailureEmoji) + } + + log.Printf("%s repositry successfully cloned", validate.SuccessEmoji) + + return cleanupFunc, nil +} + +func addGithubExternalService(ctx context.Context, client api.Client, srv ExternalService) (string, error) { + if srv.Config.GitHub.Token == "" { + return "", errors.Newf("%s failed to read `SRC_GITHUB_TOKEN` environment variable", validate.WarningSign) + } + + cfg, err := json.Marshal(srv.Config.GitHub) + if err != nil { + return "", errors.Wrap(err, "addExternalService failed") + } + + q := clientQuery{ + opName: "AddExternalService", + query: `mutation AddExternalService($kind: ExternalServiceKind!, $displayName: String!, $config: String!) { + addExternalService(input:{ + kind:$kind, + displayName:$displayName, + config: $config + }) + { + id + } + }`, + variables: jsonVars{ + "kind": GITHUB, + "displayName": srv.DisplayName, + "config": string(cfg), + }, + } + + var result struct { + AddExternalService struct { + ID string `json:"id"` + } `json:"addExternalService"` + } + + ok, err := client.NewRequest(q.query, q.variables).Do(ctx, &result) + if err != nil { + return "", errors.Wrap(err, "addExternalService failed") + } + if !ok { + return "", errors.New("addExternalService failed, no data to unmarshal") + } + + return result.AddExternalService.ID, nil +} diff --git a/internal/validate/install/insight.go b/internal/validate/install/insight.go new file mode 100644 index 0000000000..2b24d03c23 --- /dev/null +++ b/internal/validate/install/insight.go @@ -0,0 +1,101 @@ +package install + +import ( + "context" + + "github.com/sourcegraph/src-cli/internal/api" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +func createInsight(ctx context.Context, client api.Client, insight Insight) (string, error) { + var dataSeries []map[string]interface{} + + for _, ds := range insight.DataSeries { + var series = map[string]interface{}{ + "query": ds["query"], + "options": map[string]interface{}{ + "label": ds["label"], + "lineColor": ds["lineColor"], + }, + "repositoryScope": map[string]interface{}{ + "repositories": ds["repositoryScope"], + }, + "timeScope": map[string]interface{}{ + "stepInterval": map[string]interface{}{ + "unit": ds["timeScopeUnit"], + "value": ds["timeScopeValue"], + }, + }, + } + + dataSeries = append(dataSeries, series) + } + + q := clientQuery{ + opName: "CreateLineChartSearchInsight", + query: `mutation CreateLineChartSearchInsight($input: LineChartSearchInsightInput!) { + createLineChartSearchInsight(input: $input) { + view { + id + } + } + }`, + variables: jsonVars{ + "input": map[string]interface{}{ + "options": map[string]interface{}{"title": insight.Title}, + "dataSeries": dataSeries, + }, + }, + } + + var result struct { + CreateLineChartSearchInsight struct { + View struct { + ID string `json:"id"` + } `json:"view"` + } `json:"createLineChartSearchInsight"` + } + + ok, err := client.NewRequest(q.query, q.variables).Do(ctx, &result) + if err != nil { + return "", errors.Wrap(err, "createInsight failed") + } + if !ok { + return "", errors.New("createInsight failed, no data to unmarshal") + } + + return result.CreateLineChartSearchInsight.View.ID, nil +} + +func removeInsight(ctx context.Context, client api.Client, insightId string) error { + q := clientQuery{ + opName: "DeleteInsightView", + query: `mutation DeleteInsightView ($id: ID!) { + deleteInsightView(id: $id){ + alwaysNil + } + }`, + variables: jsonVars{ + "id": insightId, + }, + } + + var result struct { + Data struct { + DeleteInsightView struct { + AlwaysNil string `json:"alwaysNil"` + } `json:"deleteInsightView"` + } `json:"data"` + } + + ok, err := client.NewRequest(q.query, q.variables).Do(ctx, &result) + if err != nil { + return errors.Wrap(err, "removeInsight failed") + } + if !ok { + return errors.New("removeInsight failed, no data to unmarshal") + } + + return nil +} diff --git a/internal/validate/install/install.go b/internal/validate/install/install.go index 1dad644610..d776400a54 100644 --- a/internal/validate/install/install.go +++ b/internal/validate/install/install.go @@ -2,124 +2,15 @@ package install import ( "context" - "encoding/json" "log" - "strings" "time" - "gopkg.in/yaml.v3" - "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/validate" "github.com/sourcegraph/sourcegraph/lib/errors" ) -type ExternalService struct { - // Type of code host, e.g. GITHUB. - Kind string `yaml:"kind"` - - // Display name of external service, e.g. sourcegraph-test. - DisplayName string `yaml:"displayName"` - - // Configuration for code host. - Config Config `yaml:"config"` - - // Maximum retry attempts when cloning test repositories. Defaults to 5 retries. - MaxRetries int `yaml:"maxRetries"` - - // Retry timeout in seconds. Defaults to 5 seconds - RetryTimeoutSeconds int `yaml:"retryTimeoutSeconds"` - - // Delete code host when test is done. Defaults to true. - DeleteWhenDone bool `yaml:"deleteWhenDone"` -} - -// Config for different types of code hosts. -type Config struct { - GitHub GitHub `yaml:"gitHub"` -} - -// GitHub configuration parameters. -type GitHub struct { - // URL used to access your GitHub instance, e.g. https://github.com. - URL string `yaml:"url" json:"url"` - - // Auth token used to authenticate to GitHub instance. This should be provided via env var SRC_GITHUB_TOKEN. - Token string `yaml:"token" json:"token"` - - // List of organizations. - Orgs []string `yaml:"orgs" json:"orgs"` - - // List of repositories to pull. - Repos []string `yaml:"repos" json:"repos"` -} - -type Insight struct { - Title string `yaml:"title"` - DataSeries []map[string]any `yaml:"dataSeries"` - DeleteWhenDone bool `yaml:"deleteWhenDone"` -} - -type ValidationSpec struct { - // Search queries used for validation testing, e.g. "repo:^github\\.com/gorilla/mux$ Router". - SearchQuery []string `yaml:"searchQuery"` - - // External Service configuration. - ExternalService ExternalService `yaml:"externalService"` - - // Insight used for validation testing. - Insight Insight `yaml:"insight"` -} - -// DefaultConfig returns a default configuration to be used for testing. -func DefaultConfig() *ValidationSpec { - return &ValidationSpec{ - SearchQuery: []string{ - "repo:^github.com/sourcegraph/src-cli$ config", - "repo:^github.com/sourcegraph/src-cli$@4.0.0 config", - "repo:^github.com/sourcegraph/src-cli$ type:symbol config", - }, - ExternalService: ExternalService{ - Kind: "GITHUB", - DisplayName: "sourcegraph-test", - Config: Config{ - GitHub: GitHub{ - URL: "https://github.com", - Token: "", - Orgs: []string{}, - Repos: []string{"sourcegraph/src-cli"}, - }, - }, - MaxRetries: 5, - RetryTimeoutSeconds: 5, - DeleteWhenDone: true, - }, - Insight: Insight{ - Title: "test insight", - DataSeries: []map[string]any{ - { - "query": "lang:javascript", - "label": "javascript", - "repositoryScope": "", - "lineColor": "#6495ED", - "timeScopeUnit": "MONTH", - "timeScopeValue": 1, - }, - { - "query": "lang:typescript", - "label": "typescript", - "lineColor": "#DE3163", - "repositoryScope": "", - "timeScopeUnit": "MONTH", - "timeScopeValue": 1, - }, - }, - DeleteWhenDone: true, - }, - } -} - type jsonVars map[string]interface{} type clientQuery struct { @@ -128,62 +19,22 @@ type clientQuery struct { variables jsonVars } -// LoadYamlConfig will unmarshal a YAML configuration file into a ValidationSpec. -func LoadYamlConfig(userConfig []byte) (*ValidationSpec, error) { - var config ValidationSpec - if err := yaml.Unmarshal(userConfig, &config); err != nil { - return nil, err - } - - return &config, nil -} - -// LoadJsonConfig will unmarshal a JSON configuration file into a ValidationSpec. -func LoadJsonConfig(userConfig []byte) (*ValidationSpec, error) { - var config ValidationSpec - if err := json.Unmarshal(userConfig, &config); err != nil { - return nil, err - } - - return &config, nil -} - // Validate runs a series of validation checks such as cloning a repository, running search queries, and // creating insights, based on the configuration provided. func Validate(ctx context.Context, client api.Client, config *ValidationSpec) error { - log.Printf("%s validating external service", validate.EmojiFingerPointRight) - - if config.ExternalService.DisplayName != "" { - srvID, err := addExternalService(ctx, client, config.ExternalService) + switch config.ExternalService.Kind { + case GITHUB: + cleanup, err := validateGithub(ctx, client, config) if err != nil { return err } - - log.Printf("%s external service %s is being added", validate.HourglassEmoji, config.ExternalService.DisplayName) - - defer func() { - if srvID != "" && config.ExternalService.DeleteWhenDone { - _ = removeExternalService(ctx, client, srvID) - log.Printf("%s external service %s has been removed", validate.SuccessEmoji, config.ExternalService.DisplayName) - } - }() - } - - log.Printf("%s cloning repository", validate.HourglassEmoji) - - cloned, err := repoCloneTimeout(ctx, client, config.ExternalService) - if err != nil { - return err - } - if !cloned { - return errors.Newf("%s validate failed, repo did not clone\n", validate.FailureEmoji) + defer cleanup() } - log.Printf("%s repositry successfully cloned", validate.SuccessEmoji) - - log.Printf("%s validating search queries", validate.EmojiFingerPointRight) - + // run search queries if config.SearchQuery != nil { + log.Printf("%s validating search queries", validate.EmojiFingerPointRight) + for i := 0; i < len(config.SearchQuery); i++ { matchCount, err := searchMatchCount(ctx, client, config.SearchQuery[i]) if err != nil { @@ -196,9 +47,9 @@ func Validate(ctx context.Context, client api.Client, config *ValidationSpec) er } } - log.Printf("%s validating code insight", validate.EmojiFingerPointRight) - if config.Insight.Title != "" { + log.Printf("%s validating code insight", validate.EmojiFingerPointRight) + log.Printf("%s insight %s is being added", validate.HourglassEmoji, config.Insight.Title) insightId, err := createInsight(ctx, client, config.Insight) @@ -220,48 +71,6 @@ func Validate(ctx context.Context, client api.Client, config *ValidationSpec) er return nil } -func addExternalService(ctx context.Context, client api.Client, srv ExternalService) (string, error) { - config, err := json.Marshal(srv.Config.GitHub) - if err != nil { - return "", errors.Wrap(err, "addExternalService failed") - } - - q := clientQuery{ - opName: "AddExternalService", - query: `mutation AddExternalService($kind: ExternalServiceKind!, $displayName: String!, $config: String!) { - addExternalService(input:{ - kind:$kind, - displayName:$displayName, - config: $config - }) - { - id - } - }`, - variables: jsonVars{ - "kind": srv.Kind, - "displayName": srv.DisplayName, - "config": string(config), - }, - } - - var result struct { - AddExternalService struct { - ID string `json:"id"` - } `json:"addExternalService"` - } - - ok, err := client.NewRequest(q.query, q.variables).Do(ctx, &result) - if err != nil { - return "", errors.Wrap(err, "addExternalService failed") - } - if !ok { - return "", errors.New("addExternalService failed, no data to unmarshal") - } - - return result.AddExternalService.ID, nil -} - func removeExternalService(ctx context.Context, client api.Client, id string) error { q := clientQuery{ opName: "DeleteExternalService", @@ -321,15 +130,9 @@ func searchMatchCount(ctx context.Context, client api.Client, searchExpr string) return result.Search.Results.MatchCount, nil } -func repoCloneTimeout(ctx context.Context, client api.Client, srv ExternalService) (bool, error) { - // construct repo string for query - var name strings.Builder - - name.WriteString("github.com/") - name.WriteString(srv.Config.GitHub.Repos[0]) - +func repoCloneTimeout(ctx context.Context, client api.Client, repo string, srv ExternalService) (bool, error) { for i := 0; i < srv.MaxRetries; i++ { - repos, err := listClonedRepos(ctx, client, []string{name.String()}) + repos, err := listClonedRepos(ctx, client, []string{repo}) if err != nil { return false, err } @@ -389,95 +192,3 @@ func listClonedRepos(ctx context.Context, client api.Client, names []string) ([] return nodeNames, nil } - -func createInsight(ctx context.Context, client api.Client, insight Insight) (string, error) { - var dataSeries []map[string]interface{} - - for _, ds := range insight.DataSeries { - var series = map[string]interface{}{ - "query": ds["query"], - "options": map[string]interface{}{ - "label": ds["label"], - "lineColor": ds["lineColor"], - }, - "repositoryScope": map[string]interface{}{ - "repositories": ds["repositoryScope"], - }, - "timeScope": map[string]interface{}{ - "stepInterval": map[string]interface{}{ - "unit": ds["timeScopeUnit"], - "value": ds["timeScopeValue"], - }, - }, - } - - dataSeries = append(dataSeries, series) - } - - q := clientQuery{ - opName: "CreateLineChartSearchInsight", - query: `mutation CreateLineChartSearchInsight($input: LineChartSearchInsightInput!) { - createLineChartSearchInsight(input: $input) { - view { - id - } - } - }`, - variables: jsonVars{ - "input": map[string]interface{}{ - "options": map[string]interface{}{"title": insight.Title}, - "dataSeries": dataSeries, - }, - }, - } - - var result struct { - CreateLineChartSearchInsight struct { - View struct { - ID string `json:"id"` - } `json:"view"` - } `json:"createLineChartSearchInsight"` - } - - ok, err := client.NewRequest(q.query, q.variables).Do(ctx, &result) - if err != nil { - return "", errors.Wrap(err, "createInsight failed") - } - if !ok { - return "", errors.New("createInsight failed, no data to unmarshal") - } - - return result.CreateLineChartSearchInsight.View.ID, nil -} - -func removeInsight(ctx context.Context, client api.Client, insightId string) error { - q := clientQuery{ - opName: "DeleteInsightView", - query: `mutation DeleteInsightView ($id: ID!) { - deleteInsightView(id: $id){ - alwaysNil - } - }`, - variables: jsonVars{ - "id": insightId, - }, - } - - var result struct { - Data struct { - DeleteInsightView struct { - AlwaysNil string `json:"alwaysNil"` - } `json:"deleteInsightView"` - } `json:"data"` - } - - ok, err := client.NewRequest(q.query, q.variables).Do(ctx, &result) - if err != nil { - return errors.Wrap(err, "removeInsight failed") - } - if !ok { - return errors.New("removeInsight failed, no data to unmarshal") - } - - return nil -}