Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions cmd/src/blueprint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"flag"
"fmt"
)

const defaultBlueprintRepo = "https://github.com/sourcegraph-community/blueprints"

var blueprintCommands commander

func init() {
usage := `INTERNAL USE ONLY: 'src blueprint' manages blueprints on a Sourcegraph instance.

Usage:
src blueprint command [command options]

The commands are:

list lists blueprints from a remote repository or local path
import imports blueprints from a remote repository or local path

Use "src blueprint [command] -h" for more information about a command.

`

flagset := flag.NewFlagSet("blueprint", flag.ExitOnError)
handler := func(args []string) error {
blueprintCommands.run(flagset, "src blueprint", usage, args)
return nil
}

// Register the command.
commands = append(commands, &command{
flagSet: flagset,
handler: handler,
usageFunc: func() { fmt.Println(usage) },
})
}
245 changes: 245 additions & 0 deletions cmd/src/blueprint_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package main

import (
"context"
"flag"
"fmt"
"io"
"path/filepath"
"strings"

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

type multiStringFlag []string

func (f *multiStringFlag) String() string {
return strings.Join(*f, ", ")
}

func (f *multiStringFlag) Set(value string) error {
*f = append(*f, value)
return nil
}

func (f *multiStringFlag) ToMap() map[string]string {
result := make(map[string]string)
for _, v := range *f {
parts := strings.SplitN(v, "=", 2)
if len(parts) == 2 {
result[parts[0]] = parts[1]
}
}
return result
}

type blueprintImportOpts struct {
client api.Client
out io.Writer
repo string
rev string
subdir string
namespace string
vars map[string]string
dryRun bool
continueOnError bool
}

func init() {
usage := `
'src blueprint import' imports a blueprint from a Git repository or local directory and executes its resources.

Usage:

src blueprint import -repo <repository-url-or-path> [flags]

Examples:

Import a blueprint from the community repository (default):

$ src blueprint import -subdir monitor/cve-2025-55182

Import a specific branch or tag:

$ src blueprint import -rev v1.0.0 -subdir monitor/cve-2025-55182

Import from a local directory:

$ src blueprint import -repo ./my-blueprints -subdir monitor/cve-2025-55182

Import from an absolute path:

$ src blueprint import -repo /path/to/blueprints

Import with custom variables:

$ src blueprint import -subdir monitor/cve-2025-55182 -var webhookUrl=https://example.com/hook

Dry run to validate without executing:

$ src blueprint import -subdir monitor/cve-2025-55182 -dry-run

`

flagSet := flag.NewFlagSet("import", flag.ExitOnError)
usageFunc := func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src blueprint %s':\n", flagSet.Name())
flagSet.PrintDefaults()
fmt.Println(usage)
}

var (
repoFlag = flagSet.String("repo", defaultBlueprintRepo, "Repository URL (HTTPS) or local path to blueprint")
revFlag = flagSet.String("rev", "", "Git revision, branch, or tag to checkout (ignored for local paths)")
subdirFlag = flagSet.String("subdir", "", "Subdirectory in repo containing blueprint.yaml")
namespaceFlag = flagSet.String("namespace", "", "User or org namespace for mutations (defaults to current user)")
dryRunFlag = flagSet.Bool("dry-run", false, "Parse and validate only; do not execute any mutations")
continueOnError = flagSet.Bool("continue-on-error", false, "Continue applying resources even if one fails")
varFlags = multiStringFlag{}
apiFlags = api.NewFlags(flagSet)
)
flagSet.Var(&varFlags, "var", "Variable in the form key=value; can be repeated")

handler := func(args []string) error {
if err := flagSet.Parse(args); err != nil {
return err
}

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

opts := blueprintImportOpts{
client: client,
out: flagSet.Output(),
repo: *repoFlag,
rev: *revFlag,
subdir: *subdirFlag,
namespace: *namespaceFlag,
vars: varFlags.ToMap(),
dryRun: *dryRunFlag,
continueOnError: *continueOnError,
}

return runBlueprintImport(context.Background(), opts)
}

blueprintCommands = append(blueprintCommands, &command{
flagSet: flagSet,
handler: handler,
usageFunc: usageFunc,
})
}

func runBlueprintImport(ctx context.Context, opts blueprintImportOpts) error {
var src blueprint.BlueprintSource
var err error

if opts.subdir == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe this distinction should be made in a top level blueprint.NewSource / blueprint.ResolveSource method? We then unexport ResolveRootSource / ResolveBlueprintSource and just have a single Resolve method exposed that returns the correct source for us.

src, err = blueprint.ResolveRootSource(opts.repo, opts.rev)
} else {
src, err = blueprint.ResolveBlueprintSource(opts.repo, opts.rev, opts.subdir)
}
if err != nil {
return err
}

blueprintDir, cleanup, err := src.Prepare(ctx)
if cleanup != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not always return a valid cleanup? then you don't have to check it the whole time and can just do defer cleanup() with confidence

defer func() { _ = cleanup() }()
}
if err != nil {
return err
}

if opts.subdir == "" {
return runBlueprintImportAll(ctx, opts, blueprintDir)
}

return runBlueprintImportSingle(ctx, opts, blueprintDir)
}

