Skip to content

Commit

Permalink
Add sub-commands for specific promotion paths
Browse files Browse the repository at this point in the history
These new sub-commands simplify the interface when promoting between
branches in a repository, between environments in a repository, or
between two "simple" repositories. The previous behavior is preserved if
no sub-command is given: the user can specify any combination of
repository, branch and environment folder (even a local repo).

For #92
  • Loading branch information
Justin Kulikauskas authored and mnuttall committed Jun 22, 2020
1 parent 5ff623d commit 138702f
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 151 deletions.
44 changes: 32 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,26 +68,34 @@ promote from one environment to another

Usage:
services promote [flags]
services promote [command]

Available Commands:
branch promote between branches within one repository
env promote between environment folders within one repository
repo promote between repositories

Flags:
--branch-name string the name of the branch on the destination repository for the pull request (auto-generated if empty)
--branch-name string the branch on the destination repository for the pull request (auto-generated if empty)
--cache-dir string where to cache Git checkouts (default "~/.promotion/cache")
--from string the source Git repository (URL or local)
--from-branch string the branch on the source Git repository (default "master")
--from-env-folder string env folder on the source Git repository (if not provided, the repository should only have one folder under environments/)
-h, --help help for promote
--keep-cache whether to retain the locally cloned repositories in the cache directory
--service string the name of the service to promote
--to string the destination Git repository
--to-branch string the branch on the destination Git repository (default "master")
--to-env-folder string env folder on the destination Git repository (if not provided, the repository should only have one folder under environments/)

Global Flags:
--commit-email string the email to use for commits when creating branches
--commit-message string the msg to use on the resultant commit and pull request
--commit-message string the message to use on the resultant commit and pull request
--commit-name string the name to use for commits when creating branches
--debug additional debug logging output
--from string source Git repository
--from-branch string branch on the source Git repository (default "master")
-h, --help help for promote
--github-token string oauth access token to authenticate the request
--insecure-skip-verify Insecure skip verify TLS certificate
--keep-cache whether to retain the locally cloned repositories in the cache directory
--repository-type string the type of repository: github, gitlab or ghe (default "github")
--service string service name to promote
--to string destination Git repository
--to-branch string branch on the destination Git repository (default "master")

