Skip to content

Commit

Permalink
snapshot: add 'snapshot summary', 'snapshot test' (#888)
Browse files Browse the repository at this point in the history
  • Loading branch information
bobheadxi authored Nov 25, 2022
1 parent c9902bf commit 4d2957c
Show file tree
Hide file tree
Showing 6 changed files with 674 additions and 0 deletions.
31 changes: 31 additions & 0 deletions cmd/src/snapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"flag"
"fmt"
)

var snapshotCommands commander

func init() {
usage := `'src snapshot' manages snapshots of Sourcegraph instance data. All subcommands are currently EXPERIMENTAL.
USAGE
src [-v] snapshot <command>
COMMANDS
summary export summary data about an instance for acceptance testing of a restored Sourcegraph instance
test use exported summary data and instance health indicators to validate a restored and upgraded instance
`
flagSet := flag.NewFlagSet("snapshot", flag.ExitOnError)

commands = append(commands, &command{
flagSet: flagSet,
handler: func(args []string) error {
snapshotCommands.run(flagSet, "src snapshot", usage, args)
return nil
},
usageFunc: func() { fmt.Fprint(flag.CommandLine.Output(), usage) },
})
}
106 changes: 106 additions & 0 deletions cmd/src/snapshot_summary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"os"

"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/lib/output"

"github.com/sourcegraph/src-cli/internal/api"
)

func init() {
usage := `'src snapshot summary' generates summary data for acceptance testing of a restored Sourcegraph instance with 'src snapshot test'.
USAGE
src login # site-admin authentication required
src [-v] snapshot summary [-summary-path="./src-snapshot-summary.json"]
`
flagSet := flag.NewFlagSet("summary", flag.ExitOnError)
snapshotPath := flagSet.String("summary-path", "./src-snapshot-summary.json", "path to write snapshot summary to")
apiFlags := api.NewFlags(flagSet)

snapshotCommands = append(snapshotCommands, &command{
flagSet: flagSet,
handler: func(args []string) error {
if err := flagSet.Parse(args); err != nil {
return err
}
out := output.NewOutput(flagSet.Output(), output.OutputOpts{Verbose: *verbose})

client := cfg.apiClient(apiFlags, flagSet.Output())

snapshotResult, err := fetchSnapshotSummary(context.Background(), client)
if err != nil {
return err
}

f, err := os.OpenFile(*snapshotPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
if err != nil {
return errors.Wrap(err, "open snapshot file")
}
enc := json.NewEncoder(f)
enc.SetIndent("", "\t")
if err := enc.Encode(snapshotResult); err != nil {
return errors.Wrap(err, "write snapshot file")
}

out.WriteLine(output.Emoji(output.EmojiSuccess, "Summary snapshot data generated!"))
return nil
},
usageFunc: func() { fmt.Fprint(flag.CommandLine.Output(), usage) },
})
}

type snapshotSummary struct {
ExternalServices struct {
TotalCount int
Nodes []struct {
Kind string
ID string
}
}
Site struct {
AuthProviders struct {
TotalCount int
Nodes []struct {
ServiceType string
ServiceID string
}
}
}
}

func fetchSnapshotSummary(ctx context.Context, client api.Client) (*snapshotSummary, error) {
var snapshotResult snapshotSummary
ok, err := client.NewQuery(`
query GenerateSnapshotAcceptanceData {
externalServices {
totalCount
nodes {
kind
id
}
}
site {
authProviders {
totalCount
nodes {
serviceType
serviceID
}
}
}
}
`).Do(ctx, &snapshotResult)
if err != nil {
return nil, errors.Wrap(err, "generate snapshot")
} else if !ok {
return nil, errors.New("received no data")
}
return &snapshotResult, nil
}
102 changes: 102 additions & 0 deletions cmd/src/snapshot_testcmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"time"

"github.com/google/go-cmp/cmp"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/lib/output"

"github.com/sourcegraph/src-cli/internal/api"
"github.com/sourcegraph/src-cli/internal/instancehealth"
)

// note that this file is called '_testcmd.go' because '_test.go' cannot be used

func init() {
usage := `'src snapshot test' uses exported summary data to validate a restored and upgraded instance.
USAGE
src login # site-admin authentication required
src [-v] snapshot test [-summary-path="./src-snapshot-summary.json"]
`
flagSet := flag.NewFlagSet("test", flag.ExitOnError)
snapshotSummaryPath := flagSet.String("summary-path", "./src-snapshot-summary.json", "path to read snapshot summary from")
since := flagSet.Duration("since", 1*time.Hour, "duration ago to look for healthcheck data")
apiFlags := api.NewFlags(flagSet)

snapshotCommands = append(snapshotCommands, &command{
flagSet: flagSet,
handler: func(args []string) error {
if err := flagSet.Parse(args); err != nil {
return err
}
out := output.NewOutput(flagSet.Output(), output.OutputOpts{Verbose: *verbose})
client := cfg.apiClient(apiFlags, flagSet.Output())

// Fetch health data
instanceHealth, err := instancehealth.GetIndicators(context.Background(), client)
if err != nil {
return err
}

// Optionally validate snapshots
if *snapshotSummaryPath != "" {
f, err := os.OpenFile(*snapshotSummaryPath, os.O_RDONLY, os.ModePerm)
if err != nil {
return errors.Wrap(err, "open snapshot file")
}
var recordedSummary snapshotSummary
if err := json.NewDecoder(f).Decode(&recordedSummary); err != nil {
return errors.Wrap(err, "read snapshot file")
}
// Fetch new snapshot
newSummary, err := fetchSnapshotSummary(context.Background(), client)
if err != nil {
return errors.Wrap(err, "get snapshot")
}
if err := compareSnapshotSummaries(out, recordedSummary, *newSummary); err != nil {
return err
}
}

// generate checks set
checks := instancehealth.NewChecks(*since, *instanceHealth)

// Run checks
var validationErrors error
for _, check := range checks {
validationErrors = errors.Append(validationErrors, check(out))
}
if validationErrors != nil {
out.WriteLine(output.Linef(output.EmojiFailure, output.StyleFailure,
"Critical issues found: %s", err.Error()))
return errors.New("validation failed")
}
out.WriteLine(output.Line(output.EmojiSuccess, output.StyleSuccess,
"No critical issues found!"))
return nil
},
usageFunc: func() { fmt.Fprint(flag.CommandLine.Output(), usage) },
})
}

func compareSnapshotSummaries(out *output.Output, recordedSummary, newSummary snapshotSummary) error {
b := out.Block(output.Styled(output.StyleBold, "Snapshot contents"))
defer b.Close()

// Compare
diff := cmp.Diff(recordedSummary, newSummary)
if diff != "" {
b.WriteLine(output.Line(output.EmojiFailure, output.StyleFailure, "Snapshot diff detected:"))
b.WriteCode("diff", diff)
return errors.New("snapshot mismatch")
}
b.WriteLine(output.Emoji(output.EmojiSuccess, "Snapshots match!"))
return nil
}
Loading

0 comments on commit 4d2957c

Please sign in to comment.