Skip to content

Commit 5a4b617

Browse files
committed
feat: add blueprints command
1 parent 2835e8c commit 5a4b617

File tree

15 files changed

+2571
-0
lines changed

15 files changed

+2571
-0
lines changed

cmd/src/blueprint.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
)
7+
8+
const defaultBlueprintRepo = "https://github.com/sourcegraph-community/blueprints"
9+
10+
var blueprintCommands commander
11+
12+
func init() {
13+
usage := `INTERNAL USE ONLY: 'src blueprint' manages blueprints on a Sourcegraph instance.
14+
15+
Usage:
16+
src blueprint command [command options]
17+
18+
The commands are:
19+
20+
list lists blueprints from a remote repository or local path
21+
import imports blueprints from a remote repository or local path
22+
23+
Use "src blueprint [command] -h" for more information about a command.
24+
25+
`
26+
27+
flagset := flag.NewFlagSet("blueprint", flag.ExitOnError)
28+
handler := func(args []string) error {
29+
blueprintCommands.run(flagset, "src blueprint", usage, args)
30+
return nil
31+
}
32+
33+
// Register the command.
34+
commands = append(commands, &command{
35+
flagSet: flagset,
36+
handler: handler,
37+
usageFunc: func() { fmt.Println(usage) },
38+
})
39+
}