Global Flags:
--github-token string oauth access token to authenticate the request
```
This will _copy_ all files under `/services/service-a/base/config/*` in `first-environment` to `second-environment`, commit and push, and open a PR for the change. Any of these arguments may be provided as environment variables, using all upper case and replacing `-` with `_`. Hence you can set CACHE_DIR, COMMIT_EMAIL, etc.
Expand All @@ -99,15 +107,27 @@ This will _copy_ all files under `/services/service-a/base/config/*` in `first-e
- `--commit-name` : The other half of `commit-email`. Both must be set.
- `--debug` : prints extra debug output if true.
- `--from` : an https URL to a GitOps repository for 'remote' cases, or a path to a Git clone of a microservice for 'local' cases.
- `--from-env` : use this to specify an environment folder in the source repository, for when you have more than one environment per repository. If this is not provided when the repository has more than one folder under `environments/`, then the operation will fail.
- `--from-branch` : use this to specify a branch on the source repository, instead of using the "master" branch.
- `--help`: prints the above text if true.
- `--insecure-skip-verify` : skip TLS cerificate verification if true. Do not set this to true unless you know what you are doing.
- `--keep-cache` : `cache-dir` is deleted unless this is set to true. Keeping the cache will often cause further promotion attempts to fail. This flag is mostly used along with `--debug` when investigating failure cases.
- `--repository-type` : the type of repository: github, gitlab or ghe (default "github"). If `--from` is a Git URL, it must be of the same type as that specified via `--to`.
- `--service` : the destination path for promotion is `/environments/<env-name>/services/<service-name>/base/config/`. This argument defines `service-name` in that path.
- `--to`: an https URL to the destination GitOps repository.
- `--to-env` : use this to specify an environment folder in the destination repository, for when you have more than one environment per repository. If this is not provided when the repository has more than one folder under `environments/`, then the operation will fail.
- `--to-branch` : use this to specify a branch on the destination repository, instead of using the "master" branch.
### Promote Sub-commands
The main promote commands provides a lot of flexibility with all of its options, but the subcommands provide a simpler interface for the usual promotion paths. For example, when promoting between environment folders in the same repository and branch, you could use either of these commands:
``` bash
services promote --from "https://github.com/example/my-gitops.git" --from-env-folder "dev" --to "https://github.com/example/my-gitops.git" --to-env-folder "prod" --service "example"
# or
services promote env --from "dev" --to "prod" --repo "https://github.com/example/my-gitops.git" --service "example"
```
### Troubleshooting
- Authentication and authorisation failures: ensure that GITHUB_TOKEN is set and has the necessary permissions.
Expand Down
200 changes: 75 additions & 125 deletions pkg/cmd/promote.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"errors"
"fmt"
"log"

"github.com/mitchellh/go-homedir"
"github.com/rhd-gitops-example/services/pkg/git"
Expand All @@ -13,125 +12,61 @@ import (
"github.com/tcnksm/go-gitconfig"
)

func makePromoteCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "promote",
Short: "promote from one environment to another",
RunE: promoteAction,
}
var promoteCmd = &cobra.Command{
Use: "promote",
Short: "promote from one environment to another",
RunE: promoteAction,
}

// Required flags
cmd.Flags().String(
fromFlag,
"",
"source Git repository",
)
logIfError(cmd.MarkFlagRequired(fromFlag))
logIfError(viper.BindPFlag(fromFlag, cmd.Flags().Lookup(fromFlag)))
const (
branchNameFlag = "branch-name"
fromFlag = "from"
fromBranchFlag = "from-branch"
fromEnvFolderFlag = "from-env-folder"
serviceFlag = "service"
toFlag = "to"
toBranchFlag = "to-branch"
toEnvFolderFlag = "to-env-folder"

repoFlag = "repo" // used by subcommands
)

cmd.Flags().String(
toFlag,
"",
"destination Git repository",
)
logIfError(cmd.MarkFlagRequired(toFlag))
logIfError(viper.BindPFlag(toFlag, cmd.Flags().Lookup(toFlag)))
func init() {
rootCmd.AddCommand(promoteCmd)

cmd.Flags().String(
serviceFlag,
"",
"service name to promote",
)
logIfError(cmd.MarkFlagRequired(serviceFlag))
logIfError(viper.BindPFlag(serviceFlag, cmd.Flags().Lookup(serviceFlag)))
promoteCmd.PersistentFlags().String(branchNameFlag, "", "the branch on the destination repository for the pull request (auto-generated if empty)")
promoteCmd.PersistentFlags().String(cacheDirFlag, "~/.promotion/cache", "where to cache Git checkouts")
promoteCmd.PersistentFlags().Bool(keepCacheFlag, false, "whether to retain the locally cloned repositories in the cache directory")

// Optional flags
cmd.Flags().String(
branchNameFlag,
"",
"the name of the branch on the destination repository for the pull request (auto-generated if empty)",
)
logIfError(viper.BindPFlag(branchNameFlag, cmd.Flags().Lookup(branchNameFlag)))
promoteCmd.Flags().String(fromFlag, "", "the source Git repository (URL or local)")
promoteCmd.Flags().String(toFlag, "", "the destination Git repository")
promoteCmd.Flags().String(serviceFlag, "", "the name of the service to promote")
promoteCmd.Flags().String(fromBranchFlag, "master", "the branch on the source Git repository")
promoteCmd.Flags().String(fromEnvFolderFlag, "", "env folder on the source Git repository (if not provided, the repository should only have one folder under environments/)")
promoteCmd.Flags().String(toBranchFlag, "master", "the branch on the destination Git repository")
promoteCmd.Flags().String(toEnvFolderFlag, "", "env folder on the destination Git repository (if not provided, the repository should only have one folder under environments/)")

logIfError(promoteCmd.MarkFlagRequired(fromFlag))
logIfError(promoteCmd.MarkFlagRequired(toFlag))
logIfError(promoteCmd.MarkFlagRequired(serviceFlag))
}

cmd.Flags().String(
func promoteAction(c *cobra.Command, args []string) error {
bindFlags(c.PersistentFlags(), []string{
branchNameFlag,
cacheDirFlag,
"~/.promotion/cache",
"where to cache Git checkouts",
)
logIfError(viper.BindPFlag(cacheDirFlag, cmd.Flags().Lookup(cacheDirFlag)))

cmd.Flags().String(
emailFlag,
"",
"the email to use for commits when creating branches",
)
logIfError(viper.BindPFlag(emailFlag, cmd.Flags().Lookup(emailFlag)))

cmd.Flags().String(
msgFlag,
"",
"the msg to use on the resultant commit and pull request",
)
logIfError(viper.BindPFlag(msgFlag, cmd.Flags().Lookup(msgFlag)))

cmd.Flags().String(
nameFlag,
"",
"the name to use for commits when creating branches",
)
logIfError(viper.BindPFlag(nameFlag, cmd.Flags().Lookup(nameFlag)))

cmd.Flags().Bool(
debugFlag,
false,
"additional debug logging output",
)
logIfError(viper.BindPFlag(debugFlag, cmd.Flags().Lookup(debugFlag)))

cmd.Flags().String(
fromBranchFlag,
"master",
"branch on the source Git repository",
)
logIfError(viper.BindPFlag(fromBranchFlag, cmd.Flags().Lookup(fromBranchFlag)))

cmd.Flags().Bool(
insecureSkipVerifyFlag,
false,
"Insecure skip verify TLS certificate",
)
logIfError(viper.BindPFlag(insecureSkipVerifyFlag, cmd.Flags().Lookup(insecureSkipVerifyFlag)))

cmd.Flags().Bool(
keepCacheFlag,
false,
"whether to retain the locally cloned repositories in the cache directory",
)
logIfError(viper.BindPFlag(keepCacheFlag, cmd.Flags().Lookup(keepCacheFlag)))

cmd.Flags().String(
repoTypeFlag,
"github",
"the type of repository: github, gitlab or ghe",
)
logIfError(viper.BindPFlag(repoTypeFlag, cmd.Flags().Lookup(repoTypeFlag)))

cmd.Flags().String(
})
bindFlags(c.Flags(), []string{
fromFlag,
toFlag,
serviceFlag,
fromBranchFlag,
fromEnvFolderFlag,
toBranchFlag,
"master",
"branch on the destination Git repository",
)
logIfError(viper.BindPFlag(toBranchFlag, cmd.Flags().Lookup(toBranchFlag)))
return cmd
}

func logIfError(e error) {
if e != nil {
log.Fatal(e)
}
}
toEnvFolderFlag,
})

func promoteAction(c *cobra.Command, args []string) error {
// Required flags
fromRepo := viper.GetString(fromFlag)
toRepo := viper.GetString(toFlag)
Expand All @@ -140,36 +75,51 @@ func promoteAction(c *cobra.Command, args []string) error {
// Optional flags
newBranchName := viper.GetString(branchNameFlag)
msg := viper.GetString(msgFlag)
debug := viper.GetBool(debugFlag)
fromBranch := viper.GetString(fromBranchFlag)
insecureSkipVerify := viper.GetBool(insecureSkipVerifyFlag)
fromEnvFolder := viper.GetString(fromEnvFolderFlag)
keepCache := viper.GetBool(keepCacheFlag)
repoType := viper.GetString(repoTypeFlag)
toBranch := viper.GetString(toBranchFlag)

cacheDir, err := homedir.Expand(viper.GetString(cacheDirFlag))
if err != nil {
return fmt.Errorf("failed to expand cacheDir path: %w", err)
}

author, err := newAuthor()
if err != nil {
return fmt.Errorf("unable to establish credentials: %w", err)
}
toEnvFolder := viper.GetString(toEnvFolderFlag)

from := promotion.EnvLocation{
RepoPath: fromRepo,
Branch: fromBranch,
Folder: fromEnvFolder,
}
to := promotion.EnvLocation{
RepoPath: toRepo,
Branch: toBranch,
Folder: toEnvFolder,
}

sm, err := newServiceManager()
if err != nil {
return err
}

sm := promotion.New(cacheDir, author, promotion.WithDebug(debug), promotion.WithInsecureSkipVerify(insecureSkipVerify), promotion.WithRepoType(repoType))
return sm.Promote(service, from, to, newBranchName, msg, keepCache)
}

func newServiceManager() (*promotion.ServiceManager, error) {
cacheDir, err := homedir.Expand(viper.GetString(cacheDirFlag))
if err != nil {
return nil, fmt.Errorf("failed to expand cacheDir path: %w", err)
}

author, err := newAuthor()
if err != nil {
return nil, fmt.Errorf("unable to establish credentials: %w", err)
}

return promotion.New(
cacheDir,
author,
promotion.WithDebug(viper.GetBool(debugFlag)),
promotion.WithInsecureSkipVerify(viper.GetBool(insecureSkipVerifyFlag)),
promotion.WithRepoType(viper.GetString(repoTypeFlag)),
), nil
}

func newAuthor() (*git.Author, error) {
name := viper.GetString(nameFlag)
email := viper.GetString(emailFlag)
Expand Down
69 changes: 69 additions & 0 deletions pkg/cmd/promote_branch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package cmd

import (
"fmt"

"github.com/rhd-gitops-example/services/pkg/promotion"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var promoteBranchCmd = &cobra.Command{
Use: "branch",
Short: "promote between branches within one repository",
RunE: promoteBranchAction,
}

func init() {
promoteCmd.AddCommand(promoteBranchCmd)

promoteBranchCmd.Flags().String(fromFlag, "", "the source branch")
promoteBranchCmd.Flags().String(toFlag, "", "the destination branch")
promoteBranchCmd.Flags().String(serviceFlag, "", "the name of the service to promote")
promoteBranchCmd.Flags().String(repoFlag, "", "the URL of the Git repository")

logIfError(promoteBranchCmd.MarkFlagRequired(fromFlag))
logIfError(promoteBranchCmd.MarkFlagRequired(toFlag))
logIfError(promoteBranchCmd.MarkFlagRequired(serviceFlag))
logIfError(promoteBranchCmd.MarkFlagRequired(repoFlag))
}

func promoteBranchAction(c *cobra.Command, args []string) error {
bindFlags(c.Flags(), []string{
fromFlag,
toFlag,
serviceFlag,
repoFlag,
})

fromBranch := viper.GetString(fromFlag)
toBranch := viper.GetString(toFlag)
service := viper.GetString(serviceFlag)
repo := viper.GetString(repoFlag)

newBranchName := viper.GetString(branchNameFlag)
msg := viper.GetString(msgFlag)
keepCache := viper.GetBool(keepCacheFlag)

from := promotion.EnvLocation{
RepoPath: repo,
Branch: fromBranch,
Folder: "",
}
to := promotion.EnvLocation{
RepoPath: repo,
Branch: toBranch,
Folder: "",
}

sm, err := newServiceManager()
if err != nil {
return err
}

if msg == "" {
msg = fmt.Sprintf("Promote branch %s to %s", fromBranch, toBranch)
}

return sm.Promote(service, from, to, newBranchName, msg, keepCache)
}
Loading

0 comments on commit 138702f

Please sign in to comment.