Skip to content

tt: added tt create module #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 8, 2022
Merged
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Module ``tt cat``, to print into stdout the contents of .snap/.xlog files.
- Module ``tt play``, to play the contents of .snap/.xlog files to another Tarantool instance.
- Module ``tt coredump``, to pack/unpack/inspect tarantool coredump.
- Module ``tt run``, to start tarantool instance using tt wrapper.
- Module ``tt run``, to start tarantool instance using tt wrapper.
- Module ``tt search``, to show available tt/tarantool versions.
- Module ``tt create``, to create an application from a template.
5 changes: 3 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ Common description. For a detailed description, use ``tt help command`` .
* ``cat`` - print into stdout the contents of .snap/.xlog files.
* ``play`` - play the contents of .snap/.xlog files to another Tarantool instance.
* ``coredump`` - pack/unpack/inspect tarantool coredump.
* ``run`` - start a tarantool instance.
* ``search`` - show available tt/tarantool versions.
* ``run`` - start a tarantool instance.
* ``search`` - show available tt/tarantool versions.
* ``clean`` - clean instance(s) files.
* ``create`` - create an application from a template.
80 changes: 80 additions & 0 deletions cli/cmd/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package cmd

import (
"github.com/apex/log"
"github.com/spf13/cobra"
"github.com/tarantool/tt/cli/cmdcontext"
"github.com/tarantool/tt/cli/configure"
"github.com/tarantool/tt/cli/create"
"github.com/tarantool/tt/cli/modules"
)

var (
appName string
dstPath string
forceMode bool
nonInteractiveMode bool
varsFromCli *[]string
varsFile string
)

// NewCreateCmd creates an application from a template.
func NewCreateCmd() *cobra.Command {
var createCmd = &cobra.Command{
Use: "create [TEMPLATE] [flags]",
Short: "Create an application from a template",
Run: func(cmd *cobra.Command, args []string) {
err := modules.RunCmd(&cmdCtx, cmd.Name(), &modulesInfo, internalCreateModule, args)
if err != nil {
log.Fatalf(err.Error())
}
},
Example: `
# Create an application app1 from a template.

$ tt create <template name> --name app1

# Create cartridge_app application in /opt/tt/apps/, set user_name value,
# force replacing of application directory (cartridge_app) if it exists. ` +
`User interaction is disabled.

$ tt create <template name> --name cartridge_app --var user_name=admin -f ` +
`--non-interactive -dst /opt/tt/apps/`,
}

createCmd.Flags().StringVarP(&appName, "name", "n", "", "Application name")
createCmd.MarkFlagRequired("name")
createCmd.Flags().BoolVarP(&forceMode, "force", "f", false,
`Force rewrite application directory if already exists`)
createCmd.Flags().BoolVarP(&nonInteractiveMode, "non-interactive", "s", false,
`Non-interactive mode`)

varsFromCli = createCmd.Flags().StringArray("var", []string{},
"Variable definition. Usage: --var var_name=value")
createCmd.Flags().StringVarP(&varsFile, "vars-file", "", "", "Variables definition file path")
createCmd.Flags().StringVarP(&dstPath, "dst", "d", "",
"Path to the directory where an application will be created.")

return createCmd
}

// internalCreateModule is a default create module.
func internalCreateModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
cliOpts, err := configure.GetCliOpts(cmdCtx.Cli.ConfigPath)
if err != nil {
return err
}

cmdCtx.Create.AppName = appName
cmdCtx.Create.ForceMode = forceMode
cmdCtx.Create.SilentMode = nonInteractiveMode
cmdCtx.Create.VarsFromCli = *varsFromCli
cmdCtx.Create.VarsFile = varsFile
cmdCtx.Create.DestinationDir = dstPath

if err = create.FillCtx(cliOpts, cmdCtx, args); err != nil {
return err
}