cmd/src/blueprint_import.go

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"io"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/sourcegraph/src-cli/internal/api"
12+
"github.com/sourcegraph/src-cli/internal/blueprint"
13+
)
14+
15+
type multiStringFlag []string
16+
17+
func (f *multiStringFlag) String() string {
18+
return strings.Join(*f, ", ")
19+
}
20+
21+
func (f *multiStringFlag) Set(value string) error {
22+
*f = append(*f, value)
23+
return nil
24+
}
25+
26+
func (f *multiStringFlag) ToMap() map[string]string {
27+
result := make(map[string]string)
28+
for _, v := range *f {
29+
parts := strings.SplitN(v, "=", 2)
30+
if len(parts) == 2 {
31+
result[parts[0]] = parts[1]
32+
}
33+
}
34+
return result
35+
}
36+
37+
type blueprintImportOpts struct {
38+
client api.Client
39+
out io.Writer
40+
repo string
41+
rev string
42+
subdir string
43+
namespace string
44+
vars map[string]string
45+
dryRun bool
46+
continueOnError bool
47+
}
48+
49+
func init() {
50+
usage := `
51+
'src blueprint import' imports a blueprint from a Git repository or local directory and executes its resources.
52+
53+
Usage:
54+
55+
src blueprint import -repo <repository-url-or-path> [flags]
56+
57+
Examples:
58+
59+
Import a blueprint from the community repository (default):
60+
61+
$ src blueprint import -subdir monitor/cve-2025-55182
62+
63+
Import a specific branch or tag:
64+
65+
$ src blueprint import -rev v1.0.0 -subdir monitor/cve-2025-55182
66+
67+
Import from a local directory:
68+
69+
$ src blueprint import -repo ./my-blueprints -subdir monitor/cve-2025-55182
70+
71+
Import from an absolute path:
72+
73+
$ src blueprint import -repo /path/to/blueprints
74+
75+
Import with custom variables:
76+
77+
$ src blueprint import -subdir monitor/cve-2025-55182 -var webhookUrl=https://example.com/hook
78+
79+
Dry run to validate without executing:
80+
81+
$ src blueprint import -subdir monitor/cve-2025-55182 -dry-run
82+
83+
`
84+
85+
flagSet := flag.NewFlagSet("import", flag.ExitOnError)
86+
usageFunc := func() {
87+
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src blueprint %s':\n", flagSet.Name())
88+
flagSet.PrintDefaults()
89+
fmt.Println(usage)
90+
}
91+
92+
var (
93+
repoFlag = flagSet.String("repo", defaultBlueprintRepo, "Repository URL (HTTPS) or local path to blueprint")
94+
revFlag = flagSet.String("rev", "", "Git revision, branch, or tag to checkout (ignored for local paths)")
95+
subdirFlag = flagSet.String("subdir", "", "Subdirectory in repo containing blueprint.yaml")
96+
namespaceFlag = flagSet.String("namespace", "", "User or org namespace for mutations (defaults to current user)")
97+
dryRunFlag = flagSet.Bool("dry-run", false, "Parse and validate only; do not execute any mutations")
98+
continueOnError = flagSet.Bool("continue-on-error", false, "Continue applying resources even if one fails")
99+
varFlags = multiStringFlag{}
100+
apiFlags = api.NewFlags(flagSet)
101+
)
102+
flagSet.Var(&varFlags, "var", "Variable in the form key=value; can be repeated")
103+
104+
handler := func(args []string) error {
105+
if err := flagSet.Parse(args); err != nil {
106+
return err
107+
}
108+
109+
client := cfg.apiClient(apiFlags, flagSet.Output())
110+
111+
opts := blueprintImportOpts{
112+
client: client,
113+
out: flagSet.Output(),
114+
repo: *repoFlag,
115+
rev: *revFlag,
116+
subdir: *subdirFlag,
117+
namespace: *namespaceFlag,
118+
vars: varFlags.ToMap(),
119+
dryRun: *dryRunFlag,
120+
continueOnError: *continueOnError,
121+
}
122+
123+
return runBlueprintImport(context.Background(), opts)
124+
}
125+
126+
blueprintCommands = append(blueprintCommands, &command{
127+
flagSet: flagSet,
128+
handler: handler,
129+
usageFunc: usageFunc,
130+
})
131+
}
132+
133+
func runBlueprintImport(ctx context.Context, opts blueprintImportOpts) error {
134+
var src blueprint.BlueprintSource
135+
var err error
136+
137+
if opts.subdir == "" {
138+
src, err = blueprint.ResolveRootSource(opts.repo, opts.rev)
139+
} else {
140+
src, err = blueprint.ResolveBlueprintSource(opts.repo, opts.rev, opts.subdir)
141+
}
142+
if err != nil {
143+
return err
144+
}
145+
146+
blueprintDir, cleanup, err := src.Prepare(ctx)
147+
if cleanup != nil {
148+
defer func() { _ = cleanup() }()
149+
}
150+
if err != nil {
151+
return err
152+
}
153+
154+
if opts.subdir == "" {
155+
return runBlueprintImportAll(ctx, opts, blueprintDir)
156+
}
157+
158+
return runBlueprintImportSingle(ctx, opts, blueprintDir)
159+
}
160+
161+
func runBlueprintImportAll(ctx context.Context, opts blueprintImportOpts, rootDir string) error {
162+
found, err := blueprint.FindBlueprints(rootDir)
163+
if err != nil {
164+
return err
165+
}
166+
167+
if len(found) == 0 {
168+
fmt.Fprintf(opts.out, "No blueprints found in repository\n")
169+
return nil
170+
}
171+
172+
fmt.Fprintf(opts.out, "Found %d blueprint(s) in repository\n\n", len(found))
173+
174+
exec := blueprint.NewExecutor(blueprint.ExecutorOpts{
175+
Client: opts.client,
176+
Out: opts.out,
177+
Vars: opts.vars,
178+
DryRun: opts.dryRun,
179+
ContinueOnError: opts.continueOnError,
180+
})
181+
182+
var lastErr error
183+
for _, bp := range found {
184+
subdir, _ := filepath.Rel(rootDir, bp.Dir)
185+
if subdir == "." {
186+
subdir = ""
187+
}
188+
189+
fmt.Fprintf(opts.out, "--- Importing blueprint: %s", bp.Name)
190+
if subdir != "" {
191+
fmt.Fprintf(opts.out, " (%s)", subdir)
192+
}
193+
fmt.Fprintf(opts.out, "\n")
194+
195+
summary, err := exec.Execute(ctx, bp, bp.Dir)
196+
blueprint.PrintExecutionSummary(opts.out, summary, opts.dryRun)
197+
198+
if err != nil {
199+
lastErr = err
200+
if !opts.continueOnError {
201+
return err
202+
}
203+
}
204+
fmt.Fprintf(opts.out, "\n")
205+
}
206+
207+
return lastErr
208+
}
209+
210+
func runBlueprintImportSingle(ctx context.Context, opts blueprintImportOpts, blueprintDir string) error {
211+
bp, err := blueprint.Load(blueprintDir)
212+
if err != nil {
213+
return err
214+
}
215+
216+
fmt.Fprintf(opts.out, "Loaded blueprint: %s\n", bp.Name)
217+
if bp.Title != "" {
218+
fmt.Fprintf(opts.out, " Title: %s\n", bp.Title)
219+
}
220+
if len(bp.BatchSpecs) > 0 {
221+
fmt.Fprintf(opts.out, " Batch specs: %d\n", len(bp.BatchSpecs))
222+
}
223+
if len(bp.Monitors) > 0 {
224+
fmt.Fprintf(opts.out, " Monitors: %d\n", len(bp.Monitors))
225+
}
226+
if len(bp.Insights) > 0 {
227+
fmt.Fprintf(opts.out, " Insights: %d\n", len(bp.Insights))
228+
}
229+
if len(bp.Dashboards) > 0 {
230+
fmt.Fprintf(opts.out, " Dashboards: %d\n", len(bp.Dashboards))
231+
}
232+
233+
exec := blueprint.NewExecutor(blueprint.ExecutorOpts{
234+
Client: opts.client,
235+
Out: opts.out,
236+
Vars: opts.vars,
237+
DryRun: opts.dryRun,
238+
ContinueOnError: opts.continueOnError,
239+
})
240+
241+
summary, err := exec.Execute(ctx, bp, blueprintDir)
242+
blueprint.PrintExecutionSummary(opts.out, summary, opts.dryRun)
243+
244+
return err
245+
}

