forked from grafana/tanka
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): bulk export (grafana#450)
Changes: * moved generalized logic from `cmd/tk` to `pkg/tanka` * add `ParseParallel` to evaluate envs in parallel * `tk env list` uses `ParseParallel` * `tk export --format` now takes `{{env.<...>}}` and fills it with the tanka.dev/Environment object * `tk export` can export multiple environments and find them recursively with `--recursive` * `tk export` creates a `manifest.json` file to map files back to an environment * `tk export` can use `-l <labelFilters>`, similar to `tk env list` and `kubectl` Examples: ```bash # Format based on environment {{env.<...>}} $ tk export exportDir environments/dev/ --format '{{env.metadata.labels.cluster}}/{{env.spec.namespace}}//{{.kind}}-{{.metadata.name}}' # Export multiple environments $ tk export exportDir environments/dev/ environments/qa/ # Recursive export $ tk export exportDir environments/ --recursive # Recursive export with labelSelector $ tk export exportDir environments/ -r -l team=infra ``` Note: this changes `tk export`, `<exportDir>` now comes before the `<environment>`.
- Loading branch information
Showing
14 changed files
with
641 additions
and
296 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
dist | ||
tk | ||
/tk | ||
/cmd/tk/tk |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,137 +1,88 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"text/template" | ||
|
||
"github.com/Masterminds/sprig/v3" | ||
"github.com/go-clix/cli" | ||
"github.com/pkg/errors" | ||
|
||
"github.com/grafana/tanka/pkg/jsonnet/jpath" | ||
"github.com/grafana/tanka/pkg/tanka" | ||
) | ||
|
||
// BelRune is a string of the Ascii character BEL which made computers ring in ancient times | ||
// We use it as "magic" char for the subfolder creation as it is a non printable character and thereby will never be | ||
// in a valid filepath by accident. Only when we include it. | ||
const BelRune = string(rune(7)) | ||
|
||
func exportCmd() *cli.Command { | ||
args := workflowArgs | ||
args.Validator = cli.ValidateExact(2) | ||
args.Validator = cli.ValidateFunc(func(args []string) error { | ||
if len(args) < 2 { | ||
return fmt.Errorf("expects at least 2 args, received %v", len(args)) | ||
} | ||
return nil | ||
}) | ||
|
||
cmd := &cli.Command{ | ||
Use: "export <environment> <outputDir>", | ||
Short: "write each resources as a YAML file", | ||
Use: "export <outputDir> <path> [<path>...]", | ||
Short: "export environments found in path(s)", | ||
Args: args, | ||
} | ||
|
||
format := cmd.Flags().String("format", "{{.apiVersion}}.{{.kind}}-{{.metadata.name}}", "https://tanka.dev/exporting#filenames") | ||
format := cmd.Flags().String( | ||
"format", | ||
"{{.apiVersion}}.{{.kind}}-{{.metadata.name}}", | ||
"https://tanka.dev/exporting#filenames", | ||
) | ||
|
||
extension := cmd.Flags().String("extension", "yaml", "File extension") | ||
merge := cmd.Flags().Bool("merge", false, "Allow merging with existing directory") | ||
|
||
vars := workflowFlags(cmd.Flags()) | ||
getJsonnetOpts := jsonnetFlags(cmd.Flags()) | ||
getLabelSelector := labelSelectorFlag(cmd.Flags()) | ||
|
||
cmd.Run = func(cmd *cli.Command, args []string) error { | ||
// dir must be empty | ||
to := args[1] | ||
empty, err := dirEmpty(to) | ||
if err != nil { | ||
return fmt.Errorf("Checking target dir: %s", err) | ||
} | ||
if !empty && !*merge { | ||
return fmt.Errorf("Output dir `%s` not empty. Pass --merge to ignore this", to) | ||
} | ||
|
||
// exit early if the template is bad | ||
recursive := cmd.Flags().BoolP("recursive", "r", false, "Look recursively for Tanka environments") | ||
|
||
// Replace all os.path separators in string with BelRune for creating subfolders | ||
replacedFormat := strings.Replace(*format, string(os.PathSeparator), BelRune, -1) | ||
|
||
tmpl, err := template.New(""). | ||
Funcs(sprig.TxtFuncMap()). // register Masterminds/sprig | ||
Parse(replacedFormat) // parse template | ||
if err != nil { | ||
return fmt.Errorf("Parsing name format: %s", err) | ||
} | ||
|
||
// get the manifests | ||
res, err := tanka.Show(args[0], tanka.Opts{ | ||
JsonnetOpts: getJsonnetOpts(), | ||
Filters: stringsToRegexps(vars.targets), | ||
}) | ||
if err != nil { | ||
return err | ||
cmd.Run = func(cmd *cli.Command, args []string) error { | ||
opts := tanka.ExportEnvOpts{ | ||
Format: *format, | ||
Extension: *extension, | ||
Merge: *merge, | ||
Targets: vars.targets, | ||
ParseParallelOpts: tanka.ParseParallelOpts{ | ||
JsonnetOpts: getJsonnetOpts(), | ||
Selector: getLabelSelector(), | ||
}, | ||
} | ||
|
||
// write each to a file | ||
for _, m := range res { | ||
buf := bytes.Buffer{} | ||
if err := tmpl.Execute(&buf, m); err != nil { | ||
log.Fatalln("executing name template:", err) | ||
var paths []string | ||
for _, path := range args[1:] { | ||
if *recursive { | ||
rootDir, err := jpath.FindRoot(path) | ||
if err != nil { | ||
return errors.Wrap(err, "resolving jpath") | ||
} | ||
|
||
// get absolute path to Environment | ||
envs, err := tanka.FindEnvironments(path, opts.ParseParallelOpts.Selector) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for _, env := range envs { | ||
paths = append(paths, filepath.Join(rootDir, env.Metadata.Namespace)) | ||
} | ||
continue | ||
} | ||
|
||
// Replace all os.path separators in string in order to not accidentally create subfolders | ||
name := strings.Replace(buf.String(), string(os.PathSeparator), "-", -1) | ||
// Replace the BEL character inserted with a path separator again in order to create a subfolder | ||
name = strings.Replace(name, BelRune, string(os.PathSeparator), -1) | ||
|
||
// Create all subfolders in path | ||
path := filepath.Join(to, name+"."+*extension) | ||
|
||
// Abort if already exists | ||
if exists, err := fileExists(path); err != nil { | ||
jsonnetOpts := opts.ParseParallelOpts.JsonnetOpts | ||
jsonnetOpts.EvalScript = tanka.EnvsOnlyEvalScript | ||
_, _, err := tanka.ParseEnv(path, jsonnetOpts) | ||
if err != nil { | ||
return err | ||
} else if exists { | ||
return fmt.Errorf("File '%s' already exists. Aborting", path) | ||
} | ||
|
||
// Write file | ||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { | ||
return fmt.Errorf("creating filepath '%s': %s", filepath.Dir(path), err) | ||
} | ||
data := m.String() | ||
if err := ioutil.WriteFile(path, []byte(data), 0644); err != nil { | ||
return fmt.Errorf("writing manifest: %s", err) | ||
} | ||
paths = append(paths, path) | ||
} | ||
|
||
return nil | ||
return tanka.ExportEnvironments(paths, args[0], &opts) | ||
} | ||
return cmd | ||
} | ||
|
||
func fileExists(name string) (bool, error) { | ||
_, err := os.Stat(name) | ||
if os.IsNotExist(err) { | ||
return false, nil | ||
} | ||
if err != nil { | ||
return false, err | ||
} | ||
return true, nil | ||
} | ||
|
||
func dirEmpty(dir string) (bool, error) { | ||
f, err := os.Open(dir) | ||
if os.IsNotExist(err) { | ||
return true, os.MkdirAll(dir, os.ModePerm) | ||
} else if err != nil { | ||
return false, err | ||
} | ||
defer f.Close() | ||
|
||
_, err = f.Readdirnames(1) | ||
if err == io.EOF { | ||
return true, nil | ||
} | ||
return false, err | ||
} |
Oops, something went wrong.