return create.Run(&cmdCtx.Create)
}
1 change: 1 addition & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func NewCmdRoot() *cobra.Command {
NewRunCmd(),
NewSearchCmd(),
NewCleanCmd(),
NewCreateCmd(),
)
if err := injectCmds(rootCmd); err != nil {
panic(err.Error())
Expand Down
30 changes: 30 additions & 0 deletions cli/cmdcontext/cmdcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type CmdCtx struct {
Connect ConnectCtx
// CommandName contains name of the command.
CommandName string
// Create contains information to create an application.
Create CreateCtx
}

// CliCtx - CLI context. Contains flags passed when starting
Expand All @@ -29,6 +31,9 @@ type CliCtx struct {
TarantoolExecutable string
// Tarantool version.
TarantoolVersion string
// WorkDir is stores original tt working directory, before making chdir
// to local launch directory.
WorkDir string
}

// RunningCtx contain information for running an application instance.
Expand Down Expand Up @@ -83,3 +88,28 @@ type ConnectCtx struct {
// SrcFile describes the source of code for the evaluation.
SrcFile string
}

// CreateCtx contains information for creating applications from templates.
type CreateCtx struct {
// AppName is application name to create.
AppName string
// WorkDir is tt launch working directory.
WorkDir string
// DestinationDir is the path where an application will be created.
DestinationDir string
// TemplateSearchPaths is a set of path to search for a template.
TemplateSearchPaths []string
// TemplateName is a template to use for application creation.
TemplateName string
// VarsFromCli base directory for instances available.
VarsFromCli []string
// ForceMode - if flag is set, remove application existing application directory.
ForceMode bool
// SilentMode if set, disables user interaction. All invalid format errors fail
// app creation.
SilentMode bool
// VarsFile is a file with variables definitions.
VarsFile string
// ConfigLocation stores config file location.
ConfigLocation string
Copy link
Contributor

Choose a reason for hiding this comment

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

This parameter looks like a common one for all commands, not unique to create.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These commands have access to ConfigPath common data. And they can make filepath.Dir if config directory is required. Do we really need to store file path and its directory next to each other?

}
8 changes: 8 additions & 0 deletions cli/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ type AppOpts struct {
DataDir string `mapstructure:"data_dir"`
}

// TemplateOpts contains configuration for applications templates.
type TemplateOpts struct {
// Path is a directory to search template in.
Path string `mapstructure:"path"`
}

// CliOpts is used to store modules and app options.
type CliOpts struct {
// Modules is a struct that contain module options.
Expand All @@ -76,4 +82,6 @@ type CliOpts struct {
App *AppOpts
// EE is a struct that contains tarantool-ee options.
EE *EEOpts
// Templates options.
Templates []TemplateOpts
}
7 changes: 7 additions & 0 deletions cli/configure/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ func Cli(cmdCtx *cmdcontext.CmdCtx) error {
}
}

// Current working directory cab changed later, so save the original working directory path.
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("Failed to get working dir info: %s", err)
}
cmdCtx.Cli.WorkDir = cwd

