Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CR-11473 bootstrap recovery from repo #296

Merged
merged 16 commits into from
May 8, 2022
49 changes: 33 additions & 16 deletions cmd/commands/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type (
DryRun bool
HidePassword bool
Insecure bool
Recover bool
Timeout time.Duration
KubeFactory kube.Factory
CloneOptions *git.CloneOptions
Expand Down Expand Up @@ -113,6 +114,7 @@ func NewRepoBootstrapCommand() *cobra.Command {
dryRun bool
hidePassword bool
insecure bool
recover bool
installationMode string
cloneOpts *git.CloneOptions
f kube.Factory
Expand Down Expand Up @@ -153,6 +155,7 @@ func NewRepoBootstrapCommand() *cobra.Command {
DryRun: dryRun,
HidePassword: hidePassword,
Insecure: insecure,
Recover: recover,
Timeout: util.MustParseDuration(cmd.Flag("request-timeout").Value.String()),
KubeFactory: f,
CloneOptions: cloneOpts,
Expand All @@ -164,6 +167,7 @@ func NewRepoBootstrapCommand() *cobra.Command {
cmd.Flags().StringVar(&appSpecifier, "app", "", "The application specifier (e.g. github.com/argoproj-labs/argocd-autopilot/manifests?ref=v0.2.5), overrides the default installation argo-cd manifests")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "If true, print manifests instead of applying them to the cluster (nothing will be commited to git)")
cmd.Flags().BoolVar(&hidePassword, "hide-password", false, "If true, will not print initial argo cd password")
cmd.Flags().BoolVar(&recover, "recover", false, "Installs argo-cd and associates it with an existing repo. Used for recovery after cluster failure. Use it with --app flag to provide the installation manifests from the existing repo ( e.g. github.com/git-user/repo-name/bootstrap/argo-cd ), otherwise it will be installed from the default installation manifests")
rotem-codefresh marked this conversation as resolved.
Show resolved Hide resolved
cmd.Flags().BoolVar(&insecure, "insecure", false, "Run Argo-CD server without TLS")
cmd.Flags().StringToStringVar(&namespaceLabels, "namespace-labels", nil, "Optional labels that will be set on the namespace resource. (e.g. \"key1=value1,key2=value2\"")
cmd.Flags().StringVar(&installationMode, "installation-mode", "normal", "One of: normal|flat. "+
Expand Down Expand Up @@ -230,10 +234,11 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error {
}

log.G(ctx).Infof("using revision: \"%s\", installation path: \"%s\"", opts.CloneOptions.Revision(), opts.CloneOptions.Path())
if err = validateRepo(repofs); err != nil {
err = validateRepo(repofs, opts.Recover)
if err != nil{
return err
}

}
log.G(ctx).Debug("repository is ok")

// apply built manifest to k8s cluster
Expand All @@ -243,9 +248,11 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error {
return fmt.Errorf("failed to apply bootstrap manifests to cluster: %w", err)
}

// write argocd manifests to repo
if err = writeManifestsToRepo(repofs, manifests, opts.InstallationMode, opts.Namespace); err != nil {
return fmt.Errorf("failed to write manifests to repo: %w", err)
if !opts.Recover {
// write argocd manifests to repo
if err = writeManifestsToRepo(repofs, manifests, opts.InstallationMode, opts.Namespace); err != nil {
return fmt.Errorf("failed to write manifests to repo: %w", err)
}
}

// wait for argocd to be ready before applying argocd-apps
Expand All @@ -258,15 +265,17 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error {

stop()

// push results to repo
log.G(ctx).Infof("pushing bootstrap manifests to repo")
commitMsg := "Autopilot Bootstrap"
if opts.CloneOptions.Path() != "" {
commitMsg = "Autopilot Bootstrap at " + opts.CloneOptions.Path()
}
if !opts.Recover {
// push results to repo
log.G(ctx).Infof("pushing bootstrap manifests to repo")
commitMsg := "Autopilot Bootstrap"
if opts.CloneOptions.Path() != "" {
commitMsg = "Autopilot Bootstrap at " + opts.CloneOptions.Path()
}

if _, err = r.Persist(ctx, &git.PushOptions{CommitMsg: commitMsg}); err != nil {
return err
if _, err = r.Persist(ctx, &git.PushOptions{CommitMsg: commitMsg}); err != nil {
return err
}
}

// apply "Argo-CD" Application that references "bootstrap/argo-cd"
Expand Down Expand Up @@ -459,11 +468,19 @@ func setBootstrapOptsDefaults(opts RepoBootstrapOptions) (*RepoBootstrapOptions,
return &opts, nil
}

func validateRepo(repofs fs.FS) error {
func validateRepo(repofs fs.FS, recover bool) error {
folders := []string{store.Default.BootsrtrapDir, store.Default.ProjectsDir}
for _, folder := range folders {
if repofs.ExistsOrDie(folder) {
return fmt.Errorf("folder %s already exist in: %s", folder, repofs.Join(repofs.Root(), folder))
if recover {
continue
} else {
return fmt.Errorf("folder %s already exist in: %s", folder, repofs.Join(repofs.Root(), folder))
}
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else if recover

if recover {
return fmt.Errorf("recovery failed: invalid repository, %s directory is missing in %s", folder, repofs.Root())
}
}
}

Expand Down
76 changes: 75 additions & 1 deletion cmd/commands/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func Test_validateRepo(t *testing.T) {
tt.preFn(t, repofs)
}

if err := validateRepo(repofs); err != nil {
if err := validateRepo(repofs, false); err != nil {
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
} else {
Expand Down Expand Up @@ -414,6 +414,80 @@ func TestRunRepoBootstrap(t *testing.T) {
}
}

func TestRunRepoBootstrapRecovery(t *testing.T) {
exitCalled := false
tests := map[string]struct {
opts *RepoBootstrapOptions
beforeFn func(*gitmocks.MockRepository, *kubemocks.MockFactory)
assertFn func(*testing.T, fs.FS, error)
}{
"Recovery installation": {
opts: &RepoBootstrapOptions{
InstallationMode: installationModeNormal,
Namespace: "bar",
Recover: true,
CloneOptions: &git.CloneOptions{
Repo: "https://github.com/foo/bar/installation1?ref=main",
Auth: git.Auth{Password: "test"},
},
},
beforeFn: func(r *gitmocks.MockRepository, f *kubemocks.MockFactory) {
mockCS := fake.NewSimpleClientset(&v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-initial-admin-secret",
Namespace: "bar",
},
Data: map[string][]byte{
"password": []byte("foo"),
},
})
f.EXPECT().Apply(gomock.Any(), gomock.Any()).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also assert that repo persist is not called.

Times(2).
Return(nil)
f.EXPECT().Wait(gomock.Any(), gomock.Any()).Return(nil)
f.EXPECT().KubernetesClientSetOrDie().Return(mockCS)
},
assertFn: func(t *testing.T, repofs fs.FS, ret error) {
assert.NoError(t, ret)
assert.False(t, exitCalled)
},
},
}

origExit, origGetRepo, origRunKustomizeBuild, origArgoLogin := exit, getRepo, runKustomizeBuild, argocdLogin
defer func() {
exit = origExit
getRepo = origGetRepo
runKustomizeBuild = origRunKustomizeBuild
argocdLogin = origArgoLogin
}()
exit = func(_ int) { exitCalled = true }
runKustomizeBuild = func(k *kusttypes.Kustomization) ([]byte, error) { return []byte("test"), nil }
argocdLogin = func(opts *argocd.LoginOptions) error { return nil }

for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
ctrl := gomock.NewController(t)
r := gitmocks.NewMockRepository(ctrl)
f := kubemocks.NewMockFactory(ctrl)
repofs := fs.Create(memfs.New())
_ = repofs.MkdirAll("bootstrap", 0666)
_ = repofs.MkdirAll("projects", 0666)

exitCalled = false

tt.beforeFn(r, f)
tt.opts.KubeFactory = f
getRepo = func(_ context.Context, _ *git.CloneOptions) (git.Repository, fs.FS, error) {
return r, repofs, nil
}

err := RunRepoBootstrap(context.Background(), tt.opts)
tt.assertFn(t, repofs, err)
})
}
}

func Test_setUninstallOptsDefaults(t *testing.T) {
tests := map[string]struct {
opts RepoUninstallOptions
Expand Down
1 change: 1 addition & 0 deletions docs/commands/argocd-autopilot_repo_bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ argocd-autopilot repo bootstrap [flags]
-n, --namespace string If present, the namespace scope for this CLI request
--namespace-labels stringToString Optional labels that will be set on the namespace resource. (e.g. "key1=value1,key2=value2" (default [])
--provider string The git provider, one of: azure|gitea|github|gitlab
--recover Installs argo-cd and associates it with an existing repo. Used for recovery after cluster failure. Use it with --app flag to provide the installation manifests from the existing repo ( e.g. github.com/git-user/repo-name/bootstrap/argo-cd ), otherwise it will be installed from the default installation manifests
--repo string Repository URL [GIT_REPO]
--request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")
-b, --upsert-branch If true will try to checkout the specified branch and create it if it doesn't exist
Expand Down