Skip to content

Commit 4d2957c

Browse files
authored
snapshot: add 'snapshot summary', 'snapshot test' (#888)
1 parent c9902bf commit 4d2957c

File tree

6 files changed

+674
-0
lines changed

6 files changed

+674
-0
lines changed

cmd/src/snapshot.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
)
7+
8+
var snapshotCommands commander
9+
10+
func init() {
11+
usage := `'src snapshot' manages snapshots of Sourcegraph instance data. All subcommands are currently EXPERIMENTAL.
12+
13+
USAGE
14+
src [-v] snapshot <command>
15+
16+
COMMANDS
17+
18+
summary export summary data about an instance for acceptance testing of a restored Sourcegraph instance
19+
test use exported summary data and instance health indicators to validate a restored and upgraded instance
20+
`
21+
flagSet := flag.NewFlagSet("snapshot", flag.ExitOnError)
22+
23+
commands = append(commands, &command{
24+
flagSet: flagSet,
25+
handler: func(args []string) error {
26+
snapshotCommands.run(flagSet, "src snapshot", usage, args)
27+
return nil
28+
},
29+
usageFunc: func() { fmt.Fprint(flag.CommandLine.Output(), usage) },
30+
})
31+
}

cmd/src/snapshot_summary.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"flag"
7+
"fmt"
8+
"os"
9+
10+
"github.com/sourcegraph/sourcegraph/lib/errors"
11+
"github.com/sourcegraph/sourcegraph/lib/output"
12+
13+
"github.com/sourcegraph/src-cli/internal/api"
14+
)
15+
16+
func init() {
17+
usage := `'src snapshot summary' generates summary data for acceptance testing of a restored Sourcegraph instance with 'src snapshot test'.
18+
19+
USAGE
20+
src login # site-admin authentication required
21+
src [-v] snapshot summary [-summary-path="./src-snapshot-summary.json"]
22+
`
23+
flagSet := flag.NewFlagSet("summary", flag.ExitOnError)
24+
snapshotPath := flagSet.String("summary-path", "./src-snapshot-summary.json", "path to write snapshot summary to")
25+
apiFlags := api.NewFlags(flagSet)
26+
27+
snapshotCommands = append(snapshotCommands, &command{
28+
flagSet: flagSet,
29+
handler: func(args []string) error {
30+
if err := flagSet.Parse(args); err != nil {
31+
return err
32+
}
33+
out := output.NewOutput(flagSet.Output(), output.OutputOpts{Verbose: *verbose})
34+
35+
client := cfg.apiClient(apiFlags, flagSet.Output())
36+
37+
snapshotResult, err := fetchSnapshotSummary(context.Background(), client)
38+
if err != nil {
39+
return err
40+
}
41+
42+
f, err := os.OpenFile(*snapshotPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
43+
if err != nil {
44+
return errors.Wrap(err, "open snapshot file")
45+
}
46+
enc := json.NewEncoder(f)
47+
enc.SetIndent("", "\t")
48+
if err := enc.Encode(snapshotResult); err != nil {
49+
return errors.Wrap(err, "write snapshot file")
50+
}
51+
52+
out.WriteLine(output.Emoji(output.EmojiSuccess, "Summary snapshot data generated!"))
53+
return nil
54+
},
55+
usageFunc: func() { fmt.Fprint(flag.CommandLine.Output(), usage) },
56+
})
57+
}
58+
59+
type snapshotSummary struct {
60+
ExternalServices struct {
61+
TotalCount int
62+
Nodes []struct {
63+
Kind string
64+
ID string
65+
}
66+
}
67+
Site struct {
68+
AuthProviders struct {
69+
TotalCount int
70+
Nodes []struct {
71+
ServiceType string
72+
ServiceID string
73+
}
74+
}
75+
}
76+
}
77+
78+
func fetchSnapshotSummary(ctx context.Context, client api.Client) (*snapshotSummary, error) {
79+
var snapshotResult snapshotSummary
80+
ok, err := client.NewQuery(`
81+
query GenerateSnapshotAcceptanceData {
82+
externalServices {
83+
totalCount
84+
nodes {
85+
kind
86+
id
87+
}
88+
}
89+
site {
90+
authProviders {
91+
totalCount
92+
nodes {
93+
serviceType
94+
serviceID
95+
}
96+
}
97+
}
98+
}
99+
`).Do(ctx, &snapshotResult)
100+
if err != nil {
101+
return nil, errors.Wrap(err, "generate snapshot")
102+
} else if !ok {
103+
return nil, errors.New("received no data")
104+
}
105+
return &snapshotResult, nil
106+
}

cmd/src/snapshot_testcmd.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"flag"
7+
"fmt"
8+
"os"
9+
"time"
10+
11+
"github.com/google/go-cmp/cmp"
12+
"github.com/sourcegraph/sourcegraph/lib/errors"
13+
"github.com/sourcegraph/sourcegraph/lib/output"
14+
15+
"github.com/sourcegraph/src-cli/internal/api"
16+
"github.com/sourcegraph/src-cli/internal/instancehealth"
17+
)
18+
19+
// note that this file is called '_testcmd.go' because '_test.go' cannot be used
20+
21+
func init() {
22+
usage := `'src snapshot test' uses exported summary data to validate a restored and upgraded instance.
23+
24+
USAGE
25+
src login # site-admin authentication required
26+
src [-v] snapshot test [-summary-path="./src-snapshot-summary.json"]
27+
`
28+
flagSet := flag.NewFlagSet("test", flag.ExitOnError)
29+
snapshotSummaryPath := flagSet.String("summary-path", "./src-snapshot-summary.json", "path to read snapshot summary from")
30+
since := flagSet.Duration("since", 1*time.Hour, "duration ago to look for healthcheck data")
31+
apiFlags := api.NewFlags(flagSet)
32+
33+
snapshotCommands = append(snapshotCommands, &command{
34+
flagSet: flagSet,
35+
handler: func(args []string) error {
36+
if err := flagSet.Parse(args); err != nil {
37+
return err
38+
}
39+
out := output.NewOutput(flagSet.Output(), output.OutputOpts{Verbose: *verbose})
40+
client := cfg.apiClient(apiFlags, flagSet.Output())
41+
42+
// Fetch health data
43+
instanceHealth, err := instancehealth.GetIndicators(context.Background(), client)
44+
if err != nil {
45+
return err
46+
}
47+
48+
// Optionally validate snapshots
49+
if *snapshotSummaryPath != "" {
50+
f, err := os.OpenFile(*snapshotSummaryPath, os.O_RDONLY, os.ModePerm)
51+
if err != nil {
52+
return errors.Wrap(err, "open snapshot file")
53+
}
54+
var recordedSummary snapshotSummary
55+
if err := json.NewDecoder(f).Decode(&recordedSummary); err != nil {
56+
return errors.Wrap(err, "read snapshot file")
57+
}
58+
// Fetch new snapshot
59+
newSummary, err := fetchSnapshotSummary(context.Background(), client)
60+
if err != nil {
61+
return errors.Wrap(err, "get snapshot")
62+
}
63+
if err := compareSnapshotSummaries(out, recordedSummary, *newSummary); err != nil {
64+
return err
65+
}
66+
}
67+
68+
// generate checks set
69+
checks := instancehealth.NewChecks(*since, *instanceHealth)
70+
71+
// Run checks
72+
var validationErrors error
73+
for _, check := range checks {
74+
validationErrors = errors.Append(validationErrors, check(out))
75+
}
76+
if validationErrors != nil {
77+
out.WriteLine(output.Linef(output.EmojiFailure, output.StyleFailure,
78+
"Critical issues found: %s", err.Error()))
79+
return errors.New("validation failed")
80+
}
81+
out.WriteLine(output.Line(output.EmojiSuccess, output.StyleSuccess,
82+
"No critical issues found!"))
83+
return nil
84+
},
85+
usageFunc: func() { fmt.Fprint(flag.CommandLine.Output(), usage) },
86+
})
87+
}
88+
89+
func compareSnapshotSummaries(out *output.Output, recordedSummary, newSummary snapshotSummary) error {
90+
b := out.Block(output.Styled(output.StyleBold, "Snapshot contents"))
91+
defer b.Close()
92+
93+
// Compare
94+
diff := cmp.Diff(recordedSummary, newSummary)
95+
if diff != "" {
96+
b.WriteLine(output.Line(output.EmojiFailure, output.StyleFailure, "Snapshot diff detected:"))
97+
b.WriteCode("diff", diff)
98+
return errors.New("snapshot mismatch")
99+
}
100+
b.WriteLine(output.Emoji(output.EmojiSuccess, "Snapshots match!"))
101+
return nil
102+
}

0 commit comments

Comments
 (0)