| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Bundle commands support bundling playground examples into JSON objects |
| // compatible with the playground client. Glob filters allow specifying file |
| // subsets for different implementations of the same example. Bundles specified |
| // in a configuration file can be individually bundled or loaded into the |
| // database as named, default examples. |
| |
| package main |
| |
| import ( |
| "fmt" |
| "os" |
| "path/filepath" |
| |
| "v.io/x/lib/cmdline" |
| "v.io/x/lib/dbutil" |
| "v.io/x/playground/lib" |
| "v.io/x/playground/lib/bundle/bundler" |
| "v.io/x/playground/lib/storage" |
| ) |
| |
| var cmdBundle = &cmdline.Command{ |
| Name: "bundle", |
| Short: "Default bundle management", |
| Long: ` |
| Commands for bundling playground examples and loading default bundles into the |
| database. |
| `, |
| Children: []*cmdline.Command{cmdBundleMake, cmdBundleBootstrap}, |
| } |
| |
| var cmdBundleMake = &cmdline.Command{ |
| Runner: cmdline.RunnerFunc(runBundleMake), |
| Name: "make", |
| Short: "Make a single bundle from config file", |
| Long: ` |
| Bundles the example named <example>, as filtered by <glob_spec>, specified |
| in the bundle config file into a JSON object compatible with the playground |
| client. |
| `, |
| ArgsName: "<example> <glob_spec>", |
| ArgsLong: ` |
| <example>: Name of example in config file to be bundled. |
| |
| <glob_spec>: Name of glob spec in config file to apply when bundling example. |
| Glob spec must be referenced by the example as a valid choice. |
| `, |
| } |
| |
| // TODO(ivanpi): Add bundle metadata (title, description) via config file. |
| // TODO(ivanpi): Iterate over config file, applying commands to bundles (similar to POSIX find)? |
| var cmdBundleBootstrap = &cmdline.Command{ |
| Runner: runWithStorage(runBundleBootstrap), |
| Name: "bootstrap", |
| Short: "Bootstrap bundles from config file into database", |
| Long: ` |
| Bundles all examples specified in the bundle config file and saves them as |
| named default bundles into the database specified by sqlconf, replacing any |
| existing default examples. Bundle slugs are '<example_name>-<glob_name>'. |
| `, |
| } |
| |
| const ( |
| defaultBundleCfg = "${JIRI_ROOT}/release/projects/playground/go/src/v.io/x/playground/bundles/config.json" |
| ) |
| |
| var ( |
| flagBundleCfgFile string |
| flagBundleDir string |
| flagEmpty bool |
| ) |
| |
| func init() { |
| cmdBundle.Flags.StringVar(&flagBundleCfgFile, "bundleconf", defaultBundleCfg, "Path to bundle config file. "+bundler.BundleConfigFileDescription) |
| cmdBundle.Flags.StringVar(&flagBundleDir, "bundledir", "", "Path relative to which paths in the bundle config file are interpreted. If empty, defaults to the config file directory.") |
| cmdBundle.Flags.BoolVar(&flagEmpty, "empty", false, "Omit file contents in bundle, include only paths and metadata.") |
| } |
| |
| // Bundles an example from the specified folder using the specified glob. |
| func runBundleMake(env *cmdline.Env, args []string) error { |
| if len(args) != 2 { |
| return env.UsageErrorf("exactly two arguments expected") |
| } |
| exampleName, globName := args[0], args[1] |
| emptyFlagWarn(env) |
| |
| bundleCfg, err := parseBundleConfig(env) |
| if err != nil { |
| return err |
| } |
| |
| glob, globExists := bundleCfg.Globs[globName] |
| if !globExists { |
| return fmt.Errorf("Unknown glob: %s", globName) |
| } |
| |
| for _, example := range bundleCfg.Examples { |
| if example.Name == exampleName { |
| globValid := false |
| for _, gn := range example.Globs { |
| if gn == globName { |
| globValid = true |
| } |
| } |
| if !globValid { |
| return fmt.Errorf("Invalid glob for example %s: %s", example.Name, globName) |
| } |
| |
| bOut, err := bundler.MakeBundleJson(example.Path, glob.Patterns, flagEmpty) |
| if err != nil { |
| return fmt.Errorf("Bundling %s with %s failed: %v", example.Name, globName, err) |
| } |
| fmt.Fprintln(env.Stdout, string(bOut)) |
| if logVerbose() { |
| fmt.Fprintf(env.Stderr, "Bundled %s using %s\n", example.Name, globName) |
| } |
| |
| return nil |
| } |
| } |
| return fmt.Errorf("Unknown example: %s", exampleName) |
| } |
| |
| // Returns a cmdline.RunnerFunc for loading all bundles specified in the bundle |
| // config file into the database as default bundles. |
| func runBundleBootstrap(env *cmdline.Env, args []string) error { |
| emptyFlagWarn(env) |
| bundleCfg, err := parseBundleConfig(env) |
| if err != nil { |
| return err |
| } |
| |
| var newDefBundles []*storage.NewBundle |
| for _, example := range bundleCfg.Examples { |
| if logVerbose() { |
| fmt.Fprintf(env.Stderr, "Bundling example: %s (%q)\n", example.Name, example.Path) |
| } |
| |
| for _, globName := range example.Globs { |
| glob, globExists := bundleCfg.Globs[globName] |
| if !globExists { |
| return fmt.Errorf("Unknown glob: %s", globName) |
| } |
| if logVerbose() { |
| fmt.Fprintf(env.Stderr, "> glob: %s\n", globName) |
| } |
| |
| bOut, err := bundler.MakeBundleJson(example.Path, glob.Patterns, flagEmpty) |
| if err != nil { |
| return fmt.Errorf("Bundling %s with %s failed: %v", example.Name, globName, err) |
| } |
| |
| // Append the bundle and metadata to new default bundles. |
| newDefBundles = append(newDefBundles, &storage.NewBundle{ |
| BundleDesc: storage.BundleDesc{ |
| Slug: storage.EmptyNullString(example.Name + "-" + globName), |
| }, |
| Json: string(bOut), |
| }) |
| } |
| } |
| |
| if *flagDryRun { |
| fmt.Fprintf(env.Stderr, "Run without dry run to load %d bundles into database\n", len(newDefBundles)) |
| } else { |
| // Unmark old default bundles and store new ones. |
| if err := storage.ReplaceDefaultBundles(newDefBundles); err != nil { |
| return fmt.Errorf("Failed to replace default bundles: %v", err) |
| } |
| if logVerbose() { |
| fmt.Fprintf(env.Stderr, "Successfully loaded %d bundles into database\n", len(newDefBundles)) |
| } |
| } |
| return nil |
| } |
| |
| func emptyFlagWarn(env *cmdline.Env) { |
| if logVerbose() && flagEmpty { |
| fmt.Fprintf(env.Stderr, "Flag -empty set, omitting file contents\n") |
| } |
| } |
| |
| func parseBundleConfig(env *cmdline.Env) (*bundler.Config, error) { |
| bundleCfgFile := os.ExpandEnv(flagBundleCfgFile) |
| bundleDir := os.ExpandEnv(flagBundleDir) |
| // If bundleDir is empty, interpret paths relative to bundleCfg directory. |
| if bundleDir == "" { |
| bundleDir = filepath.Dir(bundleCfgFile) |
| } |
| bundleCfg, err := bundler.ParseConfigFromFile(bundleCfgFile, bundleDir) |
| if err != nil { |
| return nil, fmt.Errorf("Failed parsing bundle config from %q: %v", bundleCfgFile, err) |
| } |
| return bundleCfg, nil |
| } |
| |
| // runWithStorage is a wrapper method that handles opening and closing the |
| // database connections used by `v.io/x/playground/lib/storage`. |
| func runWithStorage(fx cmdline.RunnerFunc) cmdline.RunnerFunc { |
| return func(env *cmdline.Env, args []string) (rerr error) { |
| if !*flagDryRun { |
| if *flagSQLConf == "" { |
| return env.UsageErrorf("SQL configuration file (-sqlconf) must be provided") |
| } |
| |
| // Parse SQL configuration file and set up TLS. |
| dbConf, err := dbutil.ActivateSqlConfigFromFile(*flagSQLConf) |
| if err != nil { |
| return fmt.Errorf("Error parsing SQL configuration: %v", err) |
| } |
| // Connect to storage backend. |
| if err := storage.Connect(dbConf); err != nil { |
| return fmt.Errorf("Error opening database connection: %v", err) |
| } |
| // Best effort close. |
| defer func() { |
| if cerr := storage.Close(); cerr != nil { |
| cerr = fmt.Errorf("Failed closing database connection: %v", cerr) |
| rerr = lib.MergeErrors(rerr, cerr, "\n") |
| } |
| }() |
| } |
| |
| // Run wrapped function. |
| return fx(env, args) |
| } |
| } |