github.com/argoproj/argo-cd@v1.8.7/test/e2e/fixture/fixture.go (about)

     1  package fixture
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/argoproj/pkg/errors"
    15  	jsonpatch "github.com/evanphx/json-patch"
    16  	"github.com/ghodss/yaml"
    17  	log "github.com/sirupsen/logrus"
    18  	corev1 "k8s.io/api/core/v1"
    19  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/client-go/dynamic"
    21  	"k8s.io/client-go/kubernetes"
    22  	"k8s.io/client-go/rest"
    23  	"k8s.io/client-go/tools/clientcmd"
    24  
    25  	"github.com/argoproj/argo-cd/common"
    26  	argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
    27  	sessionpkg "github.com/argoproj/argo-cd/pkg/apiclient/session"
    28  	"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
    29  	appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
    30  	. "github.com/argoproj/argo-cd/util/errors"
    31  	grpcutil "github.com/argoproj/argo-cd/util/grpc"
    32  	"github.com/argoproj/argo-cd/util/io"
    33  	"github.com/argoproj/argo-cd/util/rand"
    34  	"github.com/argoproj/argo-cd/util/settings"
    35  )
    36  
    37  const (
    38  	defaultApiServer = "localhost:8080"
    39  	adminPassword    = "password"
    40  	testingLabel     = "e2e.argoproj.io"
    41  	ArgoCDNamespace  = "argocd-e2e"
    42  
    43  	// ensure all repos are in one directory tree, so we can easily clean them up
    44  	TmpDir             = "/tmp/argo-e2e"
    45  	repoDir            = "testdata.git"
    46  	submoduleDir       = "submodule.git"
    47  	submoduleParentDir = "submoduleParent.git"
    48  
    49  	GuestbookPath = "guestbook"
    50  )
    51  
    52  var (
    53  	id                  string
    54  	deploymentNamespace string
    55  	name                string
    56  	KubeClientset       kubernetes.Interface
    57  	DynamicClientset    dynamic.Interface
    58  	AppClientset        appclientset.Interface
    59  	ArgoCDClientset     argocdclient.Client
    60  	apiServerAddress    string
    61  	token               string
    62  	plainText           bool
    63  )
    64  
    65  type RepoURLType string
    66  
    67  const (
    68  	RepoURLTypeFile                 = "file"
    69  	RepoURLTypeHTTPS                = "https"
    70  	RepoURLTypeHTTPSClientCert      = "https-cc"
    71  	RepoURLTypeHTTPSSubmodule       = "https-sub"
    72  	RepoURLTypeHTTPSSubmoduleParent = "https-par"
    73  	RepoURLTypeSSH                  = "ssh"
    74  	RepoURLTypeSSHSubmodule         = "ssh-sub"
    75  	RepoURLTypeSSHSubmoduleParent   = "ssh-par"
    76  	RepoURLTypeHelm                 = "helm"
    77  	GitUsername                     = "admin"
    78  	GitPassword                     = "password"
    79  	GpgGoodKeyID                    = "D56C4FCA57A46444"
    80  )
    81  
    82  // getKubeConfig creates new kubernetes client config using specified config path and config overrides variables
    83  func getKubeConfig(configPath string, overrides clientcmd.ConfigOverrides) *rest.Config {
    84  	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
    85  	loadingRules.ExplicitPath = configPath
    86  	clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin)
    87  
    88  	restConfig, err := clientConfig.ClientConfig()
    89  	CheckError(err)
    90  	return restConfig
    91  }
    92  
    93  // creates e2e tests fixture: ensures that Application CRD is installed, creates temporal namespace, starts repo and api server,
    94  // configure currently available cluster.
    95  func init() {
    96  	// ensure we log all shell execs
    97  	log.SetLevel(log.DebugLevel)
    98  	// set-up variables
    99  	config := getKubeConfig("", clientcmd.ConfigOverrides{})
   100  	AppClientset = appclientset.NewForConfigOrDie(config)
   101  	KubeClientset = kubernetes.NewForConfigOrDie(config)
   102  	DynamicClientset = dynamic.NewForConfigOrDie(config)
   103  	apiServerAddress = os.Getenv(argocdclient.EnvArgoCDServer)
   104  	if apiServerAddress == "" {
   105  		apiServerAddress = defaultApiServer
   106  	}
   107  
   108  	tlsTestResult, err := grpcutil.TestTLS(apiServerAddress)
   109  	CheckError(err)
   110  
   111  	ArgoCDClientset, err = argocdclient.NewClient(&argocdclient.ClientOptions{Insecure: true, ServerAddr: apiServerAddress, PlainText: !tlsTestResult.TLS})
   112  	CheckError(err)
   113  
   114  	closer, client, err := ArgoCDClientset.NewSessionClient()
   115  	CheckError(err)
   116  	defer io.Close(closer)
   117  
   118  	sessionResponse, err := client.Create(context.Background(), &sessionpkg.SessionCreateRequest{Username: "admin", Password: adminPassword})
   119  	CheckError(err)
   120  
   121  	ArgoCDClientset, err = argocdclient.NewClient(&argocdclient.ClientOptions{
   122  		Insecure:   true,
   123  		ServerAddr: apiServerAddress,
   124  		AuthToken:  sessionResponse.Token,
   125  		PlainText:  !tlsTestResult.TLS,
   126  	})
   127  	CheckError(err)
   128  
   129  	token = sessionResponse.Token
   130  	plainText = !tlsTestResult.TLS
   131  
   132  	log.WithFields(log.Fields{"apiServerAddress": apiServerAddress}).Info("initialized")
   133  }
   134  
   135  func Name() string {
   136  	return name
   137  }
   138  
   139  func repoDirectory() string {
   140  	return path.Join(TmpDir, repoDir)
   141  }
   142  
   143  func submoduleDirectory() string {
   144  	return path.Join(TmpDir, submoduleDir)
   145  }
   146  
   147  func submoduleParentDirectory() string {
   148  	return path.Join(TmpDir, submoduleParentDir)
   149  }
   150  
   151  func RepoURL(urlType RepoURLType) string {
   152  	switch urlType {
   153  	// Git server via SSH
   154  	case RepoURLTypeSSH:
   155  		return "ssh://root@localhost:2222/tmp/argo-e2e/testdata.git"
   156  	// Git submodule repo
   157  	case RepoURLTypeSSHSubmodule:
   158  		return "ssh://root@localhost:2222/tmp/argo-e2e/submodule.git"
   159  		// Git submodule parent repo
   160  	case RepoURLTypeSSHSubmoduleParent:
   161  		return "ssh://root@localhost:2222/tmp/argo-e2e/submoduleParent.git"
   162  	// Git server via HTTPS
   163  	case RepoURLTypeHTTPS:
   164  		return "https://localhost:9443/argo-e2e/testdata.git"
   165  	// Git server via HTTPS - Client Cert protected
   166  	case RepoURLTypeHTTPSClientCert:
   167  		return "https://localhost:9444/argo-e2e/testdata.git"
   168  	case RepoURLTypeHTTPSSubmodule:
   169  		return "https://localhost:9443/argo-e2e/submodule.git"
   170  		// Git submodule parent repo
   171  	case RepoURLTypeHTTPSSubmoduleParent:
   172  		return "https://localhost:9443/argo-e2e/submoduleParent.git"
   173  	// Default - file based Git repository
   174  	case RepoURLTypeHelm:
   175  		return "https://localhost:9444/argo-e2e/testdata.git/helm-repo"
   176  	default:
   177  		return fmt.Sprintf("file://%s", repoDirectory())
   178  	}
   179  }
   180  
   181  func RepoBaseURL(urlType RepoURLType) string {
   182  	return path.Base(RepoURL(urlType))
   183  }
   184  
   185  func DeploymentNamespace() string {
   186  	return deploymentNamespace
   187  }
   188  
   189  // creates a secret for the current test, this currently can only create a single secret
   190  func CreateSecret(username, password string) string {
   191  	secretName := fmt.Sprintf("argocd-e2e-%s", name)
   192  	FailOnErr(Run("", "kubectl", "create", "secret", "generic", secretName,
   193  		"--from-literal=username="+username,
   194  		"--from-literal=password="+password,
   195  		"-n", ArgoCDNamespace))
   196  	FailOnErr(Run("", "kubectl", "label", "secret", secretName, testingLabel+"=true", "-n", ArgoCDNamespace))
   197  	return secretName
   198  }
   199  
   200  // Convinience wrapper for updating argocd-cm
   201  func updateSettingConfigMap(updater func(cm *corev1.ConfigMap) error) {
   202  	updateGenericConfigMap(common.ArgoCDConfigMapName, updater)
   203  }
   204  
   205  // Updates a given config map in argocd-e2e namespace
   206  func updateGenericConfigMap(name string, updater func(cm *corev1.ConfigMap) error) {
   207  	cm, err := KubeClientset.CoreV1().ConfigMaps(ArgoCDNamespace).Get(context.Background(), name, v1.GetOptions{})
   208  	errors.CheckError(err)
   209  	if cm.Data == nil {
   210  		cm.Data = make(map[string]string)
   211  	}
   212  	errors.CheckError(updater(cm))
   213  	_, err = KubeClientset.CoreV1().ConfigMaps(ArgoCDNamespace).Update(context.Background(), cm, v1.UpdateOptions{})
   214  	errors.CheckError(err)
   215  }
   216  
   217  func SetResourceOverrides(overrides map[string]v1alpha1.ResourceOverride) {
   218  	updateSettingConfigMap(func(cm *corev1.ConfigMap) error {
   219  		if len(overrides) > 0 {
   220  			yamlBytes, err := yaml.Marshal(overrides)
   221  			if err != nil {
   222  				return err
   223  			}
   224  			cm.Data["resource.customizations"] = string(yamlBytes)
   225  		} else {
   226  			delete(cm.Data, "resource.customizations")
   227  		}
   228  		return nil
   229  	})
   230  }
   231  
   232  func SetAccounts(accounts map[string][]string) {
   233  	updateSettingConfigMap(func(cm *corev1.ConfigMap) error {
   234  		for k, v := range accounts {
   235  			cm.Data[fmt.Sprintf("accounts.%s", k)] = strings.Join(v, ",")
   236  		}
   237  		return nil
   238  	})
   239  }
   240  
   241  func SetConfigManagementPlugins(plugin ...v1alpha1.ConfigManagementPlugin) {
   242  	updateSettingConfigMap(func(cm *corev1.ConfigMap) error {
   243  		yamlBytes, err := yaml.Marshal(plugin)
   244  		if err != nil {
   245  			return err
   246  		}
   247  		cm.Data["configManagementPlugins"] = string(yamlBytes)
   248  		return nil
   249  	})
   250  }
   251  
   252  func SetResourceFilter(filters settings.ResourcesFilter) {
   253  	updateSettingConfigMap(func(cm *corev1.ConfigMap) error {
   254  		exclusions, err := yaml.Marshal(filters.ResourceExclusions)
   255  		if err != nil {
   256  			return err
   257  		}
   258  		inclusions, err := yaml.Marshal(filters.ResourceInclusions)
   259  		if err != nil {
   260  			return err
   261  		}
   262  		cm.Data["resource.exclusions"] = string(exclusions)
   263  		cm.Data["resource.inclusions"] = string(inclusions)
   264  		return nil
   265  	})
   266  }
   267  
   268  func SetHelmRepos(repos ...settings.HelmRepoCredentials) {
   269  	updateSettingConfigMap(func(cm *corev1.ConfigMap) error {
   270  		yamlBytes, err := yaml.Marshal(repos)
   271  		if err != nil {
   272  			return err
   273  		}
   274  		cm.Data["helm.repositories"] = string(yamlBytes)
   275  		return nil
   276  	})
   277  }
   278  
   279  func SetRepos(repos ...settings.RepositoryCredentials) {
   280  	updateSettingConfigMap(func(cm *corev1.ConfigMap) error {
   281  		yamlBytes, err := yaml.Marshal(repos)
   282  		if err != nil {
   283  			return err
   284  		}
   285  		cm.Data["repositories"] = string(yamlBytes)
   286  		return nil
   287  	})
   288  }
   289  
   290  func SetProjectSpec(project string, spec v1alpha1.AppProjectSpec) {
   291  	proj, err := AppClientset.ArgoprojV1alpha1().AppProjects(ArgoCDNamespace).Get(context.Background(), project, v1.GetOptions{})
   292  	errors.CheckError(err)
   293  	proj.Spec = spec
   294  	_, err = AppClientset.ArgoprojV1alpha1().AppProjects(ArgoCDNamespace).Update(context.Background(), proj, v1.UpdateOptions{})
   295  	errors.CheckError(err)
   296  }
   297  
   298  func EnsureCleanState(t *testing.T) {
   299  
   300  	start := time.Now()
   301  
   302  	policy := v1.DeletePropagationBackground
   303  	// delete resources
   304  	// kubectl delete apps --all
   305  	CheckError(AppClientset.ArgoprojV1alpha1().Applications(ArgoCDNamespace).DeleteCollection(context.Background(), v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{}))
   306  	// kubectl delete appprojects --field-selector metadata.name!=default
   307  	CheckError(AppClientset.ArgoprojV1alpha1().AppProjects(ArgoCDNamespace).DeleteCollection(context.Background(),
   308  		v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{FieldSelector: "metadata.name!=default"}))
   309  	// kubectl delete secrets -l e2e.argoproj.io=true
   310  	CheckError(KubeClientset.CoreV1().Secrets(ArgoCDNamespace).DeleteCollection(context.Background(),
   311  		v1.DeleteOptions{PropagationPolicy: &policy}, v1.ListOptions{LabelSelector: testingLabel + "=true"}))
   312  
   313  	FailOnErr(Run("", "kubectl", "delete", "ns", "-l", testingLabel+"=true", "--field-selector", "status.phase=Active", "--wait=false"))
   314  	FailOnErr(Run("", "kubectl", "delete", "crd", "-l", testingLabel+"=true", "--wait=false"))
   315  
   316  	// reset settings
   317  	updateSettingConfigMap(func(cm *corev1.ConfigMap) error {
   318  		cm.Data = map[string]string{}
   319  		return nil
   320  	})
   321  
   322  	// reset gpg-keys config map
   323  	updateGenericConfigMap(common.ArgoCDGPGKeysConfigMapName, func(cm *corev1.ConfigMap) error {
   324  		cm.Data = map[string]string{}
   325  		return nil
   326  	})
   327  
   328  	SetProjectSpec("default", v1alpha1.AppProjectSpec{
   329  		OrphanedResources:        nil,
   330  		SourceRepos:              []string{"*"},
   331  		Destinations:             []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "*"}},
   332  		ClusterResourceWhitelist: []v1.GroupKind{{Group: "*", Kind: "*"}},
   333  	})
   334  
   335  	// Create separate project for testing gpg signature verification
   336  	FailOnErr(RunCli("proj", "create", "gpg"))
   337  	SetProjectSpec("gpg", v1alpha1.AppProjectSpec{
   338  		OrphanedResources:        nil,
   339  		SourceRepos:              []string{"*"},
   340  		Destinations:             []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "*"}},
   341  		ClusterResourceWhitelist: []v1.GroupKind{{Group: "*", Kind: "*"}},
   342  		SignatureKeys:            []v1alpha1.SignatureKey{{KeyID: GpgGoodKeyID}},
   343  	})
   344  
   345  	// remove tmp dir
   346  	CheckError(os.RemoveAll(TmpDir))
   347  
   348  	// random id - unique across test runs
   349  	postFix := "-" + strings.ToLower(rand.RandString(5))
   350  	id = t.Name() + postFix
   351  	name = DnsFriendly(t.Name(), "")
   352  	deploymentNamespace = DnsFriendly(fmt.Sprintf("argocd-e2e-%s", t.Name()), postFix)
   353  
   354  	// create tmp dir
   355  	FailOnErr(Run("", "mkdir", "-p", TmpDir))
   356  
   357  	// create TLS and SSH certificate directories
   358  	FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/tls"))
   359  	FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/ssh"))
   360  
   361  	// For signing during the tests
   362  	FailOnErr(Run("", "mkdir", "-p", TmpDir+"/gpg"))
   363  	FailOnErr(Run("", "chmod", "0700", TmpDir+"/gpg"))
   364  	prevGnuPGHome := os.Getenv("GNUPGHOME")
   365  	os.Setenv("GNUPGHOME", TmpDir+"/gpg")
   366  	// nolint:errcheck
   367  	Run("", "pkill", "-9", "gpg-agent")
   368  	FailOnErr(Run("", "gpg", "--import", "../fixture/gpg/signingkey.asc"))
   369  	os.Setenv("GNUPGHOME", prevGnuPGHome)
   370  
   371  	// recreate GPG directories
   372  	FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/gpg/source"))
   373  	FailOnErr(Run("", "mkdir", "-p", TmpDir+"/app/config/gpg/keys"))
   374  	FailOnErr(Run("", "chmod", "0700", TmpDir+"/app/config/gpg/keys"))
   375  
   376  	// set-up tmp repo, must have unique name
   377  	FailOnErr(Run("", "cp", "-Rf", "testdata", repoDirectory()))
   378  	FailOnErr(Run(repoDirectory(), "chmod", "777", "."))
   379  	FailOnErr(Run(repoDirectory(), "git", "init"))
   380  	FailOnErr(Run(repoDirectory(), "git", "add", "."))
   381  	FailOnErr(Run(repoDirectory(), "git", "commit", "-q", "-m", "initial commit"))
   382  
   383  	// create namespace
   384  	FailOnErr(Run("", "kubectl", "create", "ns", DeploymentNamespace()))
   385  	FailOnErr(Run("", "kubectl", "label", "ns", DeploymentNamespace(), testingLabel+"=true"))
   386  
   387  	log.WithFields(log.Fields{"duration": time.Since(start), "name": t.Name(), "id": id, "username": "admin", "password": "password"}).Info("clean state")
   388  }
   389  
   390  func RunCli(args ...string) (string, error) {
   391  	return RunCliWithStdin("", args...)
   392  }
   393  
   394  func RunCliWithStdin(stdin string, args ...string) (string, error) {
   395  	if plainText {
   396  		args = append(args, "--plaintext")
   397  	}
   398  
   399  	args = append(args, "--server", apiServerAddress, "--auth-token", token, "--insecure")
   400  
   401  	return RunWithStdin(stdin, "", "../../dist/argocd", args...)
   402  }
   403  
   404  func Patch(path string, jsonPatch string) {
   405  
   406  	log.WithFields(log.Fields{"path": path, "jsonPatch": jsonPatch}).Info("patching")
   407  
   408  	filename := filepath.Join(repoDirectory(), path)
   409  	bytes, err := ioutil.ReadFile(filename)
   410  	CheckError(err)
   411  
   412  	patch, err := jsonpatch.DecodePatch([]byte(jsonPatch))
   413  	CheckError(err)
   414  
   415  	isYaml := strings.HasSuffix(filename, ".yaml")
   416  	if isYaml {
   417  		log.Info("converting YAML to JSON")
   418  		bytes, err = yaml.YAMLToJSON(bytes)
   419  		CheckError(err)
   420  	}
   421  
   422  	log.WithFields(log.Fields{"bytes": string(bytes)}).Info("JSON")
   423  
   424  	bytes, err = patch.Apply(bytes)
   425  	CheckError(err)
   426  
   427  	if isYaml {
   428  		log.Info("converting JSON back to YAML")
   429  		bytes, err = yaml.JSONToYAML(bytes)
   430  		CheckError(err)
   431  	}
   432  
   433  	CheckError(ioutil.WriteFile(filename, bytes, 0644))
   434  	FailOnErr(Run(repoDirectory(), "git", "diff"))
   435  	FailOnErr(Run(repoDirectory(), "git", "commit", "-am", "patch"))
   436  }
   437  
   438  func Delete(path string) {
   439  
   440  	log.WithFields(log.Fields{"path": path}).Info("deleting")
   441  
   442  	CheckError(os.Remove(filepath.Join(repoDirectory(), path)))
   443  
   444  	FailOnErr(Run(repoDirectory(), "git", "diff"))
   445  	FailOnErr(Run(repoDirectory(), "git", "commit", "-am", "delete"))
   446  }
   447  
   448  func WriteFile(path, contents string) {
   449  	log.WithFields(log.Fields{"path": path}).Info("adding")
   450  
   451  	CheckError(ioutil.WriteFile(filepath.Join(repoDirectory(), path), []byte(contents), 0644))
   452  }
   453  
   454  func AddFile(path, contents string) {
   455  
   456  	WriteFile(path, contents)
   457  
   458  	FailOnErr(Run(repoDirectory(), "git", "diff"))
   459  	FailOnErr(Run(repoDirectory(), "git", "add", "."))
   460  	FailOnErr(Run(repoDirectory(), "git", "commit", "-am", "add file"))
   461  }
   462  
   463  func AddSignedFile(path, contents string) {
   464  	WriteFile(path, contents)
   465  
   466  	prevGnuPGHome := os.Getenv("GNUPGHOME")
   467  	os.Setenv("GNUPGHOME", TmpDir+"/gpg")
   468  	FailOnErr(Run(repoDirectory(), "git", "diff"))
   469  	FailOnErr(Run(repoDirectory(), "git", "add", "."))
   470  	FailOnErr(Run(repoDirectory(), "git", "-c", fmt.Sprintf("user.signingkey=%s", GpgGoodKeyID), "commit", "-S", "-am", "add file"))
   471  	os.Setenv("GNUPGHOME", prevGnuPGHome)
   472  }
   473  
   474  // create the resource by creating using "kubectl apply", with bonus templating
   475  func Declarative(filename string, values interface{}) (string, error) {
   476  
   477  	bytes, err := ioutil.ReadFile(path.Join("testdata", filename))
   478  	CheckError(err)
   479  
   480  	tmpFile, err := ioutil.TempFile("", "")
   481  	CheckError(err)
   482  	_, err = tmpFile.WriteString(Tmpl(string(bytes), values))
   483  	CheckError(err)
   484  
   485  	return Run("", "kubectl", "-n", ArgoCDNamespace, "apply", "-f", tmpFile.Name())
   486  }
   487  
   488  func CreateSubmoduleRepos(repoType string) {
   489  
   490  	// set-up submodule repo
   491  	FailOnErr(Run("", "cp", "-Rf", "testdata/git-submodule/", submoduleDirectory()))
   492  	FailOnErr(Run(submoduleDirectory(), "chmod", "777", "."))
   493  	FailOnErr(Run(submoduleDirectory(), "git", "init"))
   494  	FailOnErr(Run(submoduleDirectory(), "git", "add", "."))
   495  	FailOnErr(Run(submoduleDirectory(), "git", "commit", "-q", "-m", "initial commit"))
   496  
   497  	// set-up submodule parent repo
   498  	FailOnErr(Run("", "mkdir", submoduleParentDirectory()))
   499  	FailOnErr(Run(submoduleParentDirectory(), "chmod", "777", "."))
   500  	FailOnErr(Run(submoduleParentDirectory(), "git", "init"))
   501  	FailOnErr(Run(submoduleParentDirectory(), "git", "add", "."))
   502  	FailOnErr(Run(submoduleParentDirectory(), "git", "submodule", "add", "-b", "master", "../submodule.git", "submodule/test"))
   503  	if repoType == "ssh" {
   504  		FailOnErr(Run(submoduleParentDirectory(), "git", "config", "--file=.gitmodules", "submodule.submodule/test.url", RepoURL(RepoURLTypeSSHSubmodule)))
   505  	} else if repoType == "https" {
   506  		FailOnErr(Run(submoduleParentDirectory(), "git", "config", "--file=.gitmodules", "submodule.submodule/test.url", RepoURL(RepoURLTypeHTTPSSubmodule)))
   507  	}
   508  	FailOnErr(Run(submoduleParentDirectory(), "git", "add", "--all"))
   509  	FailOnErr(Run(submoduleParentDirectory(), "git", "commit", "-q", "-m", "commit with submodule"))
   510  }