func runBlueprintImportAll(ctx context.Context, opts blueprintImportOpts, rootDir string) error {
found, err := blueprint.FindBlueprints(rootDir)
if err != nil {
return err
}

if len(found) == 0 {
fmt.Fprintf(opts.out, "No blueprints found in repository\n")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Fprintf(opts.out, "No blueprints found in repository\n")
fmt.Fprintf(opts.out, "No blueprints found in %s\n", rootDir)

return nil
}

fmt.Fprintf(opts.out, "Found %d blueprint(s) in repository\n\n", len(found))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Fprintf(opts.out, "Found %d blueprint(s) in repository\n\n", len(found))
fmt.Fprintf(opts.out, "Found %d blueprint(s) in repository\n", len(found))

unless the extra \n was intentional?


exec := blueprint.NewExecutor(blueprint.ExecutorOpts{
Client: opts.client,
Out: opts.out,
Vars: opts.vars,
DryRun: opts.dryRun,
ContinueOnError: opts.continueOnError,
})

var lastErr error
for _, bp := range found {
subdir, _ := filepath.Rel(rootDir, bp.Dir)
if subdir == "." {
subdir = ""
}

fmt.Fprintf(opts.out, "--- Importing blueprint: %s", bp.Name)
if subdir != "" {
fmt.Fprintf(opts.out, " (%s)", subdir)
}
fmt.Fprintf(opts.out, "\n")

summary, err := exec.Execute(ctx, bp, bp.Dir)
blueprint.PrintExecutionSummary(opts.out, summary, opts.dryRun)

if err != nil {
lastErr = err
if !opts.continueOnError {
return err
}
}
fmt.Fprintf(opts.out, "\n")
}

return lastErr
}

func runBlueprintImportSingle(ctx context.Context, opts blueprintImportOpts, blueprintDir string) error {
bp, err := blueprint.Load(blueprintDir)
if err != nil {
return err
}

fmt.Fprintf(opts.out, "Loaded blueprint: %s\n", bp.Name)
if bp.Title != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can roll all this fmt.Fprintf to be part of bp.String()

fmt.Fprintln(opts.out, bp.String())

fmt.Fprintf(opts.out, " Title: %s\n", bp.Title)
}
if len(bp.BatchSpecs) > 0 {
fmt.Fprintf(opts.out, " Batch specs: %d\n", len(bp.BatchSpecs))
}
if len(bp.Monitors) > 0 {
fmt.Fprintf(opts.out, " Monitors: %d\n", len(bp.Monitors))
}
if len(bp.Insights) > 0 {
fmt.Fprintf(opts.out, " Insights: %d\n", len(bp.Insights))
}
if len(bp.Dashboards) > 0 {
fmt.Fprintf(opts.out, " Dashboards: %d\n", len(bp.Dashboards))
}

exec := blueprint.NewExecutor(blueprint.ExecutorOpts{
Client: opts.client,
Out: opts.out,
Vars: opts.vars,
DryRun: opts.dryRun,
ContinueOnError: opts.continueOnError,
})

summary, err := exec.Execute(ctx, bp, blueprintDir)
blueprint.PrintExecutionSummary(opts.out, summary, opts.dryRun)

return err
}
104 changes: 104 additions & 0 deletions cmd/src/blueprint_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package main

import (
"context"
"flag"
"fmt"
"path/filepath"

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

func init() {
usage := `
Examples:

List blueprints from the default community repository:

$ src blueprint list

List blueprints from a GitHub repository:

$ src blueprint list -repo https://github.com/org/blueprints

List blueprints from a specific branch or tag:

$ src blueprint list -repo https://github.com/org/blueprints -rev v1.0.0

List blueprints from a local directory:

$ src blueprint list -repo ./my-blueprints

Print JSON description of all blueprints:

$ src blueprint list -f '{{.|json}}'

List just blueprint names and subdirs:

$ src blueprint list -f '{{.Subdir}}: {{.Name}}'

`

flagSet := flag.NewFlagSet("list", flag.ExitOnError)
usageFunc := func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src blueprint %s':\n", flagSet.Name())
flagSet.PrintDefaults()
fmt.Println(usage)
}

var (
repoFlag = flagSet.String("repo", defaultBlueprintRepo, "Repository URL (HTTPS) or local path to blueprints")
revFlag = flagSet.String("rev", "", "Git revision, branch, or tag to checkout (ignored for local paths)")
formatFlag = flagSet.String("f", "{{.Title}}\t{{.Summary}}\t{{.Subdir}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.|json}}")`)
)

handler := func(args []string) error {
if err := flagSet.Parse(args); err != nil {
return err
}

tmpl, err := parseTemplate(*formatFlag)
if err != nil {
return err
}

src, err := blueprint.ResolveRootSource(*repoFlag, *revFlag)
if err != nil {
return err
}

rootDir, cleanup, err := src.Prepare(context.Background())
if cleanup != nil {
defer func() { _ = cleanup() }()
}
if err != nil {
return err
}

found, err := blueprint.FindBlueprints(rootDir)
if err != nil {
return err
}

for _, bp := range found {
subdir, _ := filepath.Rel(rootDir, bp.Dir)
if subdir == "." {
subdir = ""
}
data := struct {
*blueprint.Blueprint
Subdir string
}{bp, subdir}
if err := execTemplate(tmpl, data); err != nil {
return err
}
}
return nil
}

blueprintCommands = append(blueprintCommands, &command{
flagSet: flagSet,
handler: handler,
usageFunc: usageFunc,
})
}
6 changes: 6 additions & 0 deletions internal/blueprint/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Package blueprint provides parsing and validation for Sourcegraph blueprints.
//
// A blueprint is a collection of Sourcegraph resources (batch specs, monitors,
// insights, dashboards) defined in a blueprint.yaml file that can be imported
// and applied to a Sourcegraph instance.
package blueprint
Loading
Loading