mirror of https://github.com/fluxcd/flux2.git
				
				
				
			Implement GitHub repository bootstrap
							parent
							
								
									9e31bbe716
								
							
						
					
					
						commit
						4ec5b7fc69
					
				@ -0,0 +1,20 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var bootstrapCmd = &cobra.Command{
 | 
			
		||||
	Use:   "bootstrap",
 | 
			
		||||
	Short: "Bootstrap commands",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	bootstrapVersion string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	bootstrapCmd.PersistentFlags().StringVar(&bootstrapVersion, "version", "master", "toolkit tag or branch")
 | 
			
		||||
 | 
			
		||||
	rootCmd.AddCommand(bootstrapCmd)
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,364 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"sigs.k8s.io/yaml"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing/object"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing/transport/http"
 | 
			
		||||
	"github.com/google/go-github/v32/github"
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
	corev1 "k8s.io/api/core/v1"
 | 
			
		||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/wait"
 | 
			
		||||
 | 
			
		||||
	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
 | 
			
		||||
	sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var bootstrapGitHubCmd = &cobra.Command{
 | 
			
		||||
	Use:   "github",
 | 
			
		||||
	Short: "Bootstrap GitHub repository",
 | 
			
		||||
	RunE:  bootstrapGitHubCmdRun,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ghOwner      string
 | 
			
		||||
	ghRepository string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const ghTokenName = "GITHUB_TOKEN"
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	bootstrapGitHubCmd.Flags().StringVar(&ghOwner, "owner", "", "GitHub user or organization name")
 | 
			
		||||
	bootstrapGitHubCmd.Flags().StringVar(&ghRepository, "repository", "", "GitHub repository name")
 | 
			
		||||
	bootstrapCmd.AddCommand(bootstrapGitHubCmd)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 | 
			
		||||
	ghToken := os.Getenv(ghTokenName)
 | 
			
		||||
	ghURL := fmt.Sprintf("https://github.com/%s/%s", ghOwner, ghRepository)
 | 
			
		||||
	if ghToken == "" {
 | 
			
		||||
		return fmt.Errorf("%s environment variable not found", ghTokenName)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ghOwner == "" || ghRepository == "" {
 | 
			
		||||
		return fmt.Errorf("owner and repository are required")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmpDir, err := ioutil.TempDir("", namespace)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer os.RemoveAll(tmpDir)
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	// create GitHub repository if doesn't exists
 | 
			
		||||
	logAction("connecting to GitHub")
 | 
			
		||||
	if err := initGitHubRepository(ctx, ghOwner, ghRepository, ghToken); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// clone repository and checkout the master branch
 | 
			
		||||
	repo, err := checkoutGitHubRepository(ctx, tmpDir, ghURL, ghToken)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	w, err := repo.Worktree()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	logSuccess("repository cloned")
 | 
			
		||||
 | 
			
		||||
	// generate install manifests
 | 
			
		||||
	logGenerate("generating manifests")
 | 
			
		||||
	kDir := path.Join(tmpDir, ".kustomization")
 | 
			
		||||
	if err := os.MkdirAll(kDir, os.ModePerm); err != nil {
 | 
			
		||||
		return fmt.Errorf("generating manifests failed: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := genInstallManifests(bootstrapVersion, namespace, components, kDir); err != nil {
 | 
			
		||||
		return fmt.Errorf("generating manifests failed: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	manifestsDir := path.Join(tmpDir, namespace)
 | 
			
		||||
	if err := os.MkdirAll(manifestsDir, os.ModePerm); err != nil {
 | 
			
		||||
		return fmt.Errorf("generating manifests failed: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	manifest := path.Join(manifestsDir, "toolkit.yaml")
 | 
			
		||||
	if err := buildKustomization(kDir, manifest); err != nil {
 | 
			
		||||
		return fmt.Errorf("build kustomization failed: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	os.RemoveAll(kDir)
 | 
			
		||||
 | 
			
		||||
	// stage install manifests
 | 
			
		||||
	_, err = w.Add(fmt.Sprintf("%s/toolkit.yaml", namespace))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	status, err := w.Status()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	isInstall := !status.IsClean()
 | 
			
		||||
 | 
			
		||||
	if isInstall {
 | 
			
		||||
		// commit install manifests
 | 
			
		||||
		logGenerate("pushing manifests")
 | 
			
		||||
		_, err = w.Commit("Add bootstrap manifests", &git.CommitOptions{
 | 
			
		||||
			Author: &object.Signature{
 | 
			
		||||
				Name:  "tk",
 | 
			
		||||
				Email: "tk@@users.noreply.github.com",
 | 
			
		||||
				When:  time.Now(),
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// push install manifests
 | 
			
		||||
		if err := pushGitHubRepository(ctx, repo, ghToken); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		logSuccess("manifests pushed")
 | 
			
		||||
 | 
			
		||||
		// apply install manifests
 | 
			
		||||
		logAction("installing components in %s namespace", namespace)
 | 
			
		||||
		command := fmt.Sprintf("cat %s | kubectl apply -f-", manifest)
 | 
			
		||||
		if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
 | 
			
		||||
			return fmt.Errorf("install failed")
 | 
			
		||||
		}
 | 
			
		||||
		logSuccess("install completed")
 | 
			
		||||
 | 
			
		||||
		// check rollout status
 | 
			
		||||
		logWaiting("verifying installation")
 | 
			
		||||
		for _, deployment := range components {
 | 
			
		||||
			command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=%s",
 | 
			
		||||
				namespace, deployment, timeout.String())
 | 
			
		||||
			if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
 | 
			
		||||
				return fmt.Errorf("install failed")
 | 
			
		||||
			} else {
 | 
			
		||||
				logSuccess("%s ready", deployment)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// create or update auth secret
 | 
			
		||||
	if err := generateBasicAuth(ctx, namespace, namespace, "git", ghToken); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	logSuccess("authentication configured")
 | 
			
		||||
 | 
			
		||||
	// generate and push source kustomization
 | 
			
		||||
	if isInstall {
 | 
			
		||||
		logAction("generating kustomization manifests")
 | 
			
		||||
		if err := generateGitHubKustomization(ghURL, namespace, namespace, tmpDir); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// stage manifests
 | 
			
		||||
		_, err = w.Add(fmt.Sprintf("%s", namespace))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// commit manifests
 | 
			
		||||
		logAction("pushing kustomization manifests")
 | 
			
		||||
		_, err = w.Commit("Add kustomization manifests", &git.CommitOptions{
 | 
			
		||||
			Author: &object.Signature{
 | 
			
		||||
				Name:  "tk",
 | 
			
		||||
				Email: "tk@@users.noreply.github.com",
 | 
			
		||||
				When:  time.Now(),
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// push install manifests
 | 
			
		||||
		if err := pushGitHubRepository(ctx, repo, ghToken); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		logSuccess("kustomization manifests pushed")
 | 
			
		||||
 | 
			
		||||
		logAction("applying kustomization manifests")
 | 
			
		||||
		if err := applyGitHubKustomization(ctx, namespace, namespace, tmpDir); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logSuccess("bootstrap finished")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initGitHubRepository(ctx context.Context, owner, name, token string) error {
 | 
			
		||||
	isPrivate := true
 | 
			
		||||
	isAutoInit := true
 | 
			
		||||
	auth := github.BasicAuthTransport{
 | 
			
		||||
		Username: "git",
 | 
			
		||||
		Password: token,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := github.NewClient(auth.Client())
 | 
			
		||||
	_, _, err := client.Repositories.Create(ctx, owner, &github.Repository{
 | 
			
		||||
		AutoInit: &isAutoInit,
 | 
			
		||||
		Name:     &name,
 | 
			
		||||
		Private:  &isPrivate,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if !strings.Contains(err.Error(), "name already exists on this account") {
 | 
			
		||||
			return fmt.Errorf("github create repository error: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		logSuccess("repository created")
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkoutGitHubRepository(ctx context.Context, path, url, token string) (*git.Repository, error) {
 | 
			
		||||
	branch := "master"
 | 
			
		||||
	auth := &http.BasicAuth{
 | 
			
		||||
		Username: "git",
 | 
			
		||||
		Password: token,
 | 
			
		||||
	}
 | 
			
		||||
	repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
 | 
			
		||||
		URL:           url,
 | 
			
		||||
		Auth:          auth,
 | 
			
		||||
		RemoteName:    git.DefaultRemoteName,
 | 
			
		||||
		ReferenceName: plumbing.NewBranchReferenceName(branch),
 | 
			
		||||
		SingleBranch:  true,
 | 
			
		||||
		NoCheckout:    false,
 | 
			
		||||
		Progress:      nil,
 | 
			
		||||
		Tags:          git.NoTags,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("git clone error: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = repo.Head()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("git resolve HEAD error: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return repo, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func pushGitHubRepository(ctx context.Context, repo *git.Repository, token string) error {
 | 
			
		||||
	auth := &http.BasicAuth{
 | 
			
		||||
		Username: "git",
 | 
			
		||||
		Password: token,
 | 
			
		||||
	}
 | 
			
		||||
	err := repo.PushContext(ctx, &git.PushOptions{
 | 
			
		||||
		Auth:     auth,
 | 
			
		||||
		Progress: nil,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("git push error: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func generateGitHubKustomization(url, name, namespace, tmpDir string) error {
 | 
			
		||||
	gvk := sourcev1.GroupVersion.WithKind("GitRepository")
 | 
			
		||||
	gitRepository := sourcev1.GitRepository{
 | 
			
		||||
		TypeMeta: metav1.TypeMeta{
 | 
			
		||||
			Kind:       gvk.Kind,
 | 
			
		||||
			APIVersion: gvk.GroupVersion().String(),
 | 
			
		||||
		},
 | 
			
		||||
		ObjectMeta: metav1.ObjectMeta{
 | 
			
		||||
			Name:      name,
 | 
			
		||||
			Namespace: namespace,
 | 
			
		||||
		},
 | 
			
		||||
		Spec: sourcev1.GitRepositorySpec{
 | 
			
		||||
			URL: url,
 | 
			
		||||
			Interval: metav1.Duration{
 | 
			
		||||
				Duration: 1 * time.Minute,
 | 
			
		||||
			},
 | 
			
		||||
			Reference: &sourcev1.GitRepositoryRef{
 | 
			
		||||
				Branch: "master",
 | 
			
		||||
			},
 | 
			
		||||
			SecretRef: &corev1.LocalObjectReference{
 | 
			
		||||
				Name: name,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gitData, err := yaml.Marshal(gitRepository)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := utils.writeFile(string(gitData), filepath.Join(tmpDir, namespace, "toolkit-source.yaml")); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gvk = kustomizev1.GroupVersion.WithKind("Kustomization")
 | 
			
		||||
	emptyAPIGroup := ""
 | 
			
		||||
	kustomization := kustomizev1.Kustomization{
 | 
			
		||||
		TypeMeta: metav1.TypeMeta{
 | 
			
		||||
			Kind:       gvk.Kind,
 | 
			
		||||
			APIVersion: gvk.GroupVersion().String(),
 | 
			
		||||
		},
 | 
			
		||||
		ObjectMeta: metav1.ObjectMeta{
 | 
			
		||||
			Name:      name,
 | 
			
		||||
			Namespace: namespace,
 | 
			
		||||
		},
 | 
			
		||||
		Spec: kustomizev1.KustomizationSpec{
 | 
			
		||||
			Interval: metav1.Duration{
 | 
			
		||||
				Duration: 10 * time.Minute,
 | 
			
		||||
			},
 | 
			
		||||
			Path:  "./",
 | 
			
		||||
			Prune: true,
 | 
			
		||||
			SourceRef: corev1.TypedLocalObjectReference{
 | 
			
		||||
				APIGroup: &emptyAPIGroup,
 | 
			
		||||
				Kind:     "GitRepository",
 | 
			
		||||
				Name:     name,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ksData, err := yaml.Marshal(kustomization)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := utils.writeFile(string(ksData), filepath.Join(tmpDir, namespace, "toolkit-kustomization.yaml")); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func applyGitHubKustomization(ctx context.Context, name, namespace, tmpDir string) error {
 | 
			
		||||
	kubeClient, err := utils.kubeClient(kubeconfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	command := fmt.Sprintf("kubectl apply -f %s", filepath.Join(tmpDir, namespace))
 | 
			
		||||
	if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logWaiting("waiting for kustomization sync")
 | 
			
		||||
	if err := wait.PollImmediate(pollInterval, timeout,
 | 
			
		||||
		isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue