github.com/jenkins-x/jx/v2@v2.1.155/pkg/apps/gitops.go (about)

     1  package apps
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"github.com/jenkins-x/jx/v2/pkg/gits"
    10  
    11  	"github.com/jenkins-x/jx/v2/pkg/helm"
    12  	"github.com/jenkins-x/jx/v2/pkg/util"
    13  	"k8s.io/helm/pkg/proto/hapi/chart"
    14  
    15  	"github.com/ghodss/yaml"
    16  	v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1"
    17  
    18  	"io/ioutil"
    19  
    20  	"github.com/jenkins-x/jx-logging/pkg/log"
    21  	"github.com/jenkins-x/jx/v2/pkg/environments"
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  // GitOpsOptions is the options used for Git Operations for apps
    26  type GitOpsOptions struct {
    27  	*InstallOptions
    28  }
    29  
    30  // AddApp adds the app with version rooted in dir from the repository. An alias can be specified.
    31  func (o *GitOpsOptions) AddApp(app string, dir string, version string, repository string, alias string, autoMerge bool) error {
    32  	details := gits.PullRequestDetails{
    33  		BranchName: "add-app-" + app + "-" + version,
    34  		Title:      fmt.Sprintf("Add %s %s", app, version),
    35  		Message:    fmt.Sprintf("Add app %s %s", app, version),
    36  	}
    37  
    38  	options := environments.EnvironmentPullRequestOptions{
    39  		Gitter: o.Gitter,
    40  		ModifyChartFn: environments.CreateAddRequirementFn(app, alias, version,
    41  			repository, o.valuesFiles, dir, o.Verbose, o.Helmer),
    42  		GitProvider: o.GitProvider,
    43  	}
    44  
    45  	info, err := options.Create(o.DevEnv, o.EnvironmentCloneDir, &details, nil, "", autoMerge)
    46  	if err != nil {
    47  		return errors.Wrapf(err, "creating pr for %s", app)
    48  	}
    49  
    50  	if info != nil {
    51  		log.Logger().Infof("Added app via Pull Request %s", info.PullRequest.URL)
    52  	} else {
    53  		log.Logger().Infof("Already up to date")
    54  	}
    55  	return nil
    56  }
    57  
    58  // UpgradeApp upgrades the app (or all apps if empty) to a version (
    59  // or latest if empty) from a repository with username and password.
    60  // If one app is being upgraded an alias can be specified.
    61  func (o *GitOpsOptions) UpgradeApp(app string, version string, repository string, username string, password string,
    62  	alias string, interrogateChartFunc func(dir string, existing map[string]interface{}) (*ChartDetails,
    63  		error), autoMerge bool) error {
    64  	all := true
    65  	details := gits.PullRequestDetails{}
    66  
    67  	// use a random string in the branch name to ensure we use a unique git branch and fail to push
    68  	rand, err := util.RandStringBytesMaskImprSrc(5)
    69  	if err != nil {
    70  		return errors.Wrapf(err, "failed to generate a random string")
    71  	}
    72  
    73  	if app != "" {
    74  		all = false
    75  		versionBranchName := version
    76  		if versionBranchName == "" {
    77  			versionBranchName = "latest"
    78  		}
    79  		details.BranchName = fmt.Sprintf("upgrade-app-%s-%s-%s", app, versionBranchName, rand)
    80  	} else {
    81  		details.BranchName = fmt.Sprintf("upgrade-all-apps-%s", rand)
    82  		details.Title = fmt.Sprintf("Upgrade all apps")
    83  		details.Message = fmt.Sprintf("Upgrade all apps:\n")
    84  	}
    85  
    86  	var interrogateCleanup func()
    87  	defer func() {
    88  		if interrogateCleanup != nil {
    89  			interrogateCleanup()
    90  		}
    91  	}()
    92  	inspectChartFunc := func(chartDir string, values map[string]interface{}) error {
    93  		chartDetails, err := interrogateChartFunc(chartDir, values)
    94  		interrogateCleanup = chartDetails.Cleanup
    95  		if err != nil {
    96  			return errors.Wrapf(err, "asking questions for %s", chartDir)
    97  		}
    98  		return nil
    99  	}
   100  
   101  	options := environments.EnvironmentPullRequestOptions{
   102  		Gitter: o.Gitter,
   103  		ModifyChartFn: environments.CreateUpgradeRequirementsFn(all, app, alias, version, username, password,
   104  			o.Helmer, inspectChartFunc, o.Verbose, o.valuesFiles),
   105  		GitProvider: o.GitProvider,
   106  	}
   107  
   108  	_, err = options.Create(o.DevEnv, o.EnvironmentCloneDir, &details, nil, app, autoMerge)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	return nil
   113  }
   114  
   115  // DeleteApp deletes the app with alias
   116  func (o *GitOpsOptions) DeleteApp(app string, alias string, autoMerge bool) error {
   117  
   118  	modifyChartFn := func(requirements *helm.Requirements, metadata *chart.Metadata, values map[string]interface{},
   119  		templates map[string]string, dir string, details *gits.PullRequestDetails) error {
   120  		// See if the app already exists in requirements
   121  		found := false
   122  		for i, d := range requirements.Dependencies {
   123  			if d.Name == app && d.Alias == alias {
   124  				found = true
   125  				requirements.Dependencies = append(requirements.Dependencies[:i], requirements.Dependencies[i+1:]...)
   126  			}
   127  		}
   128  		// If app not found, add it
   129  		if !found {
   130  			a := app
   131  			if alias != "" {
   132  				a = fmt.Sprintf("%s with alias %s", a, alias)
   133  			}
   134  			return fmt.Errorf("unable to delete app %s as not installed", app)
   135  		}
   136  		if info, err := os.Stat(filepath.Join(dir, app)); err == nil {
   137  			if info.IsDir() {
   138  				err := util.DeleteFile(info.Name())
   139  				if err != nil {
   140  					return err
   141  				}
   142  			} else {
   143  				log.Logger().Warnf("Not removing %s for %s because it is not a directory", info.Name(), app)
   144  			}
   145  		}
   146  		return nil
   147  	}
   148  	details := gits.PullRequestDetails{
   149  		BranchName: "delete-app-" + app,
   150  		Title:      fmt.Sprintf("Delete %s", app),
   151  		Message:    fmt.Sprintf("Delete app %s", app),
   152  	}
   153  
   154  	options := environments.EnvironmentPullRequestOptions{
   155  		Gitter:        o.Gitter,
   156  		ModifyChartFn: modifyChartFn,
   157  		GitProvider:   o.GitProvider,
   158  	}
   159  
   160  	info, err := options.Create(o.DevEnv, o.EnvironmentCloneDir, &details, nil, "", autoMerge)
   161  	if err != nil {
   162  		return err
   163  	}
   164  	log.Logger().Infof("Delete app via Pull Request %s", info.PullRequest.URL)
   165  	return nil
   166  }
   167  
   168  // GetApps retrieves all the apps information for the given appNames from the repository and / or the CRD API
   169  func (o *GitOpsOptions) GetApps(appNames map[string]bool, expandFn func([]string) (*v1.AppList, error)) (*v1.AppList, error) {
   170  	// AddApp, DeleteApp, and UpgradeApps delegate selecting/creating the directory to clone in to environments/gitops.go's
   171  	// Create function, but here we need to create the directory explicitly. since we aren't calling Create, because we're
   172  	// not creating a pull request.
   173  	dir, err := ioutil.TempDir("", "get-apps-")
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	defer os.RemoveAll(dir)
   178  
   179  	gitInfo, err := gits.ParseGitURL(o.DevEnv.Spec.Source.URL)
   180  	if err != nil {
   181  		return nil, errors.Wrapf(err, "parsing dev env repo URL %s", o.DevEnv.Spec.Source.URL)
   182  	}
   183  
   184  	providerInfo, err := o.GitProvider.GetRepository(gitInfo.Organisation, gitInfo.Name)
   185  	if err != nil {
   186  		return nil, errors.Wrapf(err, "determining git provider information for %s", o.DevEnv.Spec.Source.URL)
   187  	}
   188  	cloneUrl := providerInfo.CloneURL
   189  	userDetails := o.GitProvider.UserAuth()
   190  	originFetchURL, err := o.Gitter.CreateAuthenticatedURL(cloneUrl, &userDetails)
   191  	if err != nil {
   192  		return nil, errors.Wrapf(err, "failed to create authenticated fetch URL for %s", cloneUrl)
   193  	}
   194  	err = o.Gitter.Clone(originFetchURL, dir)
   195  	if err != nil {
   196  		return nil, errors.Wrapf(err, "failed to clone %s to dir %s", cloneUrl, dir)
   197  	}
   198  	err = o.Gitter.Checkout(dir, o.DevEnv.Spec.Source.Ref)
   199  	if err != nil {
   200  		return nil, errors.Wrapf(err, "failed to checkout %s to dir %s", o.DevEnv.Spec.Source.Ref, dir)
   201  	}
   202  
   203  	envDir := filepath.Join(dir, helm.DefaultEnvironmentChartDir)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	exists, err := util.DirExists(envDir)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	if !exists {
   213  		envDir = dir
   214  	}
   215  
   216  	requirementsFile, err := ioutil.ReadFile(filepath.Join(envDir, helm.RequirementsFileName))
   217  	if err != nil {
   218  		return nil, errors.Wrap(err, "couldn't read the environment's requirements.yaml file")
   219  	}
   220  	reqs := helm.Requirements{}
   221  	err = yaml.Unmarshal(requirementsFile, &reqs)
   222  	if err != nil {
   223  		return nil, errors.Wrap(err, "couldn't unmarshal the environment's requirements.yaml file")
   224  	}
   225  
   226  	appsList := v1.AppList{}
   227  	for _, d := range reqs.Dependencies {
   228  		if appNames[d.Name] == true || len(appNames) == 0 {
   229  			//Make sure we ignore the jenkins-x-platform requirement
   230  			if d.Name != "jenkins-x-platform" {
   231  				resourcesInCRD, _ := expandFn([]string{d.Name})
   232  				if len(resourcesInCRD.Items) != 0 {
   233  					appsList.Items = append(appsList.Items, resourcesInCRD.Items...)
   234  				} else {
   235  					appPath := filepath.Join(envDir, d.Name, "templates", "app.yaml")
   236  					exists, err := util.FileExists(appPath)
   237  					if err != nil {
   238  						return nil, errors.Wrapf(err, "there was a problem checking if %s exists", appPath)
   239  					}
   240  					if exists {
   241  						appFile, err := ioutil.ReadFile(appPath)
   242  						if err != nil {
   243  							return nil, errors.Wrapf(err, "there was a problem reading the app.yaml file of %s", d.Name)
   244  						}
   245  						app := v1.App{}
   246  						err = yaml.Unmarshal(appFile, &app)
   247  						if err != nil {
   248  							return nil, errors.Wrapf(err, "there was a problem unmarshalling the app.yaml file of %s", d.Name)
   249  						}
   250  						appsList.Items = append(appsList.Items, app)
   251  					}
   252  				}
   253  			}
   254  		}
   255  	}
   256  	return &appsList, nil
   257  }