switch {
case cmdCtx.Cli.IsSystem:
return configureSystemCli(cmdCtx)
Expand Down
82 changes: 82 additions & 0 deletions cli/create/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package create

import (
"fmt"
"os"
"path/filepath"

"github.com/tarantool/tt/cli/cmdcontext"
"github.com/tarantool/tt/cli/config"
"github.com/tarantool/tt/cli/create/internal/steps"
"github.com/tarantool/tt/cli/util"
"github.com/tarantool/tt/cli/version"
)

// FillCtx fills create context.
func FillCtx(cliOpts *config.CliOpts, cmdCtx *cmdcontext.CmdCtx, args []string) error {
for _, p := range cliOpts.Templates {
cmdCtx.Create.TemplateSearchPaths = append(cmdCtx.Create.TemplateSearchPaths, p.Path)
}

if len(args) >= 1 {
cmdCtx.Create.TemplateName = args[0]
} else {
return fmt.Errorf("Missing template name argument. " +
"Try `tt create --help` for more information.")
}

cmdCtx.Create.WorkDir = cmdCtx.Cli.WorkDir
cmdCtx.Create.ConfigLocation = filepath.Dir(cmdCtx.Cli.ConfigPath)

return nil
}

// RollbackOnErr removes temporary application directory.
func rollbackOnErr(templateCtx *steps.TemplateCtx) {
if templateCtx.AppPath != "" {
os.RemoveAll(templateCtx.AppPath)
}
templateCtx.AppPath = ""
}

// Run creates an application from a template.
func Run(createCtx *cmdcontext.CreateCtx) error {
util.CheckRecommendedBinaries("git")

if err := checkCtx(createCtx); err != nil {
return util.InternalError("Create context check failed: %s", version.GetVersion, err)
}

stepsChain := []steps.Step{
steps.FillTemplateVarsFromCli{},
steps.LoadVarsFile{},
steps.CreateAppDirectory{},
steps.CopyAppTemplate{},
steps.LoadManifest{},
steps.CollectTemplateVarsFromUser{Reader: steps.NewConsoleReader()},
steps.RunHook{HookType: "pre"},
steps.RenderTemplate{},
steps.RunHook{HookType: "post"},
steps.Cleanup{},
steps.CreateDockerfile{},
}

templateCtx := steps.NewTemplateContext()
for _, step := range stepsChain {
if err := step.Run(createCtx, &templateCtx); err != nil {
rollbackOnErr(&templateCtx)
return err
}
}

return nil
}

// checkCtx checks create context for validity.
func checkCtx(ctx *cmdcontext.CreateCtx) error {
if ctx.TemplateName == "" {
return fmt.Errorf("Template name is missing")
}

return nil
}
77 changes: 77 additions & 0 deletions cli/create/internal/app_template/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package app_template

import (
"fmt"
"os"

"github.com/mitchellh/mapstructure"
"github.com/tarantool/tt/cli/util"
)

const (
DefaultManifestName = "MANIFEST.yaml"
)

// UserPrompt describes interactive prompt to get the value of variable from a user.
type UserPrompt struct {
// Prompt is an input prompt for the variable.
Prompt string
// Name is a variable name to store a value to.
Name string
// Default is a default value.
Default string
// Re is a regular expression for the value validation.
Re string
}

// TemplateManifest is a manifest for application template.
type TemplateManifest struct {
// Description is a template description.
Description string
// Vars is a set of variables, which values are to be
// requested from a user.
Vars []UserPrompt
// PreHook is a path to the executable to run before template instantiation.
// Application path is passed as a first parameter.
PreHook string `mapstructure:"pre-hook"`
// PostHook is a path to the executable to run after template instantiation.
// Application path is passed as a first parameter.
PostHook string `mapstructure:"post-hook"`
// Include contains a list of files to keep after template instantiaion.
Include []string
}

func validateManifest(manifest *TemplateManifest) error {
for _, varInfo := range manifest.Vars {
if varInfo.Prompt == "" {
return fmt.Errorf("Missing user prompt.")
}
if varInfo.Name == "" {
return fmt.Errorf("Missing variable name.")
}
}
return nil
}

// LoadManifest loads template manifest from manifestPath.
func LoadManifest(manifestPath string) (TemplateManifest, error) {
var templateManifest TemplateManifest
if _, err := os.Stat(manifestPath); err != nil {
return templateManifest, fmt.Errorf("Failed to get access to manifest file: %s", err)
}

rawConfigOpts, err := util.ParseYAML(manifestPath)
if err != nil {
return templateManifest, err
}

if err := mapstructure.Decode(rawConfigOpts, &templateManifest); err != nil {
return templateManifest, fmt.Errorf("Failed to decode template manifest: %s", err)
}

if err := validateManifest(&templateManifest); err != nil {
return templateManifest, fmt.Errorf("Invalid manifest format: %s", err)
}

return templateManifest, nil
}
Loading