cmd/src/blueprint_list.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"path/filepath"
8+
9+
"github.com/sourcegraph/src-cli/internal/blueprint"
10+
)
11+
12+
func init() {
13+
usage := `
14+
Examples:
15+
16+
List blueprints from the default community repository:
17+
18+
$ src blueprint list
19+
20+
List blueprints from a GitHub repository:
21+
22+
$ src blueprint list -repo https://github.com/org/blueprints
23+
24+
List blueprints from a specific branch or tag:
25+
26+
$ src blueprint list -repo https://github.com/org/blueprints -rev v1.0.0
27+
28+
List blueprints from a local directory:
29+
30+
$ src blueprint list -repo ./my-blueprints
31+
32+
Print JSON description of all blueprints:
33+
34+
$ src blueprint list -f '{{.|json}}'
35+
36+
List just blueprint names and subdirs:
37+
38+
$ src blueprint list -f '{{.Subdir}}: {{.Name}}'
39+
40+
`
41+
42+
flagSet := flag.NewFlagSet("list", flag.ExitOnError)
43+
usageFunc := func() {
44+
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src blueprint %s':\n", flagSet.Name())
45+
flagSet.PrintDefaults()
46+
fmt.Println(usage)
47+
}
48+
49+
var (
50+
repoFlag = flagSet.String("repo", defaultBlueprintRepo, "Repository URL (HTTPS) or local path to blueprints")
51+
revFlag = flagSet.String("rev", "", "Git revision, branch, or tag to checkout (ignored for local paths)")
52+
formatFlag = flagSet.String("f", "{{.Title}}\t{{.Summary}}\t{{.Subdir}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.|json}}")`)
53+
)
54+
55+
handler := func(args []string) error {
56+
if err := flagSet.Parse(args); err != nil {
57+
return err
58+
}
59+
60+
tmpl, err := parseTemplate(*formatFlag)
61+
if err != nil {
62+
return err
63+
}
64+
65+
src, err := blueprint.ResolveRootSource(*repoFlag, *revFlag)
66+
if err != nil {
67+
return err
68+
}
69+
70+
rootDir, cleanup, err := src.Prepare(context.Background())
71+
if cleanup != nil {
72+
defer func() { _ = cleanup() }()
73+
}
74+
if err != nil {
75+
return err
76+
}
77+
78+
found, err := blueprint.FindBlueprints(rootDir)
79+
if err != nil {
80+
return err
81+
}
82+
83+
for _, bp := range found {
84+
subdir, _ := filepath.Rel(rootDir, bp.Dir)
85+
if subdir == "." {
86+
subdir = ""
87+
}
88+
data := struct {
89+
*blueprint.Blueprint
90+
Subdir string
91+
}{bp, subdir}
92+
if err := execTemplate(tmpl, data); err != nil {
93+
return err
94+
}
95+
}
96+
return nil
97+
}
98+
99+
blueprintCommands = append(blueprintCommands, &command{
100+
flagSet: flagSet,
101+
handler: handler,
102+
usageFunc: usageFunc,
103+
})
104+
}

internal/blueprint/doc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Package blueprint provides parsing and validation for Sourcegraph blueprints.
2+
//
3+
// A blueprint is a collection of Sourcegraph resources (batch specs, monitors,
4+
// insights, dashboards) defined in a blueprint.yaml file that can be imported
5+
// and applied to a Sourcegraph instance.
6+
package blueprint

0 commit comments

Comments
 (0)