github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/step/step_tag.go (about)

     1  package step
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/olli-ai/jx/v2/pkg/cmd/opts/step"
    13  
    14  	"github.com/olli-ai/jx/v2/pkg/cmd/helper"
    15  	"github.com/olli-ai/jx/v2/pkg/kube/naming"
    16  
    17  	"github.com/jenkins-x/jx-logging/pkg/log"
    18  	"github.com/olli-ai/jx/v2/pkg/cmd/opts"
    19  	"github.com/olli-ai/jx/v2/pkg/cmd/templates"
    20  	"github.com/olli-ai/jx/v2/pkg/config"
    21  	"github.com/olli-ai/jx/v2/pkg/util"
    22  	"github.com/spf13/cobra"
    23  	"k8s.io/helm/pkg/chartutil"
    24  )
    25  
    26  const (
    27  	VERSION = "version"
    28  
    29  	defaultVersionFile = "VERSION"
    30  
    31  	ValuesYamlRepositoryPrefix = "  repository:"
    32  	ValuesYamlTagPrefix        = "  tag:"
    33  )
    34  
    35  // CreateClusterOptions the flags for running create cluster
    36  type StepTagOptions struct {
    37  	step.StepOptions
    38  
    39  	Flags StepTagFlags
    40  }
    41  
    42  type StepTagFlags struct {
    43  	Version              string
    44  	VersionFile          string
    45  	Dir                  string
    46  	ChartsDir            string
    47  	ChartValueRepository string
    48  	NoApply              bool
    49  }
    50  
    51  var (
    52  	stepTagLong = templates.LongDesc(`
    53  		This pipeline step command creates a git tag using a version number prefixed with 'v' and pushes it to a
    54  		remote origin repo.
    55  
    56  		This commands effectively runs:
    57  
    58  		    $ git commit -a -m "release $(VERSION)" --allow-empty
    59  		    $ git tag -fa v$(VERSION) -m "Release version $(VERSION)"
    60  		    $ git push origin v$(VERSION)
    61  
    62  `)
    63  
    64  	stepTagExample = templates.Examples(`
    65  
    66  		jx step tag --version 1.0.0
    67  
    68  `)
    69  )
    70  
    71  func NewCmdStepTag(commonOpts *opts.CommonOptions) *cobra.Command {
    72  	options := StepTagOptions{
    73  		StepOptions: step.StepOptions{
    74  			CommonOptions: commonOpts,
    75  		},
    76  	}
    77  	cmd := &cobra.Command{
    78  		Use:     "tag",
    79  		Short:   "Creates a git tag and pushes to remote repo",
    80  		Long:    stepTagLong,
    81  		Example: stepTagExample,
    82  		Run: func(cmd *cobra.Command, args []string) {
    83  			options.Cmd = cmd
    84  			options.Args = args
    85  			err := options.Run()
    86  			helper.CheckErr(err)
    87  		},
    88  	}
    89  
    90  	cmd.Flags().StringVarP(&options.Flags.Version, VERSION, "v", "", "version number for the tag [required]")
    91  	cmd.Flags().StringVarP(&options.Flags.VersionFile, "version-file", "", defaultVersionFile, "The file name used to load the version number from if no '--version' option is specified")
    92  
    93  	cmd.Flags().StringVarP(&options.Flags.ChartsDir, "charts-dir", "d", "", "the directory of the chart to update the version")
    94  	cmd.Flags().StringVarP(&options.Flags.Dir, "dir", "", "", "the directory which may contain a 'jenkins-x.yml'")
    95  	cmd.Flags().StringVarP(&options.Flags.ChartValueRepository, "charts-value-repository", "r", "", "the fully qualified image name without the version tag. e.g. 'dockerregistry/myorg/myapp'")
    96  
    97  	cmd.Flags().BoolVarP(&options.Flags.NoApply, "no-apply", "", false, "Do not push the tag to the server, this is used for example in dry runs")
    98  
    99  	return cmd
   100  }
   101  
   102  func (o *StepTagOptions) Run() error {
   103  	if o.Flags.Version == "" {
   104  		// lets see if its defined in the VERSION file
   105  		path := o.Flags.VersionFile
   106  		if path == "" {
   107  			path = "VERSION"
   108  		}
   109  		exists, err := util.FileExists(path)
   110  		if exists && err == nil {
   111  			data, err := ioutil.ReadFile(path)
   112  			if err != nil {
   113  				return err
   114  			}
   115  			o.Flags.Version = strings.TrimSpace(string(data))
   116  		}
   117  	}
   118  	if o.Flags.Version == "" {
   119  		return errors.New("No version flag")
   120  	}
   121  	log.Logger().Debug("looking for charts folder...")
   122  	chartsDir := o.Flags.ChartsDir
   123  	if chartsDir == "" {
   124  		exists, err := util.FileExists("Chart.yaml")
   125  		if !exists && err == nil {
   126  			// lets try find the charts/foo dir ignoring the charts/preview dir
   127  			chartsDir, err = o.findChartsDir()
   128  			if err != nil {
   129  				return err
   130  			}
   131  		}
   132  	}
   133  	log.Logger().Debugf("updating chart if it exists")
   134  	err := o.updateChart(o.Flags.Version, chartsDir)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	err = o.updateChartValues(o.Flags.Version, chartsDir)
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	tag := "v" + o.Flags.Version
   144  	log.Logger().Debugf("performing git commit")
   145  	err = o.Git().AddCommit("", fmt.Sprintf("release %s", o.Flags.Version))
   146  	if err != nil {
   147  		return err
   148  	}
   149  
   150  	err = o.Git().CreateTag("", tag, fmt.Sprintf("release %s", o.Flags.Version))
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	if o.Flags.NoApply {
   156  		log.Logger().Infof("NoApply: no push tag to git server")
   157  	} else {
   158  
   159  		log.Logger().Debugf("pushing git tag %s", tag)
   160  		err = o.Git().PushTag("", tag)
   161  		if err != nil {
   162  			return err
   163  		}
   164  
   165  		log.Logger().Infof("Tag %s created and pushed to remote origin", tag)
   166  	}
   167  	return nil
   168  }
   169  
   170  func (o *StepTagOptions) updateChart(version string, chartsDir string) error {
   171  	chartFile := filepath.Join(chartsDir, "Chart.yaml")
   172  
   173  	exists, err := util.FileExists(chartFile)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	if !exists {
   178  		return nil
   179  	}
   180  	chart, err := chartutil.LoadChartfile(chartFile)
   181  	if err != nil {
   182  		return err
   183  	}
   184  	if chart.Version == version {
   185  		return nil
   186  	}
   187  	chart.Version = version
   188  	chart.AppVersion = version
   189  	log.Logger().Infof("Updating chart version in %s to %s", chartFile, version)
   190  	err = chartutil.SaveChartfile(chartFile, chart)
   191  	if err != nil {
   192  		return fmt.Errorf("Failed to save chart %s: %s", chartFile, err)
   193  	}
   194  	return nil
   195  }
   196  
   197  func (o *StepTagOptions) updateChartValues(version string, chartsDir string) error {
   198  	valuesFile := filepath.Join(chartsDir, "values.yaml")
   199  
   200  	exists, err := util.FileExists(valuesFile)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	if !exists {
   205  		return nil
   206  	}
   207  	data, err := ioutil.ReadFile(valuesFile)
   208  	lines := strings.Split(string(data), "\n")
   209  	chartValueRepository := o.Flags.ChartValueRepository
   210  	if chartValueRepository == "" {
   211  		chartValueRepository = o.defaultChartValueRepository()
   212  	}
   213  	updated := false
   214  	changedRepository := false
   215  	changedTag := false
   216  	for idx, line := range lines {
   217  		if chartValueRepository != "" && strings.HasPrefix(line, ValuesYamlRepositoryPrefix) && !changedRepository {
   218  			// lets ensure we use a valid docker image name
   219  			chartValueRepository = naming.ToValidImageName(chartValueRepository)
   220  			updated = true
   221  			changedRepository = true
   222  			log.Logger().Infof("Updating repository in %s to %s", valuesFile, chartValueRepository)
   223  			lines[idx] = ValuesYamlRepositoryPrefix + " " + chartValueRepository
   224  		} else if strings.HasPrefix(line, ValuesYamlTagPrefix) && !changedTag {
   225  			version = naming.ToValidImageVersion(version)
   226  			updated = true
   227  			changedTag = true
   228  			log.Logger().Infof("Updating tag in %s to %s", valuesFile, version)
   229  			lines[idx] = ValuesYamlTagPrefix + " " + version
   230  		}
   231  	}
   232  	if updated {
   233  		err = ioutil.WriteFile(valuesFile, []byte(strings.Join(lines, "\n")), util.DefaultWritePermissions)
   234  		if err != nil {
   235  			return fmt.Errorf("Failed to save chart file %s: %s", valuesFile, err)
   236  		}
   237  	}
   238  	return nil
   239  }
   240  
   241  func (o *StepTagOptions) defaultChartValueRepository() string {
   242  	gitInfo, err := o.FindGitInfo(o.Flags.ChartsDir)
   243  	if err != nil {
   244  		log.Logger().Warnf("failed to find git repository: %s", err.Error())
   245  	}
   246  
   247  	projectConfig, _, _ := config.LoadProjectConfig(o.Flags.Dir)
   248  	dockerRegistry := o.GetDockerRegistry(projectConfig)
   249  	dockerRegistryOrg := o.GetDockerRegistryOrg(projectConfig, gitInfo)
   250  	if dockerRegistryOrg == "" {
   251  		dockerRegistryOrg = os.Getenv("ORG")
   252  	}
   253  	if dockerRegistryOrg == "" {
   254  		dockerRegistryOrg = os.Getenv("REPO_OWNER")
   255  	}
   256  	appName := os.Getenv("APP_NAME")
   257  	if appName == "" {
   258  		appName = os.Getenv("REPO_NAME")
   259  	}
   260  	if dockerRegistryOrg == "" && gitInfo != nil {
   261  		dockerRegistryOrg = gitInfo.Organisation
   262  	}
   263  	if appName == "" && gitInfo != nil {
   264  		appName = gitInfo.Name
   265  	}
   266  	if dockerRegistry != "" && dockerRegistryOrg != "" && appName != "" {
   267  		return dockerRegistry + "/" + dockerRegistryOrg + "/" + naming.ToValidName(appName)
   268  	}
   269  	log.Logger().Warnf("could not generate chart repository name for GetDockerRegistry %s, GetDockerRegistryOrg %s, appName %s", dockerRegistry, dockerRegistryOrg, appName)
   270  	return ""
   271  }
   272  
   273  // lets try find the charts dir
   274  func (o *StepTagOptions) findChartsDir() (string, error) {
   275  	files, err := filepath.Glob("*/*/Chart.yaml")
   276  	if err != nil {
   277  		return "", fmt.Errorf("failed to find Chart.yaml file: %s", err)
   278  	}
   279  	// get the app name as expeted in a file name
   280  	appName := os.Getenv("APP_NAME")
   281  	if appName == "" {
   282  		appName = os.Getenv("REPO_NAME")
   283  	}
   284  	if appName != "" {
   285  		appName = naming.ToValidName(appName)
   286  	}
   287  	// sort files by likehood
   288  	rank := func(file string) int {
   289  		paths := strings.Split(file, string(os.PathSeparator))
   290  		//not a candidate
   291  		if len(paths) < 3 || paths[len(paths)-2] == "preview" {
   292  			return 10
   293  		}
   294  		r := 3
   295  		// 2 points for being in charts/ directory
   296  		if paths[len(paths)-3] == "charts" {
   297  			r -= 2
   298  		}
   299  		// 1 point for being in <appName>/ directory
   300  		if appName != "" && paths[len(paths)-2] == appName {
   301  			r -= 1
   302  		}
   303  		log.Logger().Debugf("ranked %s at %d", file, r)
   304  		return r
   305  	}
   306  	less := func(i, j int) bool {
   307  		return rank(files[i]) < rank(files[j])
   308  	}
   309  	sort.Slice(files, less)
   310  	// check the most likely file
   311  	if len(files) > 0 {
   312  		file := files[0]
   313  		paths := strings.Split(file, string(os.PathSeparator))
   314  		if len(paths) >= 3 && paths[len(paths)-2] != "preview" {
   315  			dir, _ := filepath.Split(file)
   316  			return dir, nil
   317  		}
   318  	}
   319  	return "", nil
   320  }