From 9ec133664ad5a04f8a3e0d8158d31bc1cfb9f5e4 Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Wed, 11 Jan 2023 21:03:17 -0500 Subject: [PATCH 01/10] Refactor validate functionality into pkg To support expansion of the validate command, the current validate functionality will need to be moved to a new subcommand. * Use existing internal api package for graphql requests * Remove create first admin features * Refactor configuration structs to support multiple code hosts in future * Add cool emoji to output * Add insight test cleanup --- internal/validate/install.go | 476 +++++++++++++++++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 internal/validate/install.go diff --git a/internal/validate/install.go b/internal/validate/install.go new file mode 100644 index 0000000000..1465da4fd0 --- /dev/null +++ b/internal/validate/install.go @@ -0,0 +1,476 @@ +package validate + +import ( + "context" + "encoding/json" + "log" + "strings" + "time" + + "github.com/sourcegraph/src-cli/internal/api" + "gopkg.in/yaml.v3" + + "github.com/sourcegraph/sourcegraph/lib/errors" + "github.com/sourcegraph/sourcegraph/lib/output" +) + +var ( + validateFailureEmoji = output.EmojiFailure + validateSuccessEmoji = output.EmojiSuccess + validateHourglassEmoji = output.EmojiHourglass +) + +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"` +} + +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/gorilla/mux$ Router", "repo:^github\\.com/gorilla/mux$@v1.8.0 Router"}, + ExternalService: ExternalService{ + Kind: "GITHUB", + DisplayName: "sourcegraph-test", + Config: Config{ + GitHub: GitHub{ + URL: "https://github.com", + Token: "", + Orgs: []string{}, + Repos: []string{"gorilla/mux"}, + }, + }, + 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, + }, + }, + }, + } +} + +type jsonVars map[string]interface{} + +type clientQuery struct { + opName string + query string + 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 { + if config.ExternalService.DisplayName != "" { + srvID, err := addExternalService(ctx, client, config.ExternalService) + if err != nil { + return err + } + + log.Printf("%s external service %s is being added\n", validateHourglassEmoji, config.ExternalService.DisplayName) + + defer func() { + if srvID != "" && config.ExternalService.DeleteWhenDone { + _ = removeExternalService(ctx, client, srvID) + log.Printf("%s external service %s has been removed\n", validateSuccessEmoji, config.ExternalService.DisplayName) + } + }() + } + + log.Printf("%s cloning repository\n", validateHourglassEmoji) + + cloned, err := repoCloneTimeout(ctx, client, config.ExternalService) + if err != nil { + return err //TODO make sure errors are wrapped once + } + if !cloned { + return errors.Newf("%s validate failed, repo did not clone\n", validateFailureEmoji) + } + + log.Printf("%s repositry successfully cloned\n", validateSuccessEmoji) + + if config.SearchQuery != nil { + for i := 0; i < len(config.SearchQuery); i++ { + matchCount, err := searchMatchCount(ctx, client, config.SearchQuery[i]) + if err != nil { + return err + } + if matchCount == 0 { + return errors.Newf("validate failed, search query %s returned no results", config.SearchQuery[i]) + } + log.Printf("%s search query '%s' was successful\n", validateSuccessEmoji, config.SearchQuery[i]) + } + } + + if config.Insight.Title != "" { + log.Printf("%s insight %s is being added\n", validateHourglassEmoji, config.Insight.Title) + + insightId, err := createInsight(ctx, client, config.Insight) + if err != nil { + return err + } + + log.Printf("%s insight successfully added\n", validateSuccessEmoji) + + defer func() { + if insightId != "" { + _ = removeInsight(ctx, client, insightId) + log.Printf("%s insight %s has been removed\n", validateSuccessEmoji, config.Insight.Title) + + } + }() + } + + 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", + query: `mutation DeleteExternalService($id: ID!) { + deleteExternalService(externalService: $id){ + alwaysNil + } + }`, + variables: jsonVars{ + "id": id, + }, + } + + var result struct{} + + ok, err := client.NewRequest(q.query, q.variables).Do(ctx, &result) + if err != nil { + return errors.Wrap(err, "removeExternalService failed") + } + if !ok { + return errors.New("removeExternalService failed, no data to unmarshal") + } + return nil +} + +func searchMatchCount(ctx context.Context, client api.Client, searchExpr string) (int, error) { + q := clientQuery{ + opName: "SearchMatchCount", + query: `query ($query: String!) { + search(query: $query, version: V2, patternType:literal){ + results { + matchCount + } + } + }`, + variables: jsonVars{ + "query": searchExpr, + }, + } + + var result struct { + Search struct { + Results struct { + MatchCount int `json:"matchCount"` + } `json:"results"` + } `json:"search"` + } + + ok, err := client.NewRequest(q.query, q.variables).Do(ctx, &result) + if err != nil { + return 0, errors.Wrap(err, "searchMatchCount failed") + } + if !ok { + return 0, errors.New("searchMatchCount failed, no data to unmarshal") + } + + 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]) + + for i := 0; i < srv.MaxRetries; i++ { + repos, err := listClonedRepos(ctx, client, []string{name.String()}) + if err != nil { + return false, err + } + if len(repos) >= 1 { + return true, nil + } + time.Sleep(time.Second * time.Duration(srv.RetryTimeoutSeconds)) + } + return false, nil +} + +func listClonedRepos(ctx context.Context, client api.Client, names []string) ([]string, error) { + q := clientQuery{ + opName: "ListRepos", + query: `query ListRepos($names: [String!]) { + repositories( + names: $names + ) { + nodes { + name + mirrorInfo { + cloned + } + } + } + }`, + variables: jsonVars{ + "names": names, + }, + } + + var result struct { + Repositories struct { + Nodes []struct { + Name string `json:"name"` + MirrorInfo struct { + Cloned bool `json:"cloned"` + } `json:"mirrorInfo"` + } `json:"nodes"` + } `json:"repositories"` + } + + ok, err := client.NewRequest(q.query, q.variables).Do(ctx, &result) + if err != nil { + return nil, errors.Wrap(err, "listClonedRepos failed") + } + if !ok { + return nil, errors.New("listClonedRepos failed, no data to unmarshal") + } + + nodeNames := make([]string, 0, len(result.Repositories.Nodes)) + for _, node := range result.Repositories.Nodes { + if node.MirrorInfo.Cloned { + nodeNames = append(nodeNames, node.Name) + } + } + + 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 +} From 26d8e61f44552841a27f7a643625a6ba8ca32854 Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Wed, 11 Jan 2023 21:18:52 -0500 Subject: [PATCH 02/10] Add env vars for Github access token Github access token is needed for new validate command. Add token to config. --- cmd/src/main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/src/main.go b/cmd/src/main.go index 9e63c49762..8d9dac8dba 100644 --- a/cmd/src/main.go +++ b/cmd/src/main.go @@ -80,6 +80,7 @@ var cfg *config type config struct { Endpoint string `json:"endpoint"` AccessToken string `json:"accessToken"` + GithubToken string `json:"githubToken"` AdditionalHeaders map[string]string `json:"additionalHeaders"` ConfigFilePath string @@ -133,6 +134,7 @@ func readConfig() (*config, error) { envToken := os.Getenv("SRC_ACCESS_TOKEN") envEndpoint := os.Getenv("SRC_ENDPOINT") + envGithubToken := os.Getenv("SRC_GITHUB_TOKEN") if userSpecified { // If a config file is present, either zero or both environment variables must be present. @@ -149,6 +151,9 @@ func readConfig() (*config, error) { if envToken != "" { cfg.AccessToken = envToken } + if envGithubToken != "" { + cfg.GithubToken = envGithubToken + } if envEndpoint != "" { cfg.Endpoint = envEndpoint } From 2368a0cbd69e0cd069250009644d23029079ee8c Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Wed, 11 Jan 2023 21:57:31 -0500 Subject: [PATCH 03/10] Add 'src validate install' command * Move 'src validate' functionality into 'srv validate install' commmand to allow for more validate subcommands. --- cmd/src/validate.go | 774 +----------------------------------- cmd/src/validate_install.go | 104 +++++ 2 files changed, 121 insertions(+), 757 deletions(-) create mode 100644 cmd/src/validate_install.go diff --git a/cmd/src/validate.go b/cmd/src/validate.go index 2b5ee3bc9e..4e097de5ce 100644 --- a/cmd/src/validate.go +++ b/cmd/src/validate.go @@ -1,783 +1,43 @@ package main import ( - "bytes" - "context" - "encoding/json" "flag" "fmt" - "io" - "net/http" - "os" - "strings" - "text/template" - "time" - - "github.com/sourcegraph/sourcegraph/lib/errors" - - jsoniter "github.com/json-iterator/go" - "github.com/mattn/go-isatty" - "gopkg.in/yaml.v3" - - "github.com/sourcegraph/src-cli/internal/api" - "github.com/sourcegraph/src-cli/internal/cmderrors" ) -type validationSpec struct { - FirstAdmin struct { - Email string `yaml:"email"` - Username string `yaml:"username"` - Password string `yaml:"password"` - CreateAccessToken bool `yaml:"createAccessToken"` - } `yaml:"firstAdmin"` - WaitRepoCloned struct { - Repo string `yaml:"repo"` - MaxTries int `yaml:"maxTries"` - SleepBetweenTriesSeconds int `yaml:"sleepBetweenTriesSeconds"` - } `yaml:"waitRepoCloned"` - SearchQuery []string `yaml:"searchQuery"` - ExternalService struct { - Kind string `yaml:"kind"` - DisplayName string `yaml:"displayName"` - Config map[string]interface{} `yaml:"config"` - DeleteWhenDone bool `yaml:"deleteWhenDone"` - } `yaml:"externalService"` - CreateInsight struct { - Title string `yaml:"title"` - DataSeries []map[string]interface{} `yaml:"dataSeries"` - } `yaml:"createInsight"` -} - -type validator struct { - client *vdClient - apiClient api.Client -} - -const defaultVspec = `{ - "externalService": { - "config": { - "url": "https://github.com", - "token": "{{ .github_token }}", - "orgs": [], - "repos": [ - "gorilla/mux" - ] - }, - "kind": "GITHUB", - "displayName": "footest", - "deleteWhenDone": true - }, - "waitRepoCloned": { - "repo": "github.com/gorilla/mux", - "maxTries": 5, - "sleepBetweenTriesSeconds": 5 - }, - "searchQuery": ["repo:^github.com/gorilla/mux$ Router", "repo:^github.com/gorilla/mux$@v1.8.0 Router"], - "createInsight": { - "title": "test insight", - "dataSeries": [ - { - "query": "lang:javascript", - "label": "javascript", - "repositoryScope": [], - "lineColor": "#6495ED", - "timeScopeUnit": "MONTH", - "timeScopeValue": 1 - }, - { - "query": "lang:typescript", - "label": "typescript", - "lineColor": "#DE3163", - "repositoryScope": [], - "timeScopeUnit": "MONTH", - "timeScopeValue": 1 - } - ] - } -}` +var validateCommands commander func init() { usage := `'src validate' is a tool that validates a Sourcegraph instance. EXPERIMENTAL: 'validate' is an experimental command in the 'src' tool. +Please visit https://docs.sourcegraph.com/admin/validation for documentation of the validate command. + Usage: - src validate [options] src-validate.yml -or - cat src-validate.yml | src validate [options] + src validate command [command options] -if user is authenticated, user can also run default checks +The commands are: - src validate [options] + install validates a Sourcegraph installation -Please visit https://docs.sourcegraph.com/admin/validation for documentation of the validate command. +Use "src validate [command] -h" for more information about a command. ` - flagSet := flag.NewFlagSet("validate", flag.ExitOnError) - usageFunc := func() { - _, _ = fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src validate %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - - var ( - contextFlag = flagSet.String("context", "", `Comma-separated list of key=value pairs to add to the script execution context`) - secretsFlag = flagSet.String("secrets", "", "Path to a file containing key=value lines. The key value pairs will be added to the script context") - apiFlags = api.NewFlags(flagSet) - ) + flagSet := flag.NewFlagSet("validate", flag.ExitOnError) handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) - - vd := &validator{ - apiClient: client, - } - - var script []byte - var isYaml bool - var isJSON bool - - var err error - if len(flagSet.Args()) == 1 { - filename := flagSet.Arg(0) - script, err = os.ReadFile(filename) - if err != nil { - return err - } - if strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml") { - isYaml = true - } - if strings.HasSuffix(filename, ".json") { - isJSON = true - } - } - if !isatty.IsTerminal(os.Stdin.Fd()) { - // stdin is a pipe not a terminal - script, err = io.ReadAll(os.Stdin) - if err != nil { - return err - } - isYaml = true - } - - if !isYaml && !isJSON { - script = []byte(defaultVspec) - - } - - ctxm := vd.parseKVPairs(*contextFlag, ",") - - if *secretsFlag != "" { - sm, err := vd.readSecrets(*secretsFlag) - if err != nil { - return err - } - - for k, v := range sm { - ctxm[k] = v - } - } - - return vd.validate(script, ctxm, isYaml) - + validateCommands.run(flagSet, "src validate", usage, args) + return nil } + // Register the command commands = append(commands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} - -func (vd *validator) parseKVPairs(val string, pairSep string) map[string]string { - scriptContext := make(map[string]string) - - pairs := strings.Split(val, pairSep) - for _, pair := range pairs { - kv := strings.Split(pair, "=") - - if len(kv) == 2 { - scriptContext[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) - } - } - return scriptContext -} - -func (vd *validator) readSecrets(path string) (map[string]string, error) { - bs, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - return vd.parseKVPairs(string(bs), "\n"), nil -} - -func (vd *validator) validate(script []byte, scriptContext map[string]string, isYaml bool) error { - tpl, err := template.New("validate").Parse(string(script)) - if err != nil { - return err - } - var ts bytes.Buffer - err = tpl.Execute(&ts, scriptContext) - if err != nil { - return err - } - - var vspec validationSpec - - if isYaml { - if err := yaml.Unmarshal(ts.Bytes(), &vspec); err != nil { - return err - } - } else { - if err := json.Unmarshal(ts.Bytes(), &vspec); err != nil { - return err - } - } - - if vspec.FirstAdmin.Username != "" { - err = vd.createFirstAdmin(&vspec) - if err != nil { - return err - } - - if vspec.FirstAdmin.CreateAccessToken { - token, err := vd.createAccessToken(vspec.FirstAdmin.Username) - if err != nil { - return err - } - fmt.Println(token) - } - } - - if vspec.ExternalService.DisplayName != "" { - extSvcID, err := vd.addExternalService(&vspec) - if err != nil { - return err - } - fmt.Printf("External Service %s is being added \n", vspec.ExternalService.DisplayName) - - defer func() { - if extSvcID != "" && vspec.ExternalService.DeleteWhenDone { - _ = vd.deleteExternalService(extSvcID) - fmt.Printf("External Service %s has been removed \n", vspec.ExternalService.DisplayName) - fmt.Println("Validation Completed") - - } - }() - } - - if vspec.WaitRepoCloned.Repo != "" { - fmt.Printf("repo %s clonining has began \n", vspec.WaitRepoCloned.Repo) - - cloned, err := vd.waitRepoCloned(vspec.WaitRepoCloned.Repo, vspec.WaitRepoCloned.SleepBetweenTriesSeconds, - vspec.WaitRepoCloned.MaxTries) - if err != nil { - return err - } - if !cloned { - return fmt.Errorf("repo %s didn't clone \n", vspec.WaitRepoCloned.Repo) - } - - fmt.Printf("repo %s clonining was successful \n", vspec.WaitRepoCloned.Repo) - - } - - if vspec.SearchQuery != nil { - for i := 0; i < len(vspec.SearchQuery); i++ { - matchCount, err := vd.searchMatchCount(vspec.SearchQuery[i]) - if err != nil { - return err - } - if matchCount == 0 { - return fmt.Errorf("search query %s returned no results \n", vspec.SearchQuery[i]) - } - fmt.Printf("search query '%s' was successful \n", vspec.SearchQuery[i]) - } - } - - if vspec.CreateInsight.Title != "" { - id, err := vd.createInsight(vspec.CreateInsight.Title, vspec.CreateInsight.DataSeries) - if err != nil { - return err - } - - fmt.Printf("insight %s(%s) is being added \n", vspec.CreateInsight.Title, id) - } - - return nil -} - -const vdAddExternalServiceQuery = ` -mutation AddExternalService($kind: ExternalServiceKind!, $displayName: String!, $config: String!) { - addExternalService(input:{ - kind:$kind, - displayName:$displayName, - config: $config - }) - { - id - } -}` - -func (vd *validator) addExternalService(vspec *validationSpec) (string, error) { - configJson, err := json.Marshal(vspec.ExternalService.Config) - if err != nil { - return "", err - } - var resp struct { - AddExternalService struct { - ID string `json:"id"` - } `json:"addExternalService"` - } - - err = vd.graphQL(vdAddExternalServiceQuery, map[string]interface{}{ - "kind": vspec.ExternalService.Kind, - "displayName": vspec.ExternalService.DisplayName, - "config": string(configJson), - }, &resp) - - return resp.AddExternalService.ID, err -} - -const vdDeleteExternalServiceQuery = ` -mutation DeleteExternalService($id: ID!) { - deleteExternalService(externalService: $id){ - alwaysNil - } -}` - -func (vd *validator) deleteExternalService(id string) error { - var resp struct{} - - return vd.graphQL(vdDeleteExternalServiceQuery, map[string]interface{}{ - "id": id, - }, &resp) -} - -const vdSearchMatchCountQuery = ` -query ($query: String!) { - search(query: $query, version: V2, patternType:literal) { - results { - matchCount - } - } -}` - -func (vd *validator) searchMatchCount(searchStr string) (int, error) { - var resp struct { - Search struct { - Results struct { - MatchCount int `json:"matchCount"` - } `json:"results"` - } `json:"search"` - } - - err := vd.graphQL(vdSearchMatchCountQuery, map[string]interface{}{ - "query": searchStr, - }, &resp) - - return resp.Search.Results.MatchCount, err -} - -const vdListRepos = ` -query ListRepos($names: [String!]) { - repositories( - names: $names - ) { - nodes { - name - mirrorInfo { - cloned - } - } - } -}` - -func (vd *validator) listClonedRepos(fs []string) ([]string, error) { - var resp struct { - Repositories struct { - Nodes []struct { - Name string `json:"name"` - MirrorInfo struct { - Cloned bool `json:"cloned"` - } `json:"mirrorInfo"` - } `json:"nodes"` - } `json:"repositories"` - } - - err := vd.graphQL(vdListRepos, map[string]interface{}{ - "names": fs, - }, &resp) - - names := make([]string, 0, len(resp.Repositories.Nodes)) - for _, node := range resp.Repositories.Nodes { - if node.MirrorInfo.Cloned { - names = append(names, node.Name) - } - } - - return names, err -} - -func (vd *validator) waitRepoCloned(repoName string, sleepSeconds int, maxTries int) (bool, error) { - nameFilter := []string{repoName} - - for i := 0; i < maxTries; i++ { - names, err := vd.listClonedRepos(nameFilter) - if err != nil { - return false, err - } - if len(names) == 1 { - return true, nil - } - time.Sleep(time.Second * time.Duration(sleepSeconds)) - } - return false, nil -} - -const vdAddCodeInsight = ` -mutation CreateLineChartSearchInsight($input: LineChartSearchInsightInput!) { - createLineChartSearchInsight(input: $input) { - view { - id - } - } -}` - -func (vd *validator) createInsight(title string, dataseries []map[string]interface{}) (string, error) { - var resp struct { - CreateLineChartSearchInsight struct { - View struct { - ID string `json:"id"` - } `json:"view"` - } `json:"createLineChartSearchInsight"` - } - var ds []map[string]interface{} - for _, d := range dataseries { - var series = map[string]interface{}{ - "query": d["query"], - "options": map[string]interface{}{ - "label": d["label"], - "lineColor": d["lineColor"], - }, - "repositoryScope": map[string]interface{}{ - "repositories": d["repositoryScope"], - }, - "timeScope": map[string]interface{}{ - "stepInterval": map[string]interface{}{ - "unit": d["timeScopeUnit"], - "value": d["timeScopeValue"], - }, - }, - } - ds = append(ds, series) - } - err := vd.graphQL(vdAddCodeInsight, - map[string]interface{}{"input": map[string]interface{}{ - "options": map[string]interface{}{"title": title}, - "dataSeries": ds}}, &resp) - if err != nil { - return "", err - } - - return resp.CreateLineChartSearchInsight.View.ID, nil -} - -const vdUserQuery = ` -query User($username: String) { - user(username: $username) { - id - } -}` - -func (vd *validator) userID(username string) (string, error) { - var resp struct { - User struct { - ID string `json:"id"` - } `json:"user"` - } - - err := vd.graphQL(vdUserQuery, map[string]interface{}{ - "username": username, - }, &resp) - - return resp.User.ID, err -} - -const vdCreateAccessTokenMutation = ` -mutation CreateAccessToken($user: ID!, $scopes: [String!]!, $note: String!) { - createAccessToken( - user:$user, - scopes:$scopes, - note: $note - ) - { - token - } -}` - -func (vd *validator) createAccessToken(username string) (string, error) { - userID, err := vd.userID(username) - if err != nil { - return "", err - } - - var resp struct { - CreateAccessToken struct { - Token string `json:"token"` - } `json:"createAccessToken"` - } - - err = vd.graphQL(vdCreateAccessTokenMutation, map[string]interface{}{ - "user": userID, - "scopes": []string{"user:all", "site-admin:sudo"}, - "note": "src_cli_validate", - }, &resp) - - return resp.CreateAccessToken.Token, err -} - -// SiteAdminInit initializes the instance with given admin account. -// It returns an authenticated client as the admin for doing e2e testing. -func (vd *validator) siteAdminInit(baseURL, email, username, password string) (*vdClient, error) { - client := vd.newClient(baseURL) - - var request = struct { - Email string `json:"email"` - Username string `json:"username"` - Password string `json:"password"` - }{ - Email: email, - Username: username, - Password: password, - } - err := client.authenticate("/-/site-init", request) - if err != nil { - return nil, err - } - - return client, nil -} - -// SignIn performs the sign in with given user credentials. -// It returns an authenticated client as the user for doing e2e testing. -func (vd *validator) signIn(baseURL string, email, password string) (*vdClient, error) { - client := vd.newClient(baseURL) - - var request = struct { - Email string `json:"email"` - Password string `json:"password"` - }{ - Email: email, - Password: password, - } - err := client.authenticate("/-/sign-in", request) - if err != nil { - return nil, err - } - - return client, nil -} - -// Client is an authenticated client for a Sourcegraph user for doing e2e testing. -// The user may or may not be a site admin depends on how the client is instantiated. -// It works by simulating how the browser would send HTTP requests to the server. -type vdClient struct { - baseURL string - sessionCookie *http.Cookie - - userID string -} - -// newClient instantiates a new client. -func (vd *validator) newClient(baseURL string) *vdClient { - return &vdClient{ - baseURL: baseURL, - } -} - -// authenticate is used to send a HTTP POST request to an URL that is able to authenticate -// a user with given body (marshalled to JSON), e.g. site admin init, sign in. Once the -// client is authenticated, the session cookie will be stored as a proof of authentication. -func (c *vdClient) authenticate(path string, body interface{}) error { - p, err := jsoniter.Marshal(body) - if err != nil { - return err - } - - req, err := http.NewRequest("POST", c.baseURL+path, bytes.NewReader(p)) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - p, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - return errors.New(string(p)) - } - - var sessionCookie *http.Cookie - for _, cookie := range resp.Cookies() { - if cookie.Name == "sgs" { - sessionCookie = cookie - break - } - } - if sessionCookie == nil { - return err - } - c.sessionCookie = sessionCookie - - userID, err := c.currentUserID() - if err != nil { - return err - } - c.userID = userID - return nil -} - -// currentUserID returns the current user's GraphQL node ID. -func (c *vdClient) currentUserID() (string, error) { - query := ` - query { - currentUser { - id - } - } -` - var resp struct { - Data struct { - CurrentUser struct { - ID string `json:"id"` - } `json:"currentUser"` - } `json:"data"` - } - err := c.graphQL("", query, nil, &resp) - if err != nil { - return "", err - } - - return resp.Data.CurrentUser.ID, nil -} - -// graphqlError wraps a raw JSON error returned from a GraphQL endpoint. -type graphqlError struct{ v interface{} } - -func (g *graphqlError) Error() string { - j, _ := json.MarshalIndent(g.v, "", " ") - return string(j) -} - -// jsonCopy is a cheaty method of copying an already-decoded JSON (src) -// response into its destination (dst) that would usually be passed to e.g. -// json.Unmarshal. -// -// We could do this with reflection, obviously, but it would be much more -// complex and JSON re-marshaling should be cheap enough anyway. Can improve in -// the future. -func jsonCopy(dst, src interface{}) error { - data, err := json.Marshal(src) - if err != nil { - return err - } - return json.NewDecoder(bytes.NewReader(data)).Decode(dst) -} - -// GraphQL makes a GraphQL request to the server on behalf of the user authenticated by the client. -// An optional token can be passed to impersonate other users. -func (c *vdClient) graphQL(token, query string, variables map[string]interface{}, target interface{}) error { - body, err := jsoniter.Marshal(map[string]interface{}{ - "query": query, - "variables": variables, + flagSet: flagSet, + aliases: []string{"validate"}, + handler: handler, + usageFunc: func() { + fmt.Println(usage) + }, }) - if err != nil { - return err - } - - req, err := http.NewRequest("POST", fmt.Sprintf("%s/.api/graphql", c.baseURL), bytes.NewReader(body)) - if err != nil { - return err - } - if token != "" { - req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) - } else { - // NOTE: We use this header to protect from CSRF attacks of HTTP API, - // see https://sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/cmd/frontend/internal/cli/http.go#L41-42 - req.Header.Set("X-Requested-With", "Sourcegraph") - req.AddCookie(c.sessionCookie) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - p, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - return errors.New(string(p)) - } - - // Decode the response. - var result struct { - Data interface{} `json:"data,omitempty"` - Errors interface{} `json:"errors,omitempty"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return err - } - - if result.Errors != nil { - return cmderrors.ExitCode( - cmderrors.GraphqlErrorsExitCode, - fmt.Errorf("GraphQL errors:\n%s", &graphqlError{result.Errors}), - ) - } - if err := jsonCopy(target, result.Data); err != nil { - return err - } - return nil -} - -func (vd *validator) createFirstAdmin(vspec *validationSpec) error { - client, err := vd.signIn(cfg.Endpoint, vspec.FirstAdmin.Email, vspec.FirstAdmin.Password) - if err != nil { - client, err = vd.siteAdminInit(cfg.Endpoint, vspec.FirstAdmin.Email, vspec.FirstAdmin.Username, - vspec.FirstAdmin.Password) - if err != nil { - return err - } - } - - vd.client = client - return nil -} - -func (vd *validator) graphQL(query string, variables map[string]interface{}, target interface{}) error { - if vd.client != nil { - return vd.client.graphQL("", query, variables, target) - } - - _, err := vd.apiClient.NewRequest(query, variables).Do(context.TODO(), target) - if err != nil { - return err - } - return nil } diff --git a/cmd/src/validate_install.go b/cmd/src/validate_install.go new file mode 100644 index 0000000000..984bb8502d --- /dev/null +++ b/cmd/src/validate_install.go @@ -0,0 +1,104 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "os" + "strings" + + "github.com/mattn/go-isatty" + "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/validate" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +func init() { + usage := `'src validate install' is a tool that validates a Sourcegraph installation. + +Examples: + + Run default checks: + + $ src validate install + + Provide a YAML configuration file: + + $ src validate install config.yml + + Provide a JSON configuration file: + + $ src validate install config.json + +` + + flagSet := flag.NewFlagSet("install", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src validate %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + var ( + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + client := cfg.apiClient(apiFlags, flagSet.Output()) + + var validationSpec *validate.ValidationSpec + + if len(flagSet.Args()) == 1 { + fileName := flagSet.Arg(0) + f, err := os.ReadFile(fileName) + if err != nil { + return errors.Wrap(err, "failed to read installation validation config from file") + } + + if strings.HasSuffix(fileName, ".yaml") || strings.HasSuffix(fileName, ".yml") { + validationSpec, err = validate.LoadYamlConfig(f) + if err != nil { + return err + } + } + + if strings.HasSuffix(fileName, ".json") { + validationSpec, err = validate.LoadJsonConfig(f) + if err != nil { + return err + } + } + } + + if !isatty.IsTerminal(os.Stdin.Fd()) { + // stdin is a pipe not a terminal + input, err := io.ReadAll(os.Stdin) + if err != nil { + return errors.Wrap(err, "failed to read installation validation config from pipe") + } + validationSpec, err = validate.LoadYamlConfig(input) + if err != nil { + return err + } + } + + if validationSpec == nil { + validationSpec = validate.DefaultConfig() + } + + validationSpec.ExternalService.Config.GitHub.Token = cfg.GithubToken + + return validate.Validate(context.Background(), client, validationSpec) + } + + validateCommands = append(validateCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) +} From 97ee45b5dee66a1c6333df6dbb6e4487b0544c6c Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Thu, 12 Jan 2023 10:28:03 -0500 Subject: [PATCH 04/10] Change validation API to be more specific --- cmd/src/validate_install.go | 2 +- internal/validate/install.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/src/validate_install.go b/cmd/src/validate_install.go index 984bb8502d..ce50603b27 100644 --- a/cmd/src/validate_install.go +++ b/cmd/src/validate_install.go @@ -93,7 +93,7 @@ Examples: validationSpec.ExternalService.Config.GitHub.Token = cfg.GithubToken - return validate.Validate(context.Background(), client, validationSpec) + return validate.Installation(context.Background(), client, validationSpec) } validateCommands = append(validateCommands, &command{ diff --git a/internal/validate/install.go b/internal/validate/install.go index 1465da4fd0..2eedd66421 100644 --- a/internal/validate/install.go +++ b/internal/validate/install.go @@ -147,9 +147,9 @@ func LoadJsonConfig(userConfig []byte) (*ValidationSpec, error) { return &config, nil } -// Validate runs a series of validation checks such as cloning a repository, running search queries, and +// Installation 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 { +func Installation(ctx context.Context, client api.Client, config *ValidationSpec) error { if config.ExternalService.DisplayName != "" { srvID, err := addExternalService(ctx, client, config.ExternalService) if err != nil { From fce359a47c99cebe7382b581b5114bad77bb6a99 Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Thu, 12 Jan 2023 11:50:25 -0500 Subject: [PATCH 05/10] Change SRC_GITHUB_TOKEN to only be read if needed for command --- cmd/src/main.go | 5 ----- cmd/src/validate_install.go | 8 +++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/src/main.go b/cmd/src/main.go index 8d9dac8dba..9e63c49762 100644 --- a/cmd/src/main.go +++ b/cmd/src/main.go @@ -80,7 +80,6 @@ var cfg *config type config struct { Endpoint string `json:"endpoint"` AccessToken string `json:"accessToken"` - GithubToken string `json:"githubToken"` AdditionalHeaders map[string]string `json:"additionalHeaders"` ConfigFilePath string @@ -134,7 +133,6 @@ func readConfig() (*config, error) { envToken := os.Getenv("SRC_ACCESS_TOKEN") envEndpoint := os.Getenv("SRC_ENDPOINT") - envGithubToken := os.Getenv("SRC_GITHUB_TOKEN") if userSpecified { // If a config file is present, either zero or both environment variables must be present. @@ -151,9 +149,6 @@ func readConfig() (*config, error) { if envToken != "" { cfg.AccessToken = envToken } - if envGithubToken != "" { - cfg.GithubToken = envGithubToken - } if envEndpoint != "" { cfg.Endpoint = envEndpoint } diff --git a/cmd/src/validate_install.go b/cmd/src/validate_install.go index ce50603b27..cfc0523711 100644 --- a/cmd/src/validate_install.go +++ b/cmd/src/validate_install.go @@ -32,6 +32,10 @@ Examples: $ src validate install config.json +Environmental variables + + SRC_GITHUB_TOKEN GitHub access token for validation features + ` flagSet := flag.NewFlagSet("install", flag.ExitOnError) @@ -91,7 +95,9 @@ Examples: validationSpec = validate.DefaultConfig() } - validationSpec.ExternalService.Config.GitHub.Token = cfg.GithubToken + envGithubToken := os.Getenv("SRC_GITHUB_TOKEN") + + validationSpec.ExternalService.Config.GitHub.Token = envGithubToken return validate.Installation(context.Background(), client, validationSpec) } From 7413d9f691e12bfb782ba2c7110a2bb1129ed3d2 Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Thu, 12 Jan 2023 13:52:41 -0500 Subject: [PATCH 06/10] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5811752909..8ba7dc72e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,15 @@ All notable changes to `src-cli` are documented in this file. ### Added - `src validate` has an added check to determine if an instance is able to create a basic code insight. [#912](https://github.com/sourcegraph/src-cli/pull/912) +- Add visual feedback to `src validate install` CLI [#921](https://github.com/sourcegraph/src-cli/pull/921) +- Add insight cleanup as per [#912](https://github.com/sourcegraph/src-cli/pull/912#issuecomment-1377084768) ### Changed - Renamed `src users clean` command to `src users prune` [#901](https://github.com/sourcegraph/src-cli/pull/901) - Failed code-intel uploads now print every error encountered while retrying instead of only the error encountered in the final retry attempt. [#46281](https://github.com/sourcegraph/sourcegraph/pull/46281) +- `src validate` has been changed to `srv validate install` subcommand [#921](https://github.com/sourcegraph/src-cli/pull/921) +- Move GitHub token for `srv validate` to ENV var [#921](https://github.com/sourcegraph/src-cli/pull/921) ### Fixed @@ -27,6 +31,8 @@ All notable changes to `src-cli` are documented in this file. ### Removed +- Removed __create first admin__ feature from `src validate` [#921](https://github.com/sourcegraph/src-cli/pull/921) + ## 4.3.0 ### Added From 10d3be25f2f0bc1e3663e5e68b2e20bfd4ec099d Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Thu, 12 Jan 2023 17:17:06 -0500 Subject: [PATCH 07/10] Add warning if SRC_GITHUB_TOKEN isn't set * An auth token is required for code hosts when using `src validate install`, warn user and exit if not set. --- cmd/src/validate_install.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmd/src/validate_install.go b/cmd/src/validate_install.go index cfc0523711..0bab7d354a 100644 --- a/cmd/src/validate_install.go +++ b/cmd/src/validate_install.go @@ -13,6 +13,11 @@ import ( "github.com/sourcegraph/src-cli/internal/validate" "github.com/sourcegraph/sourcegraph/lib/errors" + "github.com/sourcegraph/sourcegraph/lib/output" +) + +var ( + validateWarningEmoji = output.EmojiWarning ) func init() { @@ -97,6 +102,11 @@ Environmental variables 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", validateWarningEmoji) + } + validationSpec.ExternalService.Config.GitHub.Token = envGithubToken return validate.Installation(context.Background(), client, validationSpec) From aca3a9018c243c9c61797b050b95348ee8d4f97c Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Fri, 13 Jan 2023 11:05:05 -0500 Subject: [PATCH 08/10] Remove unused deps --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 73b1835a2c..4c19f1ab36 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/hexops/autogold v1.3.0 github.com/jedib0t/go-pretty/v6 v6.3.7 github.com/jig/teereadcloser v0.0.0-20181016160506-953720c48e05 - github.com/json-iterator/go v1.1.12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-isatty v0.0.16 github.com/neelance/parallel v0.0.0-20160708114440-4de9ce63d14c @@ -72,6 +71,7 @@ require ( github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a // indirect github.com/jhump/protocompile v0.0.0-20220216033700-d705409f108f // indirect github.com/jhump/protoreflect v1.12.1-0.20220417024638-438db461d753 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.1 // indirect github.com/klauspost/pgzip v1.2.5 // indirect github.com/kr/pretty v0.3.0 // indirect From f5e64bcc7ef7430810f2f1185ecbc3830c7212c6 Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Fri, 13 Jan 2023 11:05:29 -0500 Subject: [PATCH 09/10] Separate tasks being performed --- internal/validate/install.go | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/internal/validate/install.go b/internal/validate/install.go index 2eedd66421..860c2fa14c 100644 --- a/internal/validate/install.go +++ b/internal/validate/install.go @@ -15,9 +15,10 @@ import ( ) var ( - validateFailureEmoji = output.EmojiFailure - validateSuccessEmoji = output.EmojiSuccess - validateHourglassEmoji = output.EmojiHourglass + validateEmojiFingerPointRight = output.EmojiFingerPointRight + validateFailureEmoji = output.EmojiFailure + validateHourglassEmoji = output.EmojiHourglass + validateSuccessEmoji = output.EmojiSuccess ) type ExternalService struct { @@ -150,23 +151,25 @@ func LoadJsonConfig(userConfig []byte) (*ValidationSpec, error) { // Installation runs a series of validation checks such as cloning a repository, running search queries, and // creating insights, based on the configuration provided. func Installation(ctx context.Context, client api.Client, config *ValidationSpec) error { + log.Printf("%s validating external service", validateEmojiFingerPointRight) + if config.ExternalService.DisplayName != "" { srvID, err := addExternalService(ctx, client, config.ExternalService) if err != nil { return err } - log.Printf("%s external service %s is being added\n", validateHourglassEmoji, config.ExternalService.DisplayName) + log.Printf("%s external service %s is being added", validateHourglassEmoji, config.ExternalService.DisplayName) defer func() { if srvID != "" && config.ExternalService.DeleteWhenDone { _ = removeExternalService(ctx, client, srvID) - log.Printf("%s external service %s has been removed\n", validateSuccessEmoji, config.ExternalService.DisplayName) + log.Printf("%s external service %s has been removed", validateSuccessEmoji, config.ExternalService.DisplayName) } }() } - log.Printf("%s cloning repository\n", validateHourglassEmoji) + log.Printf("%s cloning repository", validateHourglassEmoji) cloned, err := repoCloneTimeout(ctx, client, config.ExternalService) if err != nil { @@ -176,7 +179,9 @@ func Installation(ctx context.Context, client api.Client, config *ValidationSpec return errors.Newf("%s validate failed, repo did not clone\n", validateFailureEmoji) } - log.Printf("%s repositry successfully cloned\n", validateSuccessEmoji) + log.Printf("%s repositry successfully cloned", validateSuccessEmoji) + + log.Printf("%s validating search queries", validateEmojiFingerPointRight) if config.SearchQuery != nil { for i := 0; i < len(config.SearchQuery); i++ { @@ -187,24 +192,26 @@ func Installation(ctx context.Context, client api.Client, config *ValidationSpec if matchCount == 0 { return errors.Newf("validate failed, search query %s returned no results", config.SearchQuery[i]) } - log.Printf("%s search query '%s' was successful\n", validateSuccessEmoji, config.SearchQuery[i]) + log.Printf("%s search query '%s' was successful", validateSuccessEmoji, config.SearchQuery[i]) } } + log.Printf("%s validating code insight", validateEmojiFingerPointRight) + if config.Insight.Title != "" { - log.Printf("%s insight %s is being added\n", validateHourglassEmoji, config.Insight.Title) + log.Printf("%s insight %s is being added", validateHourglassEmoji, config.Insight.Title) insightId, err := createInsight(ctx, client, config.Insight) if err != nil { return err } - log.Printf("%s insight successfully added\n", validateSuccessEmoji) + log.Printf("%s insight successfully added", validateSuccessEmoji) defer func() { if insightId != "" { _ = removeInsight(ctx, client, insightId) - log.Printf("%s insight %s has been removed\n", validateSuccessEmoji, config.Insight.Title) + log.Printf("%s insight %s has been removed", validateSuccessEmoji, config.Insight.Title) } }() From aef84ee90e23930d150ba795d07cda23f37d4150 Mon Sep 17 00:00:00 2001 From: Jacob Pleiness Date: Fri, 13 Jan 2023 11:32:16 -0500 Subject: [PATCH 10/10] Add symbol search to default queries * Change default queries to use sourcegraph/src-cli repo instead of gorilla/mux --- internal/validate/install.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/validate/install.go b/internal/validate/install.go index 860c2fa14c..3f6d538448 100644 --- a/internal/validate/install.go +++ b/internal/validate/install.go @@ -80,7 +80,11 @@ type ValidationSpec struct { // DefaultConfig returns a default configuration to be used for testing. func DefaultConfig() *ValidationSpec { return &ValidationSpec{ - SearchQuery: []string{"repo:^github\\.com/gorilla/mux$ Router", "repo:^github\\.com/gorilla/mux$@v1.8.0 Router"}, + 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", @@ -89,7 +93,7 @@ func DefaultConfig() *ValidationSpec { URL: "https://github.com", Token: "", Orgs: []string{}, - Repos: []string{"gorilla/mux"}, + Repos: []string{"sourcegraph/src-cli"}, }, }, MaxRetries: 5,