snapshot/cli/cli.go (271 lines of code) (raw):
package cli
// package cli implements a cli for the SnapshotDB
// This is our first Scoot CLI that works very well with Cobra and also flags that
// main wants to set. How?
//
// First, main.go (either open-source, closed-source, or some future one) runs.
//
// main.go defines its own impl of DBInjector and constructs it; call it DBIImpl.
//
// main.go calls MakeDBCLI with DBIImpl
//
// [not yet needed/implemented] MakeDBCLI calls DBIImpl.RegisterFlags, which registers
// the flags that main needs. These may be related to closed-source impls; e.g., which
// backend server to use.
// MakeDBCLI creates the cobra commands and subcommands
// (for each cobra command, there will be a dbCommand)
// creatings the cobra command involves:
// calling dbCommand.register(), which will register the common functionality flags
// creating the cobra command with RunE as a wrapper function that will call the DBInjector()
//
// MakeDBCLI returns the root *cobra.Command
//
// main.go calls cmd.Execute()
//
// cobra will parse the command-line flags
//
// cobra will call cmd's RunE, which includes the wrapper defined in MakeDBCLI
//
// the wrapper will call DBInjector.Inject(), which will be DBIImpl.Inject()
//
// DBIImpl.Inject() will construct a SnapshotDB
// the wrapper will call dbCommand.run() with the db, the cobra command (which holds the
// registered flags) and the additional command-line args
//
// dbCommand.run() does the work of calling a function on the SnapshotDB
import (
"crypto/sha1"
"fmt"
"os"
"path"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/twitter/scoot/snapshot"
"github.com/twitter/scoot/snapshot/git/gitdb"
"github.com/twitter/scoot/snapshot/git/repo"
"github.com/twitter/scoot/snapshot/store"
"io/ioutil"
)
type DBInjector interface {
// TODO(dbentley): we probably want a way to register flags
RegisterFlags(cmd *cobra.Command)
Inject() (snapshot.DB, error)
}
func MakeDBCLI(injector DBInjector) *cobra.Command {
rootCobraCmd := &cobra.Command{
Use: "scoot-snapshot-db",
Short: "scoot snapshot db CLI",
}
injector.RegisterFlags(rootCobraCmd)
add := func(subCmd dbCommand, parentCobraCmd *cobra.Command) {
cmd := subCmd.register()
cmd.RunE = func(innerCmd *cobra.Command, args []string) error {
db, err := injector.Inject()
if err != nil {
return err
}
return subCmd.run(db, innerCmd, args)
}
parentCobraCmd.AddCommand(cmd)
}
createCobraCmd := &cobra.Command{
Use: "create",
Short: "create a snapshot",
}
rootCobraCmd.AddCommand(createCobraCmd)
add(&ingestGitWorkingDirCommand{}, createCobraCmd)
add(&ingestGitCommitCommand{}, createCobraCmd)
add(&ingestDirCommand{}, createCobraCmd)
add(&createGitBundleCommand{}, createCobraCmd)
readCobraCmd := &cobra.Command{
Use: "read",
Short: "read data from a snapshot",
}
rootCobraCmd.AddCommand(readCobraCmd)
add(&catCommand{}, readCobraCmd)
exportCobraCmd := &cobra.Command{
Use: "export",
Short: "export a snapshot",
}
rootCobraCmd.AddCommand(exportCobraCmd)
add(&exportGitCommitCommand{}, exportCobraCmd)
return rootCobraCmd
}
type dbCommand interface {
register() *cobra.Command
run(db snapshot.DB, cmd *cobra.Command, args []string) error
}
type ingestGitWorkingDirCommand struct{}
func (c *ingestGitWorkingDirCommand) register() *cobra.Command {
cmd := &cobra.Command{
Use: "ingest_git_working_dir",
Short: "ingests HEAD plus the git working dir (into the repo in cwd)",
}
return cmd
}
func (c *ingestGitWorkingDirCommand) run(db snapshot.DB, _ *cobra.Command, _ []string) error {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("cannot get working directory: wd")
}
ingestRepo, err := repo.NewRepository(wd)
if err != nil {
return fmt.Errorf("not a valid repo dir: %v, %v", wd, err)
}
id, err := db.IngestGitWorkingDir(ingestRepo)
if err != nil {
return err
}
fmt.Println(id)
return nil
}
type ingestGitCommitCommand struct {
commit string
}
func (c *ingestGitCommitCommand) register() *cobra.Command {
cmd := &cobra.Command{
Use: "ingest_git_commit",
Short: "ingests a git commit into the repo in cwd and uploads it",
}
cmd.Flags().StringVar(&c.commit, "commit", "", "commit to ingest")
return cmd
}
func (c *ingestGitCommitCommand) run(db snapshot.DB, _ *cobra.Command, _ []string) error {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("cannot get working directory: wd")
}
ingestRepo, err := repo.NewRepository(wd)
if err != nil {
return fmt.Errorf("not a valid repo dir: %v, %v", wd, err)
}
id, err := db.IngestGitCommit(ingestRepo, c.commit)
if err != nil {
return err
}
fmt.Println(id)
return nil
}
// Subcommand for creating and uploading git bundles.
// This is a workaround for creating arbitrary git bundles and keeping them in a Bundlestore.
// We need this for now because generic bundles do not fit well with the existing
// Snapshot, ID, Stream schemas.
// Example usage: scoot-snapshot-db create publish_git_bundle \
// --basis="<rev>" --ref="master" --ttl="336h" --bundlestore_url="http://localhost:9094/bundle"
// Stdout: "http://localhost:9094/bundle/bs-<rev>-master.bundle"
type createGitBundleCommand struct {
basis string // Git bundle basis commit
ref string // Git ref to bundle (will contain `rev-list basis..ref`)
ttld time.Duration // TTL of uploaded bundle in Bundlestore
outputType string // What data for generated bundle is printed to stdout
}
const outputTypeLocation = "location" // Print the location of the uploaded bundle (e.g. the URL)
const outputTypeSnapshotID = "snapshot-id" // Print the snapshot ID of the uploaded bundle
var outputTypes = []string{outputTypeLocation, outputTypeSnapshotID}
func (c *createGitBundleCommand) register() *cobra.Command {
cmd := &cobra.Command{
Use: "publish_git_bundle",
Short: "Creates and uploads a git bundle file as specified by basis/ref for the cwd repo",
}
// See https://git-scm.com/docs/git-bundle for basis, ref usage
cmd.Flags().StringVar(&c.basis, "basis", "", "Basis for bundle")
cmd.Flags().StringVar(&c.ref, "ref", "master", "Reference to be packaged")
cmd.Flags().DurationVar(&c.ttld, "ttl", store.DefaultTTL, "Stored bundle TTL (duration from now)")
cmd.Flags().StringVar(&c.outputType, "output", "location", fmt.Sprintf("Output type, one of: %q", outputTypes))
return cmd
}
// Creates a local bundle file based on basis & reference (see `git help bundle`)
// Bundle name is a sha generated from the rev-list contents of the bundle
// Bundle is uploaded to Bundlestore
// Returns location/URL of uploaded bundle, or SnapshotID of generated bundle
func (c *createGitBundleCommand) run(db snapshot.DB, _ *cobra.Command, _ []string) error {
if c.basis == "" || c.ref == "" {
return fmt.Errorf("Create bundle command requires a basis and reference")
}
validOutput := false
for _, o := range outputTypes {
if o == c.outputType {
validOutput = true
break
}
}
if !validOutput {
return fmt.Errorf("Output type must be one of: %q", outputTypes)
}
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("cannot get working directory: wd")
}
ingestRepo, err := repo.NewRepository(wd)
if err != nil {
return fmt.Errorf("not a valid repo dir: %v, %v", wd, err)
}
gdb, ok := db.(*gitdb.DB)
if !ok {
return fmt.Errorf("create bundle requires a gitdb.DB snapshot.DB")
}
td, err := ioutil.TempDir("", "")
if err != nil {
return fmt.Errorf("Couldn't create temp dir: %v", err)
}
defer func() {
os.RemoveAll(td)
}()
// Don't use commit sha as bundle name as it could collide with other bundles.
// Use the rev-list for the given basis/ref to generate a unique sha
// Use this non-commit sha as bundleKey for a snapshot, but still parse
// commit sha of provided ref to use as snapshot sha.
// TODO (dgassaway): this would be better off in a proper library package
revList := fmt.Sprintf("%s..%s", c.basis, c.ref)
revData, err := ingestRepo.Run("rev-list", revList)
if err != nil {
return fmt.Errorf("Couldn't get rev-list for %s: %v", revList, err)
}
log.Infof("Using rev-list for %s:\n%s\n", revList, revData)
commit, err := ingestRepo.Run("rev-parse", c.ref)
if err != nil {
return fmt.Errorf("Couldn't rev-parse ref %s: %v", c.ref, err)
}
commit = strings.TrimSpace(commit)
// Add ref name to the rev-list we are bundling - otherwise we can create collisions
// where a bundle with the same commit content but different ref name can get the same sha1
revData += c.ref
revSha1 := fmt.Sprintf("%x", sha1.Sum([]byte(revData)))
bundleFilename := path.Join(td, fmt.Sprintf("bs-%s.bundle", revSha1))
if _, err := ingestRepo.Run("-c",
"core.packobjectedgesonlyshallow=0",
"bundle",
"create",
bundleFilename,
revList); err != nil {
return err
}
ttlP := &store.TTLValue{TTL: time.Now().Add(c.ttld), TTLKey: store.DefaultTTLKey}
location, err := gdb.UploadFile(bundleFilename, ttlP)
if err != nil {
return err
}
if c.outputType == outputTypeLocation {
fmt.Println(location)
} else if c.outputType == outputTypeSnapshotID {
// Create a bundlestoreSnapshot to be able to return a valid snapshot ID
bs := gitdb.CreateBundlestoreSnapshot(commit, gitdb.KindGitCommitSnapshot, revSha1, gdb.StreamName())
fmt.Println(bs.ID())
}
return nil
}
type exportGitCommitCommand struct {
id string
}
func (c *exportGitCommitCommand) register() *cobra.Command {
cmd := &cobra.Command{
Use: "to_git_commit",
Short: "exports a GitCommitSnapshot identified by id into the repo in cwd",
}
cmd.Flags().StringVar(&c.id, "id", "", "id to export")
return cmd
}
func (c *exportGitCommitCommand) run(db snapshot.DB, _ *cobra.Command, _ []string) error {
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("cannot get working directory: wd")
}
exportRepo, err := repo.NewRepository(wd)
if err != nil {
return fmt.Errorf("not a valid repo dir: %v, %v", wd, err)
}
commit, err := db.ExportGitCommit(snapshot.ID(c.id), exportRepo)
if err != nil {
return err
}
fmt.Println(commit)
return nil
}
type ingestDirCommand struct {
dir string
}
func (c *ingestDirCommand) register() *cobra.Command {
cmd := &cobra.Command{
Use: "ingest_dir",
Short: "ingests a directory into the repo in cwd",
}
cmd.Flags().StringVar(&c.dir, "dir", "", "dir to ingest")
return cmd
}
func (c *ingestDirCommand) run(db snapshot.DB, _ *cobra.Command, _ []string) error {
id, err := db.IngestDir(c.dir)
if err != nil {
return err
}
fmt.Println(id)
return nil
}
type catCommand struct {
id string
}
func (c *catCommand) register() *cobra.Command {
cmd := &cobra.Command{
Use: "cat",
Short: "concatenate files from an FSSnapshot to stdout",
}
cmd.Flags().StringVar(&c.id, "id", "", "Snapshot ID to read from")
return cmd
}
func (c *catCommand) run(db snapshot.DB, _ *cobra.Command, filenames []string) error {
id := snapshot.ID(c.id)
for _, filename := range filenames {
data, err := db.ReadFileAll(id, filename)
if err != nil {
return err
}
fmt.Printf("%s", data)
}
return nil
}