diff --git a/README.md b/README.md index 1d8ba10..e49ed37 100644 --- a/README.md +++ b/README.md @@ -77,11 +77,16 @@ The `trx.yaml` file inside the project repository defines commands and environme Example: ```yaml -commands: - - werf converge - - echo "{{ .RepoUrl }} / {{ .RepoTag }} / {{ .RepoCommit }}" -env: - WERF_ENV: "production" +tasks: + - name: deploy + commands: + - werf converge + - echo "{{ .RepoUrl }} / {{ .RepoTag }} / {{ .RepoCommit }}" + - name: export + env: + WERF_SET_GIT_REV: "werf.git_rev={{ .RepoCommit }}" + commands: + - werf export ``` Available template variables: @@ -108,21 +113,23 @@ repo: # Optional, default is `trx.yaml` in the repository. configFile: "trx.yaml" - - # Optional. Commands defined here have a higher priority than those specified in `trx.yaml`. - commands: - - werf converge - - echo "{{ .RepoUrl }} / {{ .RepoTag }} / {{ .RepoCommit }}" - - # Optional. Set environment variables here to be used in the commands. - # Environment variables defined here are merged with those in the configFile, - # but have higher priority (values in this section will override those in the configFile). - env: - WERF_ENV: "production" - # Optional. Ensures processing starts from a specific tag and prevents processing older tags (safeguard against freeze attacks). initialLastProcessedTag: "v0.10.1" +# Optional. Tasks defined here have a higher priority than those specified in `trx.yaml`. +tasks: + - name: deploy + commands: + - werf converge + - echo "{{ .RepoUrl }} / {{ .RepoTag }} / {{ .RepoCommit }}" + env: + WERF_BUILDAH_MODE: auto + - name: export + env: + WERF_SET_GIT_REV: "werf.git_rev={{ .RepoCommit }}" + commands: + - werf export + quorums: - name: main minNumberOfKeys: 1 @@ -138,12 +145,14 @@ quorums: # Optional. Define actions to be taken at different stages of command execution. hooks: + env: + MSG: 'Task {{ .FailedTaskName }} failed' onCommandStarted: - "echo 'Command started: {{ .RepoTag }} at {{ .RepoCommit }}'" onCommandSuccess: - "echo 'Success: {{ .RepoTag }}'" onCommandFailure: - - "echo 'Failure: {{ .RepoTag }}'" + - "echo $MSG" onCommandSkipped: - "echo 'Skipped: {{ .RepoTag }}'" onQuorumFailure: @@ -182,7 +191,16 @@ trx --config trx.yaml -- ls -la ``` To force the execution even if no new version is detected, use the `--force` flag: - ```sh trx --force +``` + +To run on specific tag use `-r` or `--reference` flags: +```sh +trx --reference v0.0.0 +``` + +To disable quorum checking use `--disable-quorums-check` flag: +```sh +trx --disable-quorums-check ``` \ No newline at end of file diff --git a/cmd/trx/main.go b/cmd/trx/main.go index fa8d148..a42df86 100644 --- a/cmd/trx/main.go +++ b/cmd/trx/main.go @@ -2,14 +2,18 @@ package main import ( "log" + "os" "github.com/spf13/cobra" ) var ( - configPath string - force bool - disableLock bool + configPath string + force bool + disableLock bool + disableQuorumsCheck bool + reference string + task string ) type runOptions struct { @@ -32,12 +36,17 @@ By default, it uses the ./trx.yaml configuration file, but you can specify a dif } rootCmd.SilenceUsage = true + rootCmd.SilenceErrors = true rootCmd.PersistentFlags().StringVar(&configPath, "config", "./trx.yaml", "Path to config file") rootCmd.Flags().BoolVarP(&force, "force", "f", false, "Force execution if no new version found") rootCmd.Flags().BoolVarP(&disableLock, "disable-lock", "", false, "Disable execution locking") + rootCmd.Flags().BoolVarP(&disableQuorumsCheck, "disable-quorums-check", "", false, "Run without checking quorums") + rootCmd.Flags().StringVarP(&reference, "reference", "r", "", "Tag to run on (default: latest tag)") + rootCmd.Flags().StringVarP(&task, "task", "t", "", "Name of the task to run. If no name provided use ordinal number e.g. 1,2,3...etc. (default: first task in config)") if err := rootCmd.Execute(); err != nil { - log.Fatal(err) + log.Println(err) + os.Exit(1) } } diff --git a/cmd/trx/run.go b/cmd/trx/run.go index 18e4b7f..f1ad355 100644 --- a/cmd/trx/run.go +++ b/cmd/trx/run.go @@ -7,18 +7,23 @@ import ( "log" "os" "os/signal" - "strings" "syscall" "time" - "trx/internal/command" "trx/internal/config" "trx/internal/git" + "trx/internal/hooks" "trx/internal/lock" "trx/internal/quorum" "trx/internal/storage" + "trx/internal/tasks" + "trx/internal/templates" ) +type Executor interface { + RunTasks(tasks []tasks.Task) error +} + func run(opts runOptions) error { log.SetFlags(0) log.SetOutput(os.Stdout) @@ -38,14 +43,7 @@ func run(opts runOptions) error { cfg, err := config.NewConfig(configPath) if err != nil { - return fmt.Errorf("config error: %w", err) - } - - storage, err := storage.NewStorage(&storage.StorageOpts{ - Config: cfg, - }) - if err != nil { - return fmt.Errorf("init storage error: %w", err) + return err } locker := lock.NewManager(lock.NewLocalLocker(disableLock)) @@ -56,126 +54,117 @@ func run(opts runOptions) error { log.Println("Processing without execution lock") } - gitClient, err := git.NewGitClient(cfg.Repo) + gitClient, err := git.NewGitClient(*cfg.Repo) if err != nil { return fmt.Errorf("new git client error: %w", err) } - gitTargetObject, err := gitClient.GetTargetGitObject() + gitTargetObject, err := gitClient.GetTargetGitObject(reference) if err != nil { return fmt.Errorf("get target git object error: %w", err) } - lastSucceedTag, err := storage.CheckLastSucceedTag() - if err != nil { - return fmt.Errorf("check last published commit error: %w", err) - } + repoTemplatevars := templates.GetRepoTemplateVars(templates.RepoTemplateVarsData{ + RepoTag: gitTargetObject.Tag, + RepoUrl: cfg.Repo.Url, + RepoCommit: gitTargetObject.Commit, + }) - executor, err := command.NewExecutor(ctx, cfg.Env, generateCmdVars(cfg, gitTargetObject)) + hookExecutor, err := hooks.NewHookExecutor(ctx, cfg, hooks.HookExecutorOptions{ + TemplateVars: repoTemplatevars, + WorkDir: gitClient.RepoPath, + }) if err != nil { - return fmt.Errorf("command executor error: %w", err) + return fmt.Errorf("hooks executor error: %w", err) } - isNewVersion, err := git.IsNewerVersion(gitTargetObject.Tag, lastSucceedTag, cfg.Repo.InitialLastProcessedTag) - if err != nil { - return fmt.Errorf("can't check if tag is new: %w", err) - } - if !isNewVersion { - switch force { - case true: - log.Println("No new version, but force flag specified. Proceeding... ") - case false: - if hookErr := executor.RunOnCommandSkippedHook(cfg); hookErr != nil { - log.Println("WARNING onCommandSkipped hook execution error: %w", hookErr) - } - log.Println("No new version. execution will be skipped") - return nil + if !disableQuorumsCheck { + if err := quorum.CheckQuorums(&quorum.CheckQuorumsRequest{ + Quorums: cfg.Quorums, + Repo: gitClient.Repo, + Tag: gitTargetObject.Tag, + HookExecutor: hookExecutor, + }); err != nil { + return err } } - err = quorum.CheckQuorums(cfg.Quorums, gitClient.Repo, gitTargetObject.Tag) + storage, err := storage.NewStorage(&storage.StorageOpts{ + Config: cfg, + }) if err != nil { - var qErr *quorum.Error - if errors.As(err, &qErr) { - executor.Vars["FailedQuorumName"] = qErr.QuorumName - if hookErr := executor.RunOnQuorumFailedHook(cfg); hookErr != nil { - log.Println("WARNING onCommandSkipped hook execution error: %w", hookErr) - } - return fmt.Errorf("quorum error: %w", qErr.Err) - } else { - return fmt.Errorf("quorum error: %w", err) - } + return fmt.Errorf("init storage error: %w", err) } - cmdsToRun, err := getCmdsToRun(cfg, opts, executor) + taskExecutor, err := getExecutor(ctx, tasks.TaskExecutorOptions{ + Storage: storage, + TemplateVars: repoTemplatevars, + WorkDir: gitClient.RepoPath, + }) if err != nil { - return fmt.Errorf("get commands to run error: %w", err) - } - - // TODO: think about running this hook concurrently with the command - if hookErr := executor.RunOnCommandStartedHook(cfg); hookErr != nil { - log.Printf("WARNING onCommandStarted hook execution error: %s", hookErr.Error()) + return fmt.Errorf("task executor error: %w", err) } - if err := executor.Exec(cmdsToRun); err != nil { - if hookErr := executor.RunOnCommandFailureHook(cfg); hookErr != nil { - log.Println("WARNING onCommandFailure hook execution error: %w", hookErr) - } - return fmt.Errorf("run command error: %w", err) + tasksToRun, err := tasks.GetTasksToRun(cfg, gitClient.RepoPath, tasks.GetTasksToRunOpts{ + CmdFromCli: opts.cmdFromCli, + Forced: force, + TargetTaskName: task, + Version: gitTargetObject.Tag, + }) + if err != nil { + return fmt.Errorf("task executor error: %w", err) } - if err := storage.StoreSucceedTag(gitTargetObject.Tag); err != nil { - return fmt.Errorf("store last successed tag error: %w", err) + // TODO: think about running this hook concurrently with the command + for _, t := range tasksToRun { + hookExecutor.RunOnCommandStartedHook(t.Name) } - if hookErr := executor.RunOnCommandSuccessHook(cfg); hookErr != nil { - log.Println("WARNING onCommandSuccess hook execution error: %w", hookErr) + if err := taskExecutor.RunTasks(tasksToRun); err != nil { + return handleRunTasksError(err, hookExecutor) } - log.Println("All done") + hookExecutor.RunOnCommandSuccessHook() return nil } -func generateCmdVars(cfg *config.Config, t *git.TargetGitObject) map[string]string { - vars := make(map[string]string) - vars["RepoTag"] = t.Tag - vars["RepoUrl"] = cfg.Repo.Url - vars["RepoCommit"] = t.Commit - return vars -} +func handleRunTasksError(err error, hookExecutor *hooks.HookExecutor) error { + var runErr *tasks.Error + if errors.As(err, &runErr) { + switch { + case errors.Is(runErr.Err, tasks.ErrNoNewVersion): + hookExecutor.RunOnCommandSkippedHook() + log.Printf("task %s skipped: no new version detected\n", runErr.TaskName) + return nil -func mergeEnvs(envs, cfgEnv map[string]string) []string { - for k, v := range cfgEnv { - envs[k] = v - } - newEnv := make([]string, 0, len(envs)) - for k, v := range envs { - newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, v)) + case errors.Is(runErr.Err, tasks.ErrExcutionFailed): + hookExecutor.RunOnCommandFailureHook(runErr.TaskName) + return fmt.Errorf("task %s failed: %w", runErr.TaskName, runErr.Err) + + default: + return fmt.Errorf("task running error: %w", runErr.Err) + } } - return newEnv + return fmt.Errorf("tasks running error: %w", err) } -func getCmdsToRun(cfg *config.Config, opts runOptions, executor *command.Executor) ([]string, error) { - var cmdsToRun []string - if len(opts.cmdFromCli) > 0 { - cmdsToRun = []string{strings.Join(opts.cmdFromCli, " ")} - return cmdsToRun, nil +func getExecutor(ctx context.Context, opts tasks.TaskExecutorOptions) (Executor, error) { + commonOpts := tasks.TaskExecutorOptions{ + Storage: opts.Storage, + TemplateVars: opts.TemplateVars, + WorkDir: opts.WorkDir, } - - if len(cfg.Commands) > 0 { - cmdsToRun = cfg.Commands + if force { + taskExecutor, err := tasks.NewTaskForceExecutor(ctx, commonOpts) + if err != nil { + return nil, fmt.Errorf("task executor error: %w", err) + } + return taskExecutor, nil } else { - runCfg, err := config.NewRunnerConfig(command.WorkDir, cfg.Repo.ConfigFile) + taskExecutor, err := tasks.NewTaskExecutor(ctx, commonOpts) if err != nil { - return nil, fmt.Errorf("config error: %w", err) + return nil, fmt.Errorf("task executor error: %w", err) } - cmdsToRun = runCfg.Commands - executor.Env = mergeEnvs(cfg.Env, runCfg.Env) + return taskExecutor, nil } - - if len(cmdsToRun) == 0 { - return nil, fmt.Errorf("no commands to run") - } - - return cmdsToRun, nil } diff --git a/e2e/flow/_fixtures/pgp_keys/developer_private.pgp b/e2e/flow/_fixtures/pgp_keys/developer_private.pgp new file mode 100644 index 0000000..abf28c9 --- /dev/null +++ b/e2e/flow/_fixtures/pgp_keys/developer_private.pgp @@ -0,0 +1,81 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=VtLC +-----END PGP PRIVATE KEY BLOCK----- diff --git a/e2e/flow/_fixtures/pgp_keys/developer_public.pgp b/e2e/flow/_fixtures/pgp_keys/developer_public.pgp new file mode 100644 index 0000000..62e5527 --- /dev/null +++ b/e2e/flow/_fixtures/pgp_keys/developer_public.pgp @@ -0,0 +1,41 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +XaOlJrPDM5E9zw== +=bIYD +-----END PGP PUBLIC KEY BLOCK----- diff --git a/e2e/flow/_fixtures/pgp_keys/pm_private.pgp b/e2e/flow/_fixtures/pgp_keys/pm_private.pgp new file mode 100644 index 0000000..34d9795 --- /dev/null +++ b/e2e/flow/_fixtures/pgp_keys/pm_private.pgp @@ -0,0 +1,81 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=qxSs +-----END PGP PRIVATE KEY BLOCK----- diff --git a/e2e/flow/_fixtures/pgp_keys/pm_public.pgp b/e2e/flow/_fixtures/pgp_keys/pm_public.pgp new file mode 100644 index 0000000..141942b --- /dev/null +++ b/e2e/flow/_fixtures/pgp_keys/pm_public.pgp @@ -0,0 +1,41 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +WPi4XYQTYGzk +=4KEN +-----END PGP PUBLIC KEY BLOCK----- diff --git a/e2e/flow/_fixtures/pgp_keys/tl_private.pgp b/e2e/flow/_fixtures/pgp_keys/tl_private.pgp new file mode 100644 index 0000000..01b855e --- /dev/null +++ b/e2e/flow/_fixtures/pgp_keys/tl_private.pgp @@ -0,0 +1,81 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +WXtkD/Bn30CpLkYx0Yr97KZxQF44mUyfRGUW +=Xho7 +-----END PGP PRIVATE KEY BLOCK----- diff --git a/e2e/flow/_fixtures/pgp_keys/tl_public.pgp b/e2e/flow/_fixtures/pgp_keys/tl_public.pgp new file mode 100644 index 0000000..7e5830c --- /dev/null +++ b/e2e/flow/_fixtures/pgp_keys/tl_public.pgp @@ -0,0 +1,41 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +TJ9EZRY= +=vkcq +-----END PGP PUBLIC KEY BLOCK----- diff --git a/e2e/flow/complete_cycle_test.go b/e2e/flow/complete_cycle_test.go new file mode 100644 index 0000000..a0a37ef --- /dev/null +++ b/e2e/flow/complete_cycle_test.go @@ -0,0 +1,204 @@ +package flow + +import ( + "fmt" + "trx/internal/config" + + . "github.com/onsi/ginkgo/v2" + "github.com/werf/trdl/server/pkg/testutil" +) + +type testOptions struct { + trxConfig *config.Config + + mainCfgPath string + + addtasksInMainCfg bool + + pgpKeys map[string]string + tag1 string +} + +var _ = Describe("trx flow test", Label("e2e", "trx", "flow"), func() { + DescribeTable("should perform all steps", + func(testOpts testOptions) { + By("initializing git repo") + { + + initGitRepo(SuiteData.TestDir, "main") + + } + By("creating main config file") + { + testOpts.trxConfig = &config.Config{ + Repo: &config.GitRepo{ + Url: SuiteData.TestDir, + }, + Quorums: []config.Quorum{ + { + Name: func(s string) *string { return &s }("dev"), + MinNumberOfKeys: 1, + GPGKeys: []string{ + keyMap["developer"], + }, + GPGKeyFilesPaths: nil, + }, + { + Name: func(s string) *string { return &s }("prod"), + MinNumberOfKeys: 1, + GPGKeyFilesPaths: []string{FixturePath("pgp_keys", "pm_public.pgp")}, + }, + }, + Hooks: config.Hooks{ + Env: map[string]string{ + "WERF_SET_GIT_REV": "werf.git_rev={{ .RepoCommit }}", + "MESSAGE": `:eight_pointed_black_star: Start converge {{ .RepoTag }} for ({{ .RepoUrl }})`, + }, + OnCommandStarted: &[]string{ + "echo 'task !!{{ .StartedTaskName }}!! started'", "echo $WERF_SET_GIT_REV", + }, + OnCommandSuccess: &[]string{ + "echo 'command success'", + }, + OnCommandFailure: &[]string{ + "echo 'command failure'", + }, + OnCommandSkipped: &[]string{ + "echo $MESSAGE", + }, + }, + } + + if testOpts.addtasksInMainCfg { + testOpts.trxConfig.Tasks = []config.Task{ + { + Name: "dev", + Env: map[string]string{"TRX_ENV": "dev"}, + Commands: []string{"echo 'hello world'", "echo $TRX_ENV"}, + InitialLastProcessedTag: "", + }, + { + Name: "test", + Env: map[string]string{"TRX_ENV": "test"}, + Commands: []string{"echo 'hello world'", "echo $TRX_ENV"}, + InitialLastProcessedTag: "", + }, + } + } + + testOpts.mainCfgPath, _ = writeConfigFile(SuiteData.TestDir, testOpts.trxConfig) + + } + By(fmt.Sprintf("Creating tag tag %q", testOpts.tag1)) + { + gitTag(SuiteData.TestDir, testOpts.tag1, testOpts.pgpKeys["developer"]) + + By(fmt.Sprintf("[server] Signing tag %q", testOpts.tag1)) + quorumSignTag(SuiteData.TestDir, testOpts.pgpKeys["tl"], testOpts.pgpKeys["pm"], testOpts.tag1) + } + By(fmt.Sprintf("Running trx with tag %q", testOpts.tag1)) + { + testutil.RunSucceedCommand( + "", + SuiteData.TrxBinPath, + "--config", testOpts.mainCfgPath, + ) + } + By(fmt.Sprintf("Running trx with tag %q should be skipped", testOpts.tag1)) + { + testutil.RunSucceedCommand( + "", + SuiteData.TrxBinPath, + "--config", testOpts.mainCfgPath, + ) + } + By(fmt.Sprintf("Running trx with tag %q another task should successed", testOpts.tag1)) + { + testutil.RunSucceedCommand( + "", + SuiteData.TrxBinPath, + "--config", testOpts.mainCfgPath, "--task", "test", + ) + } + By(fmt.Sprintf("Running trx with tag %q force", testOpts.tag1)) + { + testutil.RunSucceedCommand( + "", + SuiteData.TrxBinPath, + "--config", testOpts.mainCfgPath, "--task", "test", "--force", + ) + } + By(fmt.Sprintf("Running trx with tag %q force cli", testOpts.tag1)) + { + testutil.RunSucceedCommand( + "", + SuiteData.TrxBinPath, + "--config", testOpts.mainCfgPath, "--force", "--", "ls", + ) + } + }, + Entry("standart test -- tasks from main file", testOptions{ + tag1: "v0.0.1", + pgpKeys: map[string]string{ + "developer": "74E1259029B147CB4033E8B80D4C9C140E8A1030", + "tl": "2BA55FD8158034EEBE92AA9ED9D79B63AFC30C7A", + "pm": "C353F279F552B3EF16DAE0A64354E51BF178F735", + }, + addtasksInMainCfg: true, + }), + Entry("standart test -- tasks from repo file", testOptions{ + tag1: "v0.0.1", + pgpKeys: map[string]string{ + "developer": "74E1259029B147CB4033E8B80D4C9C140E8A1030", + "tl": "2BA55FD8158034EEBE92AA9ED9D79B63AFC30C7A", + "pm": "C353F279F552B3EF16DAE0A64354E51BF178F735", + }, + addtasksInMainCfg: false, + }), + ) +}) + +var keyMap = map[string]string{ + "developer": `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBGH6xLwBDACmDGe0qiJ3jXAJFbuWVMV6yAhk0ube/qGtijnsbyAkSU9bG6DM +DWgIVY1C86KVBqQBnJpiIsWYTUbtmxjEgg+KgUCxHUYXXhiTBW6aD+7Mpj7mxQ3A +Zim/8pNAIPRtQHTODPpFFxekfO1XuFC+CPQv3/XsuVHv6rTKK9V+ScbVL0Et7Vc9 +PuZJfhTSrKQUnL8AMsI4cpLObO68lee3uU70aGG1twd0kfwzKuTTODCYIxbMfpAS +cMiORMYyK/e94mZb1EK0qVuZTiOqhVFjBFcMBeRDnUzB4nM3wWiVOdA/2TItLxyG +4QnQ/BSzBJRumdaFvk26rgTcacdXFiNUviODhM8J12JOYAq8d75ipQ3wyPDwz2IJ +3ZoeNhq66UslMpdL7xWK/06IelPCk2WrSWU+NGmmR0wBu1pnHZwS64gwjakH0OgH +cAKa1UQPBcpC35yoxToWn+HpUBx+cehPfRyWP9F3CdkleJQ6UVvpfwU1uJgSqt0V +Wvdb7rz+4T3spMMAEQEAAbQeRGV2ZWxvcGVyIDxkZXZlbG9wZXJAdHJkbC5kZXY+ +iQHOBBMBCgA4FiEEdOElkCmxR8tAM+i4DUycFA6KEDAFAmH6xLwCGwMFCwkIBwIG +FQoJCAsCBBYCAwECHgECF4AACgkQDUycFA6KEDANEQv9GkFZz2+/giuhY82RKpS1 +doiNfMezGRnQqp73x6ot24/HwbCxDyrnfpGv145qIH9ApKFRGMNvQHpAWYEfWddo +nHo9kkR7qqVaKnR9/V7NzuyOKbI4rtB/1i9RQjz1JLctvGY/7WdA0SVDz+tPnSBw +/aIfa5nEgD20Oyqgd8qakHfyHFVmfMGQ27rDihuNOHuL1eDmschEeFRPa3uzKeIQ +tOuw0uw9jSDOLoHGUCe3SmV7oMJ+B4biDL7ZazZgTXD/fOvBN/SN5MVr7fbL/BcT +jWBxyPhUy1QvF6j9pA84LcsOA61MptVGslOw9l6oEzGWlYZMrZfhQEW4DX7LmfOc +F9SuZE9Usu1fVP//ljxwg5mEXtcdyeo3u57hIwot7Jbv/18R3Nx2o4u2WMbZA1u5 +H13Ow4FLsqgdCEz8BxCp3luqJalIiViEn3Fl6CqpSdveaNya+EHhwAqLdlRapGTO +1DcACljS/ToUzD9GmmzEfMF+j9Cg0QV928nkhpWwO2l3uQGNBGH6xLwBDAC03NfW +m0+JgBAGse/xeiMBf7zmtuE3fbe0nW/YqC2MWCUiC3QMfNFUAz1tktev5HNUw2A4 +0ON6DV8Lb5YqOOZqya+e2QR/Z50MF362895fYz2pske1oV8/D3t3lJk47Cb9s2TN +yD26yWp4vhessTutZmqPourEAddeicrJGoCPn6Dt/cyI0wW/vFwlTju7zhem/Lyx +vQSSBzKoKXFaG5xGlnT4WXLtNb85ePxrYLzcvAGYgmp3yF1EYeD3t9bdD/kmXu2P +5yBlZesYZJiF9Qw6Xvzvmcp8EsMURGCFLU4tk0k8Xs6gWyddtmhfhrj6OXmoVHZN +5pwIMzXoUtL765fnsqPiflIU521dTbk9Q/Kw9p6GnQ30Ebz1lkws9fefEkm2TdRN +ViJ/CwxgqquChXpYbo3fkeh5b/Z8pSgLXGJafRtuiD/keuc+Gg+2SpLHbvuBSzhp +cE/YUt7jYqvHC1la1gMWZbNuGePa2ICDDnonvo7vnprgQ3Z9+i2CwyZh2RUAEQEA +AYkBtgQYAQoAIBYhBHThJZApsUfLQDPouA1MnBQOihAwBQJh+sS8AhsMAAoJEA1M +nBQOihAwmpEL/RaECBsCa0yRcbldE972+w9kC7aEmlaS/k5P/v6b9QRHVKGO2CPO +ImdeeOwRWGxARU4LxjSBD3JjhK2YfKgBJqiIodeNDy7S06ORvTQfpQxpKZe66ySJ +FaUEE4rrb7F3IegnrkJ20mId10wn/exEFc/+H5UzzlXvbD29Ussq+3TXgtPHdrk9 +qwTYDMlJpq4hGJVSRBcBSHKMMaEwPr/9qb82bd0yhRPdxVA7d29J1fcI3joCjDQy +L5fboMLUPyzfrv1VlILQZHaxvC5oATU9HfuGBdbze840p7DSYuekUpXYBgUlaIWC +R56SxbtJhHPwj8B/pqJX1LKDUHHF8rv1BqlHLy/iTulJn9pNlvWYaM1iWM1FnncZ +k2NYwYspTmI+WsmagXtueszb5p4exlCKyheT2/z1fvrWinOmU8ylsI0OA9FGXVma +eiX/1DGByT7JKMWA6P1+v+YXmHBdyoAYAoUdhRJFZoVKTC06PeZT8tOwMXeDZCdW +XaOlJrPDM5E9zw== +=bIYD +-----END PGP PUBLIC KEY BLOCK----- + `, +} diff --git a/e2e/flow/suite_test.go b/e2e/flow/suite_test.go new file mode 100644 index 0000000..2c95943 --- /dev/null +++ b/e2e/flow/suite_test.go @@ -0,0 +1,71 @@ +package flow + +import ( + "os" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/prashantv/gostub" + "github.com/werf/trdl/server/pkg/testutil" +) + +func Test(t *testing.T) { + testutil.MeetsRequirementTools([]string{"git", "git-signatures", "gpg"}) + RegisterFailHandler(Fail) + RunSpecs(t, "Flow Suite") +} + +var SuiteData = struct { + TrxBinPath string + + TmpDir string + TestDir string + + Stubs *gostub.Stubs + + GPGKeys []string +}{} +var ( + _ = BeforeSuite(func() { + SuiteData.TrxBinPath = BuildTrxBin() + keys := map[string]string{ + "developer": "74E1259029B147CB4033E8B80D4C9C140E8A1030", + "tl": "2BA55FD8158034EEBE92AA9ED9D79B63AFC30C7A", + "pm": "C353F279F552B3EF16DAE0A64354E51BF178F735", + } + importGPGKeys(keys) + for _, v := range keys { + SuiteData.GPGKeys = append(SuiteData.GPGKeys, v) + } + }) + + _ = BeforeEach(func() { + SuiteData.Stubs = gostub.New() + SuiteData.TmpDir = testutil.GetTempDir() + + SuiteData.TestDir = filepath.Join(SuiteData.TmpDir, "trx-project") + Ω(os.Mkdir(SuiteData.TestDir, os.ModePerm)) + }) + + _ = AfterEach(func() { + err := os.RemoveAll(SuiteData.TmpDir) + Ω(err).ShouldNot(HaveOccurred()) + }) + + _ = AfterSuite(func() { + removeGPGKeys(SuiteData.GPGKeys) + }) +) + +func BuildTrxBin() string { + testutil.RunSucceedCommand( + "", + "task", + "build", + "-d", "../../", + ) + return "../../bin/trx" +} diff --git a/e2e/flow/utils.go b/e2e/flow/utils.go new file mode 100644 index 0000000..cdbada8 --- /dev/null +++ b/e2e/flow/utils.go @@ -0,0 +1,155 @@ +package flow + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "trx/internal/config" + + . "github.com/onsi/gomega" + "github.com/werf/trdl/server/pkg/testutil" + "gopkg.in/yaml.v3" +) + +func FixturePath(paths ...string) string { + absFixturesPath, err := filepath.Abs("_fixtures") + Ω(err).ShouldNot(HaveOccurred()) + pathsToJoin := append([]string{absFixturesPath}, paths...) + return filepath.Join(pathsToJoin...) +} + +func importGPGKeys(keys map[string]string) { + for user := range keys { + testutil.RunSucceedCommand( + testutil.FixturePath("pgp_keys"), + "gpg", + "--import", + fmt.Sprintf("%s_private.pgp", user), + ) + } +} + +func removeGPGKeys(keys []string) { + for _, keyId := range keys { + testutil.RunSucceedCommand( + testutil.FixturePath("pgp_keys"), + "gpg", + "--batch", "--yes", "--delete-secret-and-public-key", + keyId, + ) + } +} + +func initGitRepo(testDir, branchName string) { + testutil.CopyIn(testutil.FixturePath("complete_cycle"), testDir) + + testutil.RunSucceedCommand( + testDir, + "git", + "-c", "init.defaultBranch="+branchName, + "init", + ) + + testutil.RunSucceedCommand( + testDir, + "touch", "testfile", + ) + + writeRunnerConfigFile(testDir, &config.RunnerConfig{ + Tasks: []config.Task{ + { + Name: "dev-repo", + Env: map[string]string{"TRX_ENV": "dev"}, + Commands: []string{"echo 'hello world'", "echo $TRX_ENV"}, + InitialLastProcessedTag: "", + }, + { + Name: "test", + Env: map[string]string{"TRX_ENV": "test"}, + Commands: []string{"echo 'hello world'", "echo $TRX_ENV"}, + InitialLastProcessedTag: "", + }, + }, + }) + + testutil.RunSucceedCommand( + testDir, + "git", + "add", "-A", + ) + + testutil.RunSucceedCommand( + testDir, + "git", + "commit", "-m", "Initial commit", + ) +} + +func gitTag(testDir, tag, pgpSigningKeyDeveloper string) { + testutil.RunSucceedCommand( + testDir, + "git", + "-c", "tag.gpgsign=true", + "-c", "user.signingkey="+pgpSigningKeyDeveloper, + "tag", tag, "-m", "New version", + ) +} + +func quorumSignTag(testDir, pgpSigningKeyTL, pgpSigningKeyPM, tag string) { + if runtime.GOOS == "darwin" { + err := os.Setenv("GIT_EDITOR", `vim -c ":normal iNew version" -c ":wq"`) + Ω(err).ShouldNot(HaveOccurred()) + } + testutil.RunSucceedCommand( + testDir, + "git", + "signatures", "add", "--key", pgpSigningKeyTL, tag, + ) + + testutil.RunSucceedCommand( + testDir, + "git", + "signatures", "add", "--key", pgpSigningKeyPM, tag, + ) +} + +func writeRunnerConfigFile(path string, cfg *config.RunnerConfig) (string, error) { + data, err := yaml.Marshal(cfg) + if err != nil { + return "", fmt.Errorf("failed to marshal config: %w", err) + } + + filePath := filepath.Join(path, "trx.yaml") + err = os.WriteFile(filePath, data, 0644) + if err != nil { + return "", fmt.Errorf("failed to write file: %w", err) + } + + testutil.RunSucceedCommand( + "", + "cat", filePath, + ) + + return filePath, nil +} + +func writeConfigFile(path string, cfg *config.Config) (string, error) { + data, err := yaml.Marshal(cfg) + if err != nil { + return "", fmt.Errorf("failed to marshal config: %w", err) + } + + filePath := filepath.Join(path, "trx.yaml") + err = os.WriteFile(filePath, data, 0644) + if err != nil { + return "", fmt.Errorf("failed to write file: %w", err) + } + + testutil.RunSucceedCommand( + "", + "cat", filePath, + ) + + return filePath, nil +} diff --git a/go.mod b/go.mod index c7079da..ecfd9c5 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,18 @@ go 1.23.2 require ( github.com/go-playground/validator/v10 v10.24.0 github.com/hashicorp/go-hclog v1.6.3 + github.com/onsi/ginkgo/v2 v2.20.1 + github.com/onsi/gomega v1.36.0 + github.com/prashantv/gostub v1.1.0 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 + github.com/zeebo/assert v1.3.1 golang.org/x/sync v0.10.0 ) require ( dario.cat/mergo v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.5 // indirect github.com/armon/go-metrics v0.4.1 // indirect @@ -28,12 +33,16 @@ require ( github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -61,6 +70,7 @@ require ( github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.0.0 // indirect + github.com/otiai10/copy v1.9.0 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -69,12 +79,14 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.3.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/werf/logboek v0.6.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/term v0.28.0 // indirect + golang.org/x/tools v0.26.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/grpc v1.66.3 // indirect google.golang.org/protobuf v1.35.1 // indirect diff --git a/go.sum b/go.sum index 7c6e763..734133c 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= @@ -110,12 +110,10 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= -github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -198,8 +196,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= @@ -208,8 +204,6 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -219,12 +213,17 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= -github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/gomega v1.36.0 h1:Pb12RlruUtj4XUuPUqeEWc6j5DkVVVA49Uf6YLfC95Y= +github.com/onsi/gomega v1.36.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4= github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.4.0 h1:umwcf7gbpEwf7WFzqmWwSv0CzbeMsae2u9ZvpP8j2q4= +github.com/otiai10/mint v1.4.0/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -241,6 +240,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= @@ -307,20 +308,16 @@ github.com/werf/common-go v0.0.0-20250317135621-3a6772a9f88d h1:u+0+ivCKL6E/OLGS github.com/werf/common-go v0.0.0-20250317135621-3a6772a9f88d/go.mod h1:7pkHNfgZ2wvdwcMWCuDjdkY7iR3mIX5snYwbd1Iu7T4= github.com/werf/lockgate v0.1.1 h1:S400JFYjtWfE4i4LY9FA8zx0fMdfui9DPrBiTciCrx4= github.com/werf/lockgate v0.1.1/go.mod h1:0yIFSLq9ausy6ejNxF5uUBf/Ib6daMAfXuCaTMZJzIE= -github.com/werf/logboek v0.5.5 h1:RmtTejHJOyw0fub4pIfKsb7OTzD90ZOUyuBAXqYqJpU= -github.com/werf/logboek v0.5.5/go.mod h1:Gez5J4bxekyr6MxTmIJyId1F61rpO+0/V4vjCIEIZmk= github.com/werf/logboek v0.6.1 h1:oEe6FkmlKg0z0n80oZjLplj6sXcBeLleCkjfOOZEL2g= github.com/werf/logboek v0.6.1/go.mod h1:Gez5J4bxekyr6MxTmIJyId1F61rpO+0/V4vjCIEIZmk= -github.com/werf/trdl/server v0.0.0-20250121125358-ad0ef5178f9b h1:C4UDzx32nm688clMejq1h/k5Bo+xYeZFcKiajfK9Kj8= -github.com/werf/trdl/server v0.0.0-20250121125358-ad0ef5178f9b/go.mod h1:Kyj5iTcO6PnVpCikFDWoo1FPTQ4Cd1TQOhq+jCY1IwA= github.com/werf/trdl/server v0.0.0-20250314141720-5f76cb564636 h1:rbtK4PmXVHCH7N9kZHywj3OLCH2eO4aDwvHBFKVpATY= github.com/werf/trdl/server v0.0.0-20250314141720-5f76cb564636/go.mod h1:Kyj5iTcO6PnVpCikFDWoo1FPTQ4Cd1TQOhq+jCY1IwA= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A= +github.com/zeebo/assert v1.3.1/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= @@ -353,7 +350,6 @@ golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -372,8 +368,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= diff --git a/internal/command/hooks.go b/internal/command/hooks.go deleted file mode 100644 index 069ec7e..0000000 --- a/internal/command/hooks.go +++ /dev/null @@ -1,57 +0,0 @@ -package command - -import ( - "log" - - "trx/internal/config" -) - -func (e *Executor) RunOnCommandStartedHook(cfg *config.Config) error { - if cfg.Hooks != nil && cfg.Hooks.OnCommandStarted != nil { - log.Println("Running onStartedSuccess hook") - if err := e.Exec(*cfg.Hooks.OnCommandStarted); err != nil { - return err - } - } - return nil -} - -func (e *Executor) RunOnCommandSuccessHook(cfg *config.Config) error { - if cfg.Hooks != nil && cfg.Hooks.OnCommandSuccess != nil { - log.Println("Running onCommandSuccess hook") - if err := e.Exec(*cfg.Hooks.OnCommandSuccess); err != nil { - return err - } - } - return nil -} - -func (e *Executor) RunOnCommandFailureHook(cfg *config.Config) error { - if cfg.Hooks != nil && cfg.Hooks.OnCommandFailure != nil { - log.Println("Running onCommandFailure hook") - if err := e.Exec(*cfg.Hooks.OnCommandFailure); err != nil { - return err - } - } - return nil -} - -func (e *Executor) RunOnCommandSkippedHook(cfg *config.Config) error { - if cfg.Hooks != nil && cfg.Hooks.OnCommandSkipped != nil { - log.Println("Running onCommandSkipped hook") - if err := e.Exec(*cfg.Hooks.OnCommandSkipped); err != nil { - return err - } - } - return nil -} - -func (e *Executor) RunOnQuorumFailedHook(cfg *config.Config) error { - if cfg.Hooks != nil && cfg.Hooks.OnQuorumFailure != nil { - log.Println("Running onQuorumFailure hook") - if err := e.Exec(*cfg.Hooks.OnQuorumFailure); err != nil { - return err - } - } - return nil -} diff --git a/internal/config/config.go b/internal/config/config.go index 5f32b70..1be1f69 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,21 +3,22 @@ package config import ( "fmt" "os" + "path/filepath" "regexp" - "github.com/go-playground/validator/v10" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" ) type Config struct { - Repo GitRepo `mapstructure:"repo" validate:"required"` - Quorums []Quorum `mapstructure:"quorums" validate:"required,min=1"` - Env map[string]string `mapstructure:"env"` - - Hooks *Hooks `mapstructure:"hooks,omitempty"` - InitLastPublished string `mapstructure:"initial_last_published_git_commit"` - Commands []string `mapstructure:"commands"` + Repo *GitRepo `mapstructure:"repo"` + Quorums []Quorum `mapstructure:"quorums"` + Hooks Hooks `mapstructure:"hooks,omitempty"` + Tasks []Task `mapstructure:"tasks"` + + // Legacy + Env map[string]string `mapstructure:"env"` + Commands []string `mapstructure:"commands"` } type GitRepo struct { @@ -42,10 +43,12 @@ type Quorum struct { Name *string `mapstructure:"name,omitempty"` MinNumberOfKeys int `mapstructure:"minNumberOfKeys" validate:"required,gt=0"` GPGKeys []string `mapstructure:"gpgKeys"` - GPGKeyFilesPaths []string `mapstructure:"gpgKeyPaths"` + GPGKeyFilesPaths []string `mapstructure:"gpgKeyPaths" yaml:"gpgKeyPaths"` } type Hooks struct { + Env map[string]string `mapstructure:"env"` + OnCommandSuccess *[]string `mapstructure:"onCommandSuccess,omitempty"` OnCommandFailure *[]string `mapstructure:"onCommandFailure,omitempty"` OnCommandSkipped *[]string `mapstructure:"onCommandSkipped,omitempty"` @@ -53,6 +56,13 @@ type Hooks struct { OnCommandStarted *[]string `mapstructure:"onCommandStarted,omitempty"` } +type Task struct { + Name string `mapstructure:"name"` + Env map[string]string `mapstructure:"env"` + Commands []string `mapstructure:"commands"` + InitialLastProcessedTag string `mapstructure:"initialLastProcessedTag"` +} + func NewConfig(configPath string) (*Config, error) { config := &Config{} @@ -71,23 +81,22 @@ func _default() { } func (config *Config) Validate() error { - validate := validator.New() - if err := validate.Struct(config); err != nil { - return err - } - if err := validateGitRepoPath(config.Repo); err != nil { - return err + return fmt.Errorf("invalid git repo config: %w", err) + } + if err := validateTasks(config.Tasks); err != nil { + return fmt.Errorf("invalid tasks config: %w", err) } - if err := validateQuorums(config.Quorums); err != nil { - return err + return fmt.Errorf("invalid quorums config: %w", err) } - return nil } -func validateGitRepoPath(repo GitRepo) error { +func validateGitRepoPath(repo *GitRepo) error { + if repo == nil { + return fmt.Errorf("field 'repo' is required") + } sshGitRegex := regexp.MustCompile(`^git@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:[a-zA-Z0-9-_/]+\.git)$`) httpsGitRegex := regexp.MustCompile(`^https?://(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:/[^\s]*)?\.git$`) @@ -109,11 +118,22 @@ func validateGitRepoPath(repo GitRepo) error { } return nil default: - return fmt.Errorf("invalid Git repository URL: must be SSH (git@...) or HTTPS (https://...)") + if err := fileExists(repo.Url); err != nil { + return fmt.Errorf("invalid Git repository URL: must be SSH (git@...) or HTTPS (https://...) or local path") + } + gitDir := filepath.Join(repo.Url, ".git") + info, err := os.Stat(gitDir) + if err != nil || !info.IsDir() { + return fmt.Errorf("provided path is not a valid Git repository (missing .git directory)") + } + return nil } } func validateQuorums(quorums []Quorum) error { + if len(quorums) == 0 { + return fmt.Errorf("no quorums specified") + } for _, q := range quorums { if q.MinNumberOfKeys < 1 { return fmt.Errorf("quorum size needs to be greater or equal 1") @@ -142,8 +162,21 @@ func validateKeyFilePath(path []string) error { return nil } +func validateTasks(tasks []Task) error { + for _, t := range tasks { + if len(t.Commands) == 0 { + return fmt.Errorf("no command specified for task %s", t.Name) + } + } + return nil +} + func fileExists(path string) error { - _, err := os.Stat(path) + absPath, err := filepath.Abs(path) + if err != nil { + return fmt.Errorf("error getting absolute path: %w", err) + } + _, err = os.Stat(absPath) if err != nil { return fmt.Errorf("error stat key file path: %w", err) } diff --git a/internal/config/runner.go b/internal/config/runner.go index fc7c818..0a27ef2 100644 --- a/internal/config/runner.go +++ b/internal/config/runner.go @@ -9,6 +9,9 @@ import ( ) type RunnerConfig struct { + Tasks []Task `mapstructure:"tasks"` + + // Legacy Commands []string `mapstructure:"commands"` Env map[string]string `mapstructure:"env"` } @@ -40,8 +43,8 @@ func (config *RunnerConfig) Validate() error { return err } - if len(config.Commands) == 0 { - return fmt.Errorf("runner config error: no commands to run") + if err := validateTasks(config.Tasks); err != nil { + return fmt.Errorf("invalid tasks config: %w", err) } return nil diff --git a/internal/command/command.go b/internal/executor/executor.go similarity index 71% rename from internal/command/command.go rename to internal/executor/executor.go index 891dd1f..fe98227 100644 --- a/internal/command/command.go +++ b/internal/executor/executor.go @@ -1,4 +1,4 @@ -package command +package executor import ( "bufio" @@ -13,8 +13,6 @@ import ( "strings" ) -var WorkDir = "" - type Vars struct { RepoUrl string RepoTag string @@ -23,45 +21,49 @@ type Vars struct { type Executor struct { Ctx context.Context WorkDir string - Env []string - Vars map[string]string } -func NewExecutor(ctx context.Context, e, vars map[string]string) (*Executor, error) { - wd := WorkDir - if wd == "" { - wd, _ = os.Getwd() - } - var envs []string - for k, v := range e { - envs = append(envs, fmt.Sprintf("%s=%s", strings.ToUpper(k), v)) +func NewExecutor(ctx context.Context, workDir string) (*Executor, error) { + if workDir == "" { + workDir, _ = os.Getwd() } return &Executor{ Ctx: ctx, - WorkDir: wd, - Env: envs, - Vars: vars, + WorkDir: workDir, }, nil } -func (e *Executor) Exec(commands []string) error { - cmds, err := resolve(commands, e.Vars) +func (e *Executor) Exec(commands []string, env, templateVars map[string]string) error { + opts, err := prepareExecOpts(e.WorkDir, commands, env, templateVars) if err != nil { - return fmt.Errorf("can't resolve commands: %w", err) + return fmt.Errorf("can't prepare exec opts: %w", err) + } + if err := execute(e.Ctx, opts); err != nil { + return fmt.Errorf("executor error: %w", err) } - envs, err := resolve(e.Env, e.Vars) + return nil +} + +func prepareExecOpts(wd string, commands []string, env, templateVars map[string]string) (*excuteOpts, error) { + var envs []string + for k, v := range env { + envs = append(envs, fmt.Sprintf("%s=%s", strings.ToUpper(k), v)) + } + cmds, err := resolve(commands, templateVars) if err != nil { - return fmt.Errorf("can't resolve envs: %w", err) + return nil, fmt.Errorf("can't resolve commands: %w", err) + } + envs, err = resolve(envs, templateVars) + if err != nil { + return nil, fmt.Errorf("can't resolve envs: %w", err) } script := "set -e\n" + strings.Join(cmds, "\n") - if err := execute(e.Ctx, &excuteOpts{ + + return &excuteOpts{ cmd: script, env: envs, - wd: e.WorkDir, - }); err != nil { - return fmt.Errorf("executor error: %w", err) - } - return nil + wd: wd, + }, nil } func resolve(commands []string, vars map[string]string) ([]string, error) { @@ -131,7 +133,7 @@ func execute(ctx context.Context, opts *excuteOpts) error { if err := cmd.Wait(); err != nil { if stderr.Len() > 0 { - log.Println("executing error:", stderr.String()) + log.Println(stderr.String()) } return fmt.Errorf("error executing command: %w", err) } diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go new file mode 100644 index 0000000..cc1639e --- /dev/null +++ b/internal/executor/executor_test.go @@ -0,0 +1,69 @@ +package executor + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPrepareExecOpts(t *testing.T) { + tests := []struct { + name string + wd string + commands []string + env map[string]string + templateVars map[string]string + wantErr bool + wantCmd string + wantEnv []string + }{ + { + name: "basic command with env", + wd: "/app", + commands: []string{"echo Hello, World!"}, + env: map[string]string{"env": "test"}, + templateVars: map[string]string{}, + wantErr: false, + wantCmd: "set -e\necho Hello, World!", + wantEnv: []string{"ENV=test"}, + }, + { + name: "command with template vars", + wd: "/app", + commands: []string{"echo {{ .RepoTag }}"}, + env: map[string]string{"env": "{{ .RepoUrl }}"}, + templateVars: map[string]string{ + "RepoTag": "v1.0.0", + "RepoUrl": "https://example.com/repo.git", + "RepoCommit": "abcdefg", + }, + wantErr: false, + wantCmd: "set -e\necho v1.0.0", + wantEnv: []string{"ENV=https://example.com/repo.git"}, + }, + { + name: "unresolved template", + wd: "/app", + commands: []string{"echo {{ .UnknownVar }}"}, + env: map[string]string{}, + templateVars: map[string]string{}, + wantErr: false, + wantCmd: "set -e\necho ", + wantEnv: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := prepareExecOpts(tt.wd, tt.commands, tt.env, tt.templateVars) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantCmd, got.cmd) + assert.Equal(t, tt.wantEnv, got.env) + assert.Equal(t, tt.wd, got.wd) + } + }) + } +} diff --git a/internal/executor/mock.go b/internal/executor/mock.go new file mode 100644 index 0000000..85edb35 --- /dev/null +++ b/internal/executor/mock.go @@ -0,0 +1,37 @@ +package executor + +import ( + "context" + "fmt" +) + +type MockExecutor struct { + Ctx context.Context + WorkDir string +} + +func NewMockExecutor(ctx context.Context, workDir string) (*MockExecutor, error) { + if workDir == "" { + workDir = "/mock/work/dir" + } + return &MockExecutor{ + Ctx: ctx, + WorkDir: workDir, + }, nil +} + +func (e *MockExecutor) Exec(commands []string, env, templateVars map[string]string) error { + opts, err := prepareExecOpts(e.WorkDir, commands, env, templateVars) + if err != nil { + return fmt.Errorf("can't prepare exec opts: %w", err) + } + if err := executeMock(e.Ctx, opts); err != nil { + return fmt.Errorf("executor error: %w", err) + } + return nil +} + +func executeMock(_ context.Context, opts *excuteOpts) error { + fmt.Printf("Mock executing commands: %s\n", opts.cmd) + return nil +} diff --git a/internal/git/client.go b/internal/git/client.go index cc9d6fd..2db5964 100644 --- a/internal/git/client.go +++ b/internal/git/client.go @@ -5,8 +5,6 @@ import ( "fmt" "log" "os" - "os/user" - "path/filepath" "sort" "github.com/Masterminds/semver/v3" @@ -15,12 +13,12 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" - "trx/internal/command" "trx/internal/config" ) type GitClient struct { - Repo *git.Repository + Repo *git.Repository + RepoPath string } func NewGitClient(cfg config.GitRepo) (*GitClient, error) { @@ -35,16 +33,17 @@ func NewGitClient(cfg config.GitRepo) (*GitClient, error) { } return &GitClient{ - Repo: repo, + Repo: repo, + RepoPath: repoConf.RepoPath, }, nil } -func (g *GitClient) GetTargetGitObject() (*TargetGitObject, error) { - tag, commit, err := g.GetLastSemverTag() +func (g *GitClient) GetTargetGitObject(t string) (*TargetGitObject, error) { + to, err := getTagInfo(g, t) if err != nil { - return nil, err + return nil, fmt.Errorf("get tag info error: %w", err) } - to := &TargetGitObject{Tag: tag, Commit: commit} + err = g.Checkout(to) if err != nil { return nil, fmt.Errorf("checkout error: %w", err) @@ -52,6 +51,13 @@ func (g *GitClient) GetTargetGitObject() (*TargetGitObject, error) { return to, nil } +func getTagInfo(g *GitClient, tag string) (*TargetGitObject, error) { + if tag != "" { + return g.GetSpecificTag(tag) + } + return g.GetLastSemverTag() +} + type TargetGitObject struct { Tag string Commit string @@ -87,10 +93,10 @@ func (g *GitClient) Checkout(o *TargetGitObject) error { return nil } -func (g *GitClient) GetLastSemverTag() (string, string, error) { +func (g *GitClient) GetLastSemverTag() (*TargetGitObject, error) { tagRefs, err := g.Repo.Tags() if err != nil { - return "", "", err + return nil, err } var versions []*semver.Version @@ -106,11 +112,11 @@ func (g *GitClient) GetLastSemverTag() (string, string, error) { return nil }) if err != nil { - return "", "", err + return nil, err } if len(versions) == 0 { - return "", "", fmt.Errorf("no semantic version tags found") + return nil, fmt.Errorf("no semantic version tags found") } sort.Sort(sort.Reverse(semver.Collection(versions))) @@ -119,44 +125,47 @@ func (g *GitClient) GetLastSemverTag() (string, string, error) { ref, err := g.Repo.Reference(refName, true) if err != nil { - return "", "", err + return nil, err } - hash := ref.Hash() - - return lastTag, hash.String(), nil + return &TargetGitObject{ + Tag: lastTag, + Commit: ref.Hash().String(), + }, nil } -func openGitRepo(r *RepoConfig) (*git.Repository, error) { - usr, err := user.Current() +func (g *GitClient) GetSpecificTag(tag string) (*TargetGitObject, error) { + ref, err := g.Repo.Tag(tag) if err != nil { - return nil, err + return nil, fmt.Errorf("tag %q not found: %w", tag, err) } + return &TargetGitObject{ + Tag: tag, + Commit: ref.Hash().String(), + }, nil +} - repoName := RepoNameFromUrl(r.Url) - repoPath := filepath.Join(usr.HomeDir, ".trx", repoName) - +func openGitRepo(r *RepoConfig) (*git.Repository, error) { var repo *git.Repository - if _, err := os.Stat(repoPath); os.IsNotExist(err) { + if _, err := os.Stat(r.RepoPath); os.IsNotExist(err) { cloneOptions := &git.CloneOptions{URL: r.Url} if r.Auth != nil { cloneOptions.Auth = r.Auth.AuthMethod } - log.Printf("Cloning %s into %s\n", r.Url, repoPath) - repo, err = git.PlainClone(repoPath, false, cloneOptions) + log.Printf("Cloning %s into %s\n", r.Url, r.RepoPath) + repo, err = git.PlainClone(r.RepoPath, false, cloneOptions) if err != nil { return nil, fmt.Errorf("unable to clone repo: %w", err) } log.Println("Cloning done") } else { - repo, err = git.PlainOpen(repoPath) + repo, err = git.PlainOpen(r.RepoPath) if err != nil { return nil, fmt.Errorf("unable to open repo: %w", err) } } - command.WorkDir = repoPath log.Println("Fetching tags") fetchOptions := &git.FetchOptions{ RefSpecs: []gitconfig.RefSpec{ @@ -166,7 +175,7 @@ func openGitRepo(r *RepoConfig) (*git.Repository, error) { if r.Auth != nil { fetchOptions.Auth = r.Auth.AuthMethod } - err = repo.Fetch(fetchOptions) + err := repo.Fetch(fetchOptions) if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { return nil, fmt.Errorf("unable to fetch tags: %w", err) } diff --git a/internal/git/config.go b/internal/git/config.go index 0f43a0a..00dce00 100644 --- a/internal/git/config.go +++ b/internal/git/config.go @@ -3,6 +3,8 @@ package git import ( "fmt" "os" + "os/user" + "path/filepath" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" @@ -12,8 +14,9 @@ import ( ) type RepoConfig struct { - Url string - Auth *Auth + Url string + Auth *Auth + RepoPath string } type Auth struct { @@ -25,6 +28,11 @@ func NewRepoConfig(config config.GitRepo) (*RepoConfig, error) { return nil, fmt.Errorf("git url not specified") } + usr, err := user.Current() + if err != nil { + return nil, err + } + if config.Auth.BasicAuth != nil { auth, err := newBasicAuth(config.Auth.BasicAuth.Username, config.Auth.BasicAuth.Password) if err != nil { @@ -41,8 +49,9 @@ func NewRepoConfig(config config.GitRepo) (*RepoConfig, error) { return nil, err } return &RepoConfig{ - Url: config.Url, - Auth: auth, + Url: config.Url, + Auth: auth, + RepoPath: filepath.Join(usr.HomeDir, ".trx", RepoNameFromUrl(config.Url)), }, nil } diff --git a/internal/git/git.go b/internal/git/git.go index 846020e..10c8197 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -3,7 +3,7 @@ package git import ( "fmt" "log" - "path" + "net/url" "strings" "github.com/Masterminds/semver/v3" @@ -27,8 +27,32 @@ func VerifyTagSignatures(repo *git.Repository, r VerifyTagSignaturesRequest) err return nil } -func RepoNameFromUrl(url string) string { - return strings.TrimSuffix(path.Base(url), ".git") +func RepoNameFromUrl(repoUrl string) string { + repoUrl = strings.TrimSuffix(repoUrl, ".git") + + // SSH case: git@github.com:user/repo + if strings.HasPrefix(repoUrl, "git@") { + // git@github.com:user/repo → github.com/user/repo + parts := strings.SplitN(repoUrl, ":", 2) + host := strings.TrimPrefix(parts[0], "git@") + path := parts[1] + return toDottedName(host + "/" + path) + } + + // HTTPS case: https://github.com/user/repo + if u, err := url.Parse(repoUrl); err == nil { + return toDottedName(u.Host + u.Path) + } + + // fallback + return toDottedName(repoUrl) +} + +func toDottedName(s string) string { + s = strings.TrimPrefix(s, "/") + s = strings.TrimSuffix(s, "/") + parts := strings.Split(s, "/") + return strings.Join(parts, ".") } func IsNewerVersion(current, last, initial string) (bool, error) { diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go new file mode 100644 index 0000000..31acd2d --- /dev/null +++ b/internal/hooks/hooks.go @@ -0,0 +1,115 @@ +package hooks + +import ( + "context" + "fmt" + "log" + + "trx/internal/config" + "trx/internal/executor" + "trx/internal/templates" +) + +type Executor interface { + Exec(commands []string, env, templateVars map[string]string) error +} + +type HookExecutor struct { + hooks config.Hooks + executor Executor + env map[string]string + templateVars map[string]string +} + +type HookExecutorOptions struct { + TemplateVars map[string]string + WorkDir string +} + +func NewHookExecutor(ctx context.Context, cfg *config.Config, opts HookExecutorOptions) (*HookExecutor, error) { + env := getEnv(cfg) + hooks := cfg.Hooks + e, err := executor.NewExecutor(ctx, opts.WorkDir) + if err != nil { + return nil, fmt.Errorf("failed to create executor: %v", err) + } + return &HookExecutor{ + hooks: hooks, + executor: e, + env: env, + templateVars: opts.TemplateVars, + }, nil +} + +func (e *HookExecutor) RunOnCommandStartedHook(taskname string) error { + e.templateVars[templates.StartedTaskName] = taskname + if e.hooks.OnCommandStarted != nil { + log.Println("Running onStartedSuccess hook") + if err := e.executor.Exec(*e.hooks.OnCommandStarted, e.env, e.templateVars); err != nil { + log.Printf("WARNING onCommandStarted hook execution error: %s\n", err.Error()) + return err + } + } + return nil +} + +func (e *HookExecutor) RunOnCommandSuccessHook() error { + if e.hooks.OnCommandSuccess != nil { + log.Println("Running onCommandSuccess hook") + if err := e.executor.Exec(*e.hooks.OnCommandSuccess, e.env, e.templateVars); err != nil { + log.Printf("WARNING onCommandSuccess hook execution error: %s\n", err.Error()) + return err + } + } + return nil +} + +func (e *HookExecutor) RunOnCommandFailureHook(taskname string) error { + e.templateVars[templates.FailedTaskName] = taskname + if e.hooks.OnCommandFailure != nil { + log.Println("Running onCommandFailure hook") + if err := e.executor.Exec(*e.hooks.OnCommandFailure, e.env, e.templateVars); err != nil { + log.Printf("WARNING onCommandFailure hook execution error: %s\n", err.Error()) + return err + } + } + return nil +} + +func (e *HookExecutor) RunOnCommandSkippedHook() error { + if e.hooks.OnCommandSkipped != nil { + log.Println("Running onCommandSkipped hook") + if err := e.executor.Exec(*e.hooks.OnCommandSkipped, e.env, e.templateVars); err != nil { + log.Printf("WARNING onCommandFailure hook execution error: %s", err.Error()) + return err + } + } + return nil +} + +func (e *HookExecutor) RunOnQuorumFailedHook(quorumName string) error { + e.templateVars[templates.FailedQuorumName] = quorumName + if e.hooks.OnQuorumFailure != nil { + log.Println("Running onQuorumFailure hook") + if err := e.executor.Exec(*e.hooks.OnQuorumFailure, e.env, e.templateVars); err != nil { + log.Printf("WARNING onCommandSkipped hook execution error: %s\n", err.Error()) + return err + } + } + return nil +} + +func getEnv(cfg *config.Config) map[string]string { + var hooksEnvs map[string]string + if cfg.Env != nil { + throwDeprWarning() + hooksEnvs = cfg.Env + } else { + hooksEnvs = cfg.Hooks.Env + } + return hooksEnvs +} + +func throwDeprWarning() { + log.Println("WARNING! You're using deprecated 'env' field in config. Please use 'tasks.task.env' and 'hooks.env' instead.") +} diff --git a/internal/hooks/hooks_test.go b/internal/hooks/hooks_test.go new file mode 100644 index 0000000..7b60c7c --- /dev/null +++ b/internal/hooks/hooks_test.go @@ -0,0 +1,33 @@ +package hooks + +import ( + "context" + "testing" + "trx/internal/config" + "trx/internal/templates" + + "github.com/stretchr/testify/assert" +) + +func TestRunHooks(t *testing.T) { + repoTemplatevars := templates.GetRepoTemplateVars(templates.RepoTemplateVarsData{ + RepoTag: "v1.0.0", + RepoUrl: "https://github.com/stretchr/testify/assert", + RepoCommit: "1234567890abcdef", + }) + hookExecutor, err := NewHookMockExecutor(context.Background(), &config.Config{ + Hooks: config.Hooks{ + + Env: map[string]string{"ENV": "test"}, + OnCommandStarted: &[]string{"commit: {{ .RepoCommit }}, tag: {{ .RepoTag }}, url: {{ .RepoUrl }}"}, + OnCommandFailure: &[]string{`T_FAILED -- {{ .FailedTaskName }}`}, + }, + }, HookExecutorOptions{ + TemplateVars: repoTemplatevars, + WorkDir: "/tmp", + }) + assert.NoError(t, err) + + err = hookExecutor.RunOnCommandStartedHook("name") + assert.NoError(t, err) +} diff --git a/internal/hooks/mock.go b/internal/hooks/mock.go new file mode 100644 index 0000000..93a62f9 --- /dev/null +++ b/internal/hooks/mock.go @@ -0,0 +1,23 @@ +package hooks + +import ( + "context" + "fmt" + "trx/internal/config" + "trx/internal/executor" +) + +func NewHookMockExecutor(ctx context.Context, cfg *config.Config, opts HookExecutorOptions) (*HookExecutor, error) { + env := getEnv(cfg) + hooks := cfg.Hooks + e, err := executor.NewMockExecutor(ctx, opts.WorkDir) + if err != nil { + return nil, fmt.Errorf("failed to create executor: %v", err) + } + return &HookExecutor{ + hooks: hooks, + executor: e, + env: env, + templateVars: opts.TemplateVars, + }, nil +} diff --git a/internal/quorum/quorum.go b/internal/quorum/quorum.go index 2122e20..c68beb6 100644 --- a/internal/quorum/quorum.go +++ b/internal/quorum/quorum.go @@ -1,6 +1,7 @@ package quorum import ( + "errors" "fmt" "log" "os" @@ -25,7 +26,33 @@ func (e *Error) Unwrap() error { return e.Err } -func CheckQuorums(quorums []config.Quorum, repo *git.Repository, tag string) error { +type HookExecutor interface { + RunOnQuorumFailedHook(quorumName string) error +} + +type CheckQuorumsRequest struct { + Quorums []config.Quorum + Repo *git.Repository + Tag string + HookExecutor HookExecutor +} + +func CheckQuorums(r *CheckQuorumsRequest) error { + if err := checkQuorums(r.Quorums, r.Repo, r.Tag); err != nil { + var qErr *Error + if errors.As(err, &qErr) { + if r.HookExecutor != nil { + r.HookExecutor.RunOnQuorumFailedHook(qErr.QuorumName) + } + return fmt.Errorf("quorum error: %w", qErr.Err) + } else { + return fmt.Errorf("quorum error: %w", err) + } + } + return nil +} + +func checkQuorums(quorums []config.Quorum, repo *git.Repository, tag string) error { var g errgroup.Group for _, q := range quorums { g.Go(func() error { diff --git a/internal/storage/local/local.go b/internal/storage/local/local.go index 4424ac0..4efa113 100644 --- a/internal/storage/local/local.go +++ b/internal/storage/local/local.go @@ -29,8 +29,12 @@ func NewLocalStorage(repoUrl string) *Local { } } -func (s *Local) CheckLastSucceedTag() (string, error) { - filePath := filepath.Join(s.path, fileLastProcessedCommit) +func (s *Local) CheckTaskLastSucceedTag(taskName string) (string, error) { + if taskName == "" { + return "", fmt.Errorf("task name can't be empty") + } + path := filepath.Join(s.path, taskName) + filePath := filepath.Join(path, fileLastProcessedCommit) data, err := os.ReadFile(filePath) if err != nil { @@ -48,16 +52,19 @@ func (s *Local) CheckLastSucceedTag() (string, error) { return commit, nil } -func (s *Local) StoreSucceedTag(commit string) error { +func (s *Local) StoreTaskSucceedTag(taskName, commit string) error { + if taskName == "" { + return fmt.Errorf("task name can't be empty") + } if commit == "" { return fmt.Errorf("tag can't be empty") } - - if err := os.MkdirAll(s.path, 0o755); err != nil { + path := filepath.Join(s.path, taskName) + if err := os.MkdirAll(path, 0o755); err != nil { return err } - filePath := filepath.Join(s.path, fileLastProcessedCommit) + filePath := filepath.Join(path, fileLastProcessedCommit) return os.WriteFile(filePath, []byte(commit+"\n"), 0o644) } diff --git a/internal/storage/storage.go b/internal/storage/storage.go index beb3bb1..2c89886 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -6,8 +6,8 @@ import ( ) type Storage interface { - CheckLastSucceedTag() (string, error) - StoreSucceedTag(commit string) error + CheckTaskLastSucceedTag(taskName string) (string, error) + StoreTaskSucceedTag(taskName, commit string) error } type StorageService struct { @@ -28,10 +28,10 @@ func NewStorage(opts *StorageOpts) (*StorageService, error) { } } -func (s *StorageService) CheckLastSucceedTag() (string, error) { - return s.storage.CheckLastSucceedTag() +func (s *StorageService) CheckTaskLastSucceedTag(taskName string) (string, error) { + return s.storage.CheckTaskLastSucceedTag(taskName) } -func (s *StorageService) StoreSucceedTag(commit string) error { - return s.storage.StoreSucceedTag(commit) +func (s *StorageService) StoreTaskSucceedTag(taskName, commit string) error { + return s.storage.StoreTaskSucceedTag(taskName, commit) } diff --git a/internal/tasks/tasks.go b/internal/tasks/tasks.go new file mode 100644 index 0000000..c04089e --- /dev/null +++ b/internal/tasks/tasks.go @@ -0,0 +1,270 @@ +package tasks + +import ( + "context" + "crypto/sha1" + "errors" + "fmt" + "log" + "strings" + "time" + + "trx/internal/config" + "trx/internal/executor" + "trx/internal/git" +) + +type Executor interface { + Exec(commands []string, env, templateVars map[string]string) error +} + +type Storage interface { + StoreTaskSucceedTag(taskName, commit string) error + CheckTaskLastSucceedTag(taskName string) (string, error) +} + +type TaskExecutor struct { + executor Executor + templateVars map[string]string + storage Storage +} + +type TaskExecutorForced struct { + executor Executor + templateVars map[string]string +} + +type RunOptions struct { + CmdFromCli []string +} + +type Task struct { + Name string + Env map[string]string + Commands []string + Version string + InitialVersion string +} + +type TaskExecutorOptions struct { + Storage Storage + TemplateVars map[string]string + WorkDir string +} + +type Error struct { + TaskName string + Err error + ErrMessage string +} + +var ( + ErrNoNewVersion = errors.New("no new version") + ErrExcutionFailed = errors.New("error running task") +) + +func (e *Error) Error() string { + return fmt.Sprintf("task `%s` error: %v", e.TaskName, e.Err) +} + +func (e *Error) Unwrap() error { + return e.Err +} + +func NewTaskExecutor(ctx context.Context, opts TaskExecutorOptions) (*TaskExecutor, error) { + e, err := executor.NewExecutor(ctx, opts.WorkDir) + if err != nil { + return nil, fmt.Errorf("can't create executor: %w", err) + } + if opts.Storage == nil { + return nil, fmt.Errorf("can't create executor: storage is required") + } + return &TaskExecutor{ + executor: e, + templateVars: opts.TemplateVars, + storage: opts.Storage, + }, nil +} + +func NewTaskForceExecutor(ctx context.Context, opts TaskExecutorOptions) (*TaskExecutorForced, error) { + e, err := executor.NewExecutor(ctx, opts.WorkDir) + if err != nil { + return nil, fmt.Errorf("can't create executor: %w", err) + } + return &TaskExecutorForced{ + executor: e, + templateVars: opts.TemplateVars, + }, nil +} + +func (e *TaskExecutorForced) RunTasks(tasks []Task) error { + for _, t := range tasks { + t.Name = fmt.Sprintf("%s (forced)", t.Name) + if err := run(e.executor, t, e.templateVars); err != nil { + return err + } + } + return nil +} + +func (e *TaskExecutor) RunTasks(tasks []Task) error { + for _, t := range tasks { + + if err := t.checkIfNewVersion(e.storage); err != nil { + return &Error{ + TaskName: t.Name, + Err: ErrNoNewVersion, + } + } + if err := run(e.executor, t, e.templateVars); err != nil { + return err + } + if err := e.storage.StoreTaskSucceedTag(t.Name, t.Version); err != nil { + return fmt.Errorf("store last successed tag error for task %s: %w", t.Name, err) + } + } + return nil +} + +func run(e Executor, t Task, vars map[string]string) error { + log.Printf("--- Running task %s", t.Name) + start := time.Now() + if err := e.Exec(t.Commands, t.Env, vars); err != nil { + return &Error{ + TaskName: t.Name, + Err: ErrExcutionFailed, + ErrMessage: err.Error(), + } + } + elapsed := time.Since(start) + log.Printf("--- Task %s completed in %s", t.Name, formatDuration(elapsed)) + return nil +} + +func formatDuration(d time.Duration) string { + totalMillis := d.Milliseconds() + minutes := totalMillis / 60000 + seconds := (totalMillis % 60000) / 1000 + millis := totalMillis % 1000 + return fmt.Sprintf("%02dm:%02ds:%03dms", minutes, seconds, millis) +} + +func (t *Task) checkIfNewVersion(storage Storage) error { + lastSucceedTag, err := storage.CheckTaskLastSucceedTag(t.Name) + if err != nil { + return fmt.Errorf("check last published commit error: %w", err) + } + isNewVersion, err := git.IsNewerVersion(t.Version, lastSucceedTag, t.InitialVersion) + if err != nil { + return fmt.Errorf("can't check if tag is new: %w", err) + } + if !isNewVersion { + return fmt.Errorf("no new version") + } + return nil +} + +type GetTasksToRunOpts struct { + CmdFromCli []string + Forced bool + TargetTaskName string + Version string +} + +func GetTasksToRun(cfg *config.Config, wd string, opts GetTasksToRunOpts) ([]Task, error) { + if len(cfg.Commands) > 0 { + throwDeprWarning() + return []Task{ + { + Name: "legacy-commands-main", + Commands: cfg.Commands, + Env: cfg.Env, + Version: opts.Version, + InitialVersion: cfg.Repo.InitialLastProcessedTag, + }, + }, nil + } + if len(opts.CmdFromCli) > 0 { + cmd := []string{strings.Join(opts.CmdFromCli, " ")} + return []Task{ + { + Name: GetHashOfCommands(cmd), + Commands: cmd, + Version: opts.Version, + }, + }, nil + } + + if len(cfg.Tasks) > 0 { + return getTaskToRun(cfg.Tasks, opts.TargetTaskName, opts.Version) + } + + runCfg, err := config.NewRunnerConfig(wd, cfg.Repo.ConfigFile) + if err != nil { + return nil, fmt.Errorf("runner config error: %w", err) + } + + if len(runCfg.Commands) > 0 { + throwDeprWarning() + return []Task{ + { + Name: "legacy-commands", + Commands: runCfg.Commands, + Env: runCfg.Env, + Version: opts.Version, + InitialVersion: cfg.Repo.InitialLastProcessedTag, + }, + }, nil + } + return getTaskToRun(runCfg.Tasks, opts.TargetTaskName, opts.Version) +} + +func throwDeprWarning() { + deprWarning := "WARNING! You're using deprecated 'commands' field in config. Please use 'tasks' instead." + log.Println(deprWarning) +} + +func GetHashOfCommands(commands []string) string { + joined := strings.Join(commands, "") + sum := sha1.Sum([]byte(joined)) + return fmt.Sprintf("%x", sum) +} + +func getTaskToRun(c []config.Task, name, version string) ([]Task, error) { + if len(c) == 0 { + return nil, fmt.Errorf("no tasks found") + } + if name == "" { + return []Task{ + { + Name: func() string { + if c[0].Name != "" { + return c[0].Name + } + return "1" + }(), + Commands: c[0].Commands, + Env: c[0].Env, + Version: version, + InitialVersion: c[0].InitialLastProcessedTag, + }, + }, nil + } + + for _, t := range c { + if t.Name == name { + return []Task{ + { + Name: t.Name, + Commands: t.Commands, + Env: t.Env, + Version: version, + InitialVersion: t.InitialLastProcessedTag, + }, + }, nil + + } + + } + return nil, fmt.Errorf("task `%s` not found", name) +} diff --git a/internal/templates/templates.go b/internal/templates/templates.go new file mode 100644 index 0000000..6b48211 --- /dev/null +++ b/internal/templates/templates.go @@ -0,0 +1,26 @@ +package templates + +const ( + RepoTag = "RepoTag" + RepoUrl = "RepoUrl" + RepoCommit = "RepoCommit" + + FailedTaskName = "FailedTaskName" + FailedQuorumName = "FailedQuorumName" + + StartedTaskName = "StartedTaskName" +) + +type RepoTemplateVarsData struct { + RepoTag string + RepoUrl string + RepoCommit string +} + +func GetRepoTemplateVars(data RepoTemplateVarsData) map[string]string { + vars := make(map[string]string) + vars[RepoTag] = data.RepoTag + vars[RepoUrl] = data.RepoUrl + vars[RepoCommit] = data.RepoCommit + return vars +} diff --git a/trx.yaml b/trx.yaml index fcd9280..b4498fa 100644 --- a/trx.yaml +++ b/trx.yaml @@ -1,3 +1,12 @@ commands: - echo "$TEST" | base64 - echo "{{ .RepoTag }} {{ .RepoCommit }} {{ .RepoUrl }}" + +tasks: + - name: test + commands: + - echo "$TEST" | base64 + - echo "{{ .RepoTag }} {{ .RepoCommit }} {{ .RepoUrl }}" + - name: test2 + commands: + - ls -l /xz