Use git package for bootstrap

pull/42/head
stefanprodan 5 years ago
parent bd781bbcfb
commit d0a79c2b4c

@ -1,7 +1,25 @@
package main package main
import ( import (
"context"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"sigs.k8s.io/yaml"
"strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
) )
var bootstrapCmd = &cobra.Command{ var bootstrapCmd = &cobra.Command{
@ -13,8 +31,203 @@ var (
bootstrapVersion string bootstrapVersion string
) )
const (
bootstrapBranch = "master"
bootstrapInstallManifest = "toolkit-components.yaml"
bootstrapSourceManifest = "toolkit-source.yaml"
bootstrapKustomizationManifest = "toolkit-kustomization.yaml"
)
func init() { func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapVersion, "version", "master", "toolkit tag or branch") bootstrapCmd.PersistentFlags().StringVar(&bootstrapVersion, "version", "master", "toolkit tag or branch")
rootCmd.AddCommand(bootstrapCmd) rootCmd.AddCommand(bootstrapCmd)
} }
func generateInstallManifests(targetPath, namespace, tmpDir string) (string, error) {
tkDir := path.Join(tmpDir, ".tk")
defer os.RemoveAll(tkDir)
if err := os.MkdirAll(tkDir, os.ModePerm); err != nil {
return "", fmt.Errorf("generating manifests failed: %w", err)
}
if err := genInstallManifests(bootstrapVersion, namespace, components, tkDir); err != nil {
return "", fmt.Errorf("generating manifests failed: %w", err)
}
manifestsDir := path.Join(tmpDir, targetPath, namespace)
if err := os.MkdirAll(manifestsDir, os.ModePerm); err != nil {
return "", fmt.Errorf("generating manifests failed: %w", err)
}
manifest := path.Join(manifestsDir, bootstrapInstallManifest)
if err := buildKustomization(tkDir, manifest); err != nil {
return "", fmt.Errorf("build kustomization failed: %w", err)
}
return manifest, nil
}
func applyInstallManifests(ctx context.Context, manifestPath string, components []string) error {
command := fmt.Sprintf("kubectl apply -f %s", manifestPath)
if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
return fmt.Errorf("install failed")
}
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")
}
}
return nil
}
func generateSyncManifests(url, name, namespace, targetPath, tmpDir string, interval time.Duration) 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: interval,
},
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, targetPath, namespace, bootstrapSourceManifest)); 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: fmt.Sprintf("./%s", strings.TrimPrefix(targetPath, "./")),
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, targetPath, namespace, bootstrapKustomizationManifest)); err != nil {
return err
}
return nil
}
func applySyncManifests(ctx context.Context, kubeClient client.Client, name, namespace, targetPath, tmpDir string) error {
command := fmt.Sprintf("kubectl apply -f %s", filepath.Join(tmpDir, targetPath, namespace))
if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
return err
}
logWaiting("waiting for cluster sync")
if err := wait.PollImmediate(pollInterval, timeout,
isGitRepositoryReady(ctx, kubeClient, name, namespace)); err != nil {
return err
}
if err := wait.PollImmediate(pollInterval, timeout,
isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil {
return err
}
return nil
}
func shouldInstallManifests(ctx context.Context, kubeClient client.Client, namespace string) bool {
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: namespace,
}
var kustomization kustomizev1.Kustomization
if err := kubeClient.Get(ctx, namespacedName, &kustomization); err != nil {
return true
}
return kustomization.Status.LastAppliedRevision == ""
}
func shouldCreateDeployKey(ctx context.Context, kubeClient client.Client, namespace string) bool {
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: namespace,
}
var existing corev1.Secret
if err := kubeClient.Get(ctx, namespacedName, &existing); err != nil {
return true
}
return false
}
func generateDeployKey(ctx context.Context, kubeClient client.Client, url *url.URL, namespace string) (string, error) {
pair, err := generateKeyPair(ctx)
if err != nil {
return "", err
}
hostKey, err := scanHostKey(ctx, url)
if err != nil {
return "", err
}
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
Namespace: namespace,
},
StringData: map[string]string{
"identity": string(pair.PrivateKey),
"identity.pub": string(pair.PublicKey),
"known_hosts": string(hostKey),
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return "", err
}
return string(pair.PublicKey), nil
}

@ -7,25 +7,11 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"path/filepath"
"sigs.k8s.io/yaml"
"strings"
"time" "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" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/fluxcd/toolkit/pkg/git"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
) )
var bootstrapGitHubCmd = &cobra.Command{ var bootstrapGitHubCmd = &cobra.Command{
@ -70,13 +56,7 @@ var (
) )
const ( const (
ghTokenName = "GITHUB_TOKEN" ghDefaultPermission = "maintain"
ghBranch = "master"
ghInstallManifest = "toolkit-components.yaml"
ghSourceManifest = "toolkit-source.yaml"
ghKustomizationManifest = "toolkit-kustomization.yaml"
ghDefaultHostname = "github.com"
ghDefaultPermission = "maintain"
) )
func init() { func init() {
@ -86,22 +66,26 @@ func init() {
bootstrapGitHubCmd.Flags().BoolVar(&ghPersonal, "personal", false, "is personal repository") bootstrapGitHubCmd.Flags().BoolVar(&ghPersonal, "personal", false, "is personal repository")
bootstrapGitHubCmd.Flags().BoolVar(&ghPrivate, "private", true, "is private repository") bootstrapGitHubCmd.Flags().BoolVar(&ghPrivate, "private", true, "is private repository")
bootstrapGitHubCmd.Flags().DurationVar(&ghInterval, "interval", time.Minute, "sync interval") bootstrapGitHubCmd.Flags().DurationVar(&ghInterval, "interval", time.Minute, "sync interval")
bootstrapGitHubCmd.Flags().StringVar(&ghHostname, "hostname", ghDefaultHostname, "GitHub hostname") bootstrapGitHubCmd.Flags().StringVar(&ghHostname, "hostname", git.GitHubDefaultHostname, "GitHub hostname")
bootstrapGitHubCmd.Flags().StringVar(&ghPath, "path", "", "repository path, when specified the cluster sync will be scoped to this path") bootstrapGitHubCmd.Flags().StringVar(&ghPath, "path", "", "repository path, when specified the cluster sync will be scoped to this path")
bootstrapCmd.AddCommand(bootstrapGitHubCmd) bootstrapCmd.AddCommand(bootstrapGitHubCmd)
} }
func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
ghToken := os.Getenv(ghTokenName) ghToken := os.Getenv(git.GitHubTokenName)
if ghToken == "" { if ghToken == "" {
return fmt.Errorf("%s environment variable not found", ghTokenName) return fmt.Errorf("%s environment variable not found", git.GitHubTokenName)
}
repository, err := git.NewRepository(ghRepository, ghOwner, ghHostname, ghToken, "tk", "tk@users.noreply.github.com")
if err != nil {
return err
} }
ghURL := fmt.Sprintf("https://%s/%s/%s", ghHostname, ghOwner, ghRepository) provider := &git.GithubProvider{
sshURL := fmt.Sprintf("ssh://git@%s/%s/%s", ghHostname, ghOwner, ghRepository) IsPrivate: ghPrivate,
if ghOwner == "" || ghRepository == "" { IsPersonal: ghPersonal,
return fmt.Errorf("owner and repository are required")
} }
kubeClient, err := utils.kubeClient(kubeconfig) kubeClient, err := utils.kubeClient(kubeconfig)
@ -120,46 +104,49 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
// create GitHub repository if doesn't exists // create GitHub repository if doesn't exists
logAction("connecting to %s", ghHostname) logAction("connecting to %s", ghHostname)
if err := createGitHubRepository(ctx, ghHostname, ghOwner, ghRepository, ghToken, ghPrivate, ghPersonal); err != nil { changed, err := provider.CreateRepository(ctx, repository)
if err != nil {
return err return err
} }
if changed {
logSuccess("repository created")
}
withErrors := false withErrors := false
// add teams to org repository // add teams to org repository
if !ghPersonal { if !ghPersonal {
for _, team := range ghTeams { for _, team := range ghTeams {
if err := addGitHubTeam(ctx, ghHostname, ghOwner, ghRepository, ghToken, team, ghDefaultPermission); err != nil { if changed, err := provider.AddTeam(ctx, repository, team, ghDefaultPermission); err != nil {
logFailure(err.Error()) logFailure(err.Error())
withErrors = true withErrors = true
} else { } else if changed {
logSuccess("%s team access granted", team) logSuccess("%s team access granted", team)
} }
} }
} }
// clone repository and checkout the master branch // clone repository and checkout the master branch
repo, err := checkoutGitHubRepository(ctx, ghURL, ghBranch, ghToken, tmpDir) if err := repository.Checkout(ctx, bootstrapBranch, tmpDir); err != nil {
if err != nil {
return err return err
} }
logSuccess("repository cloned") logSuccess("repository cloned")
// generate install manifests // generate install manifests
logGenerate("generating manifests") logGenerate("generating manifests")
manifest, err := generateGitHubInstall(ghPath, namespace, tmpDir) manifest, err := generateInstallManifests(ghPath, namespace, tmpDir)
if err != nil { if err != nil {
return err return err
} }
// stage install manifests // stage install manifests
changed, err := commitGitHubManifests(repo, ghPath, namespace) changed, err = repository.Commit(ctx, path.Join(ghPath, namespace), "Add manifests")
if err != nil { if err != nil {
return err return err
} }
// push install manifests // push install manifests
if changed { if changed {
if err := pushGitHubRepository(ctx, repo, ghToken); err != nil { if err := repository.Push(ctx); err != nil {
return err return err
} }
logSuccess("components manifests pushed") logSuccess("components manifests pushed")
@ -168,74 +155,63 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
} }
// determine if repo synchronization is working // determine if repo synchronization is working
isInstall := shouldInstallGitHub(ctx, kubeClient, namespace) isInstall := shouldInstallManifests(ctx, kubeClient, namespace)
if isInstall { if isInstall {
// apply install manifests // apply install manifests
logAction("installing components in %s namespace", namespace) logAction("installing components in %s namespace", namespace)
command := fmt.Sprintf("kubectl apply -f %s", manifest) if err := applyInstallManifests(ctx, manifest, components); err != nil {
if _, err := utils.execCommand(ctx, ModeOS, command); err != nil { return err
return fmt.Errorf("install failed")
} }
logSuccess("install completed") logSuccess("install completed")
// check installation
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)
}
}
} }
// setup SSH deploy key // setup SSH deploy key
if shouldCreateGitHubDeployKey(ctx, kubeClient, namespace) { if shouldCreateDeployKey(ctx, kubeClient, namespace) {
logAction("configuring deploy key") logAction("configuring deploy key")
u, err := url.Parse(sshURL) u, err := url.Parse(repository.GetSSH())
if err != nil { if err != nil {
return fmt.Errorf("git URL parse failed: %w", err) return fmt.Errorf("git URL parse failed: %w", err)
} }
key, err := generateGitHubDeployKey(ctx, kubeClient, u, namespace) key, err := generateDeployKey(ctx, kubeClient, u, namespace)
if err != nil { if err != nil {
return fmt.Errorf("generating deploy key failed: %w", err) return fmt.Errorf("generating deploy key failed: %w", err)
} }
if err := createGitHubDeployKey(ctx, key, ghHostname, ghOwner, ghRepository, ghPath, ghToken); err != nil { keyName := "tk"
if ghPath != "" {
keyName = fmt.Sprintf("tk-%s", ghPath)
}
if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil {
return err return err
} else if changed {
logSuccess("deploy key configured")
} }
logSuccess("deploy key configured")
} }
// configure repo synchronization // configure repo synchronization
if isInstall { if isInstall {
// generate source and kustomization manifests // generate source and kustomization manifests
logAction("generating sync manifests") logAction("generating sync manifests")
if err := generateGitHubKustomization(sshURL, namespace, namespace, ghPath, tmpDir, ghInterval); err != nil { if err := generateSyncManifests(repository.GetSSH(), namespace, namespace, ghPath, tmpDir, ghInterval); err != nil {
return err return err
} }
// stage manifests // commit and push manifests
changed, err = commitGitHubManifests(repo, ghPath, namespace) if changed, err = repository.Commit(ctx, path.Join(ghPath, namespace), "Add manifests"); err != nil {
if err != nil {
return err return err
} } else if changed {
if err := repository.Push(ctx); err != nil {
// push manifests
if changed {
if err := pushGitHubRepository(ctx, repo, ghToken); err != nil {
return err return err
} }
logSuccess("sync manifests pushed")
} }
logSuccess("sync manifests pushed")
// apply manifests and waiting for sync // apply manifests and waiting for sync
logAction("applying sync manifests") logAction("applying sync manifests")
if err := applyGitHubKustomization(ctx, kubeClient, namespace, namespace, ghPath, tmpDir); err != nil { if err := applySyncManifests(ctx, kubeClient, namespace, namespace, ghPath, tmpDir); err != nil {
return err return err
} }
} }
@ -247,390 +223,3 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
logSuccess("bootstrap finished") logSuccess("bootstrap finished")
return nil return nil
} }
func makeGitHubClient(hostname, token string) (*github.Client, error) {
auth := github.BasicAuthTransport{
Username: "git",
Password: token,
}
gh := github.NewClient(auth.Client())
if hostname != ghDefaultHostname {
baseURL := fmt.Sprintf("https://%s/api/v3/", hostname)
uploadURL := fmt.Sprintf("https://%s/api/uploads/", hostname)
if g, err := github.NewEnterpriseClient(baseURL, uploadURL, auth.Client()); err == nil {
gh = g
} else {
return nil, fmt.Errorf("github client error: %w", err)
}
}
return gh, nil
}
func createGitHubRepository(ctx context.Context, hostname, owner, name, token string, isPrivate, isPersonal bool) error {
gh, err := makeGitHubClient(hostname, token)
if err != nil {
return err
}
org := ""
if !isPersonal {
org = owner
}
if _, _, err := gh.Repositories.Get(ctx, org, name); err == nil {
return nil
}
autoInit := true
_, _, err = gh.Repositories.Create(ctx, org, &github.Repository{
AutoInit: &autoInit,
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 addGitHubTeam(ctx context.Context, hostname, owner, repository, token string, teamSlug, permission string) error {
gh, err := makeGitHubClient(hostname, token)
if err != nil {
return err
}
// check team exists
_, _, err = gh.Teams.GetTeamBySlug(ctx, owner, teamSlug)
if err != nil {
return fmt.Errorf("github get team %s error: %w", teamSlug, err)
}
// check if team is assigned to the repo
_, resp, err := gh.Teams.IsTeamRepoBySlug(ctx, owner, teamSlug, owner, repository)
if resp == nil && err != nil {
return fmt.Errorf("github is team %s error: %w", teamSlug, err)
}
// add team to the repo
if resp.StatusCode == 404 {
_, err = gh.Teams.AddTeamRepoBySlug(ctx, owner, teamSlug, owner, repository, &github.TeamAddTeamRepoOptions{
Permission: permission,
})
if err != nil {
return fmt.Errorf("github add team %s error: %w", teamSlug, err)
}
}
return nil
}
func checkoutGitHubRepository(ctx context.Context, url, branch, token, path string) (*git.Repository, error) {
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 generateGitHubInstall(targetPath, namespace, tmpDir string) (string, error) {
tkDir := path.Join(tmpDir, ".tk")
defer os.RemoveAll(tkDir)
if err := os.MkdirAll(tkDir, os.ModePerm); err != nil {
return "", fmt.Errorf("generating manifests failed: %w", err)
}
if err := genInstallManifests(bootstrapVersion, namespace, components, tkDir); err != nil {
return "", fmt.Errorf("generating manifests failed: %w", err)
}
manifestsDir := path.Join(tmpDir, targetPath, namespace)
if err := os.MkdirAll(manifestsDir, os.ModePerm); err != nil {
return "", fmt.Errorf("generating manifests failed: %w", err)
}
manifest := path.Join(manifestsDir, ghInstallManifest)
if err := buildKustomization(tkDir, manifest); err != nil {
return "", fmt.Errorf("build kustomization failed: %w", err)
}
return manifest, nil
}
func commitGitHubManifests(repo *git.Repository, targetPath, namespace string) (bool, error) {
w, err := repo.Worktree()
if err != nil {
return false, err
}
_, err = w.Add(path.Join(targetPath, namespace))
if err != nil {
return false, err
}
status, err := w.Status()
if err != nil {
return false, err
}
if !status.IsClean() {
if _, err := w.Commit("Add manifests", &git.CommitOptions{
Author: &object.Signature{
Name: "tk",
Email: "tk@users.noreply.github.com",
When: time.Now(),
},
}); err != nil {
return false, err
}
return true, nil
}
return false, 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, targetPath, tmpDir string, interval time.Duration) 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: interval,
},
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, targetPath, namespace, ghSourceManifest)); 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: fmt.Sprintf("./%s", strings.TrimPrefix(targetPath, "./")),
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, targetPath, namespace, ghKustomizationManifest)); err != nil {
return err
}
return nil
}
func applyGitHubKustomization(ctx context.Context, kubeClient client.Client, name, namespace, targetPath, tmpDir string) error {
command := fmt.Sprintf("kubectl apply -f %s", filepath.Join(tmpDir, targetPath, namespace))
if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
return err
}
logWaiting("waiting for cluster sync")
if err := wait.PollImmediate(pollInterval, timeout,
isGitRepositoryReady(ctx, kubeClient, name, namespace)); err != nil {
return err
}
if err := wait.PollImmediate(pollInterval, timeout,
isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil {
return err
}
return nil
}
func shouldInstallGitHub(ctx context.Context, kubeClient client.Client, namespace string) bool {
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: namespace,
}
var kustomization kustomizev1.Kustomization
if err := kubeClient.Get(ctx, namespacedName, &kustomization); err != nil {
return true
}
return kustomization.Status.LastAppliedRevision == ""
}
func shouldCreateGitHubDeployKey(ctx context.Context, kubeClient client.Client, namespace string) bool {
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: namespace,
}
var existing corev1.Secret
if err := kubeClient.Get(ctx, namespacedName, &existing); err != nil {
return true
}
return false
}
func generateGitHubDeployKey(ctx context.Context, kubeClient client.Client, url *url.URL, namespace string) (string, error) {
pair, err := generateKeyPair(ctx)
if err != nil {
return "", err
}
hostKey, err := scanHostKey(ctx, url)
if err != nil {
return "", err
}
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
Namespace: namespace,
},
StringData: map[string]string{
"identity": string(pair.PrivateKey),
"identity.pub": string(pair.PublicKey),
"known_hosts": string(hostKey),
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return "", err
}
return string(pair.PublicKey), nil
}
func createGitHubDeployKey(ctx context.Context, key, hostname, owner, repository, targetPath, token string) error {
gh, err := makeGitHubClient(hostname, token)
if err != nil {
return err
}
keyName := "tk"
if targetPath != "" {
keyName = fmt.Sprintf("tk-%s", targetPath)
}
// list deploy keys
keys, resp, err := gh.Repositories.ListKeys(ctx, owner, repository, nil)
if err != nil {
return fmt.Errorf("github list deploy keys error: %w", err)
}
if resp.StatusCode >= 300 {
return fmt.Errorf("github list deploy keys failed with status code: %s", resp.Status)
}
// check if the key exists
shouldCreateKey := true
var existingKey *github.Key
for _, k := range keys {
if k.Title != nil && k.Key != nil && *k.Title == keyName {
if *k.Key != key {
existingKey = k
} else {
shouldCreateKey = false
}
break
}
}
// delete existing key if the value differs
if existingKey != nil {
resp, err := gh.Repositories.DeleteKey(ctx, owner, repository, *existingKey.ID)
if err != nil {
return fmt.Errorf("github delete deploy key error: %w", err)
}
if resp.StatusCode >= 300 {
return fmt.Errorf("github delete deploy key failed with status code: %s", resp.Status)
}
}
// create key
if shouldCreateKey {
isReadOnly := true
_, _, err = gh.Repositories.CreateKey(ctx, owner, repository, &github.Key{
Title: &keyName,
Key: &key,
ReadOnly: &isReadOnly,
})
if err != nil {
return fmt.Errorf("github create deploy key error: %w", err)
}
}
return nil
}

@ -6,10 +6,12 @@ import (
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"os" "os"
"path"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/xanzy/go-gitlab"
"github.com/fluxcd/toolkit/pkg/git"
) )
var bootstrapGitLabCmd = &cobra.Command{ var bootstrapGitLabCmd = &cobra.Command{
@ -21,14 +23,20 @@ commits the toolkit components manifests to the master branch.
Then it configure the target cluster to synchronize with the repository. Then it configure the target cluster to synchronize with the repository.
If the toolkit components are present on the cluster, If the toolkit components are present on the cluster,
the bootstrap command will perform an upgrade if needed.`, the bootstrap command will perform an upgrade if needed.`,
Example: ` # Create a GitLab personal access token and export it as an env var Example: ` # Create a GitLab API token and export it as an env var
export GITLAB_TOKEN=<my-token> export GITLAB_TOKEN=<my-token>
# Run bootstrap for a private repo owned by a GitLab organization # Run bootstrap for a private repo owned by a GitLab group
bootstrap gitlab --owner=<organization> --repository=<repo name> bootstrap gitlab --owner=<group> --repository=<repo name>
# Run bootstrap for a repository path
bootstrap gitlab --owner=<group> --repository=<repo name> --path=dev-cluster
# Run bootstrap for a public repository on a personal account
bootstrap gitlab --owner=<user> --repository=<repo name> --private=false --personal=true
# Run bootstrap for a private repo hosted on GitLab server # Run bootstrap for a private repo hosted on GitLab server
bootstrap gitlab --owner=<organization> --repository=<repo name> --hostname=<domain> bootstrap gitlab --owner=<group> --repository=<repo name> --hostname=<domain>
`, `,
RunE: bootstrapGitLabCmdRun, RunE: bootstrapGitLabCmdRun,
} }
@ -43,33 +51,32 @@ var (
glPath string glPath string
) )
const (
glTokenName = "GITLAB_TOKEN"
glDefaultHostname = "gitlab.com"
)
func init() { func init() {
bootstrapGitLabCmd.Flags().StringVar(&glOwner, "owner", "", "GitLab user or organization name") bootstrapGitLabCmd.Flags().StringVar(&glOwner, "owner", "", "GitLab user or organization name")
bootstrapGitLabCmd.Flags().StringVar(&glRepository, "repository", "", "GitLab repository name") bootstrapGitLabCmd.Flags().StringVar(&glRepository, "repository", "", "GitLab repository name")
bootstrapGitLabCmd.Flags().BoolVar(&glPersonal, "personal", false, "is personal repository") bootstrapGitLabCmd.Flags().BoolVar(&glPersonal, "personal", false, "is personal repository")
bootstrapGitLabCmd.Flags().BoolVar(&glPrivate, "private", true, "is private repository") bootstrapGitLabCmd.Flags().BoolVar(&glPrivate, "private", true, "is private repository")
bootstrapGitLabCmd.Flags().DurationVar(&glInterval, "interval", time.Minute, "sync interval") bootstrapGitLabCmd.Flags().DurationVar(&glInterval, "interval", time.Minute, "sync interval")
bootstrapGitLabCmd.Flags().StringVar(&glHostname, "hostname", glDefaultHostname, "GitLab hostname") bootstrapGitLabCmd.Flags().StringVar(&glHostname, "hostname", git.GitLabDefaultHostname, "GitLab hostname")
bootstrapGitLabCmd.Flags().StringVar(&glPath, "path", "", "repository path, when specified the cluster sync will be scoped to this path") bootstrapGitLabCmd.Flags().StringVar(&glPath, "path", "", "repository path, when specified the cluster sync will be scoped to this path")
bootstrapCmd.AddCommand(bootstrapGitLabCmd) bootstrapCmd.AddCommand(bootstrapGitLabCmd)
} }
func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
glToken := os.Getenv(glTokenName) glToken := os.Getenv(git.GitLabTokenName)
if glToken == "" { if glToken == "" {
return fmt.Errorf("%s environment variable not found", glTokenName) return fmt.Errorf("%s environment variable not found", git.GitLabTokenName)
} }
gitURL := fmt.Sprintf("https://%s/%s/%s", glHostname, glOwner, glRepository) repository, err := git.NewRepository(glRepository, glOwner, glHostname, glToken, "tk", "tk@users.noreply.gitlab.com")
sshURL := fmt.Sprintf("ssh://git@%s/%s/%s", glHostname, glOwner, glRepository) if err != nil {
if glOwner == "" || glRepository == "" { return err
return fmt.Errorf("owner and repository are required") }
provider := &git.GitLabProvider{
IsPrivate: glPrivate,
IsPersonal: glPersonal,
} }
kubeClient, err := utils.kubeClient(kubeconfig) kubeClient, err := utils.kubeClient(kubeconfig)
@ -88,32 +95,36 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
// create GitLab project if doesn't exists // create GitLab project if doesn't exists
logAction("connecting to %s", glHostname) logAction("connecting to %s", glHostname)
if err := createGitLabRepository(ctx, glHostname, glOwner, glRepository, glToken, glPrivate, glPersonal); err != nil { changed, err := provider.CreateRepository(ctx, repository)
if err != nil {
return err return err
} }
if changed {
logSuccess("repository created")
}
// clone repository and checkout the master branch // clone repository and checkout the master branch
repo, err := checkoutGitHubRepository(ctx, gitURL, ghBranch, glToken, tmpDir) if err := repository.Checkout(ctx, bootstrapBranch, tmpDir); err != nil {
if err != nil {
return err return err
} }
logSuccess("repository cloned") logSuccess("repository cloned")
// generate install manifests // generate install manifests
logGenerate("generating manifests") logGenerate("generating manifests")
manifest, err := generateGitHubInstall(glPath, namespace, tmpDir) manifest, err := generateInstallManifests(glPath, namespace, tmpDir)
if err != nil { if err != nil {
return err return err
} }
// stage install manifests // stage install manifests
changed, err := commitGitHubManifests(repo, glPath, namespace) changed, err = repository.Commit(ctx, path.Join(glPath, namespace), "Add manifests")
if err != nil { if err != nil {
return err return err
} }
// push install manifests
if changed { if changed {
if err := pushGitHubRepository(ctx, repo, glToken); err != nil { if err := repository.Push(ctx); err != nil {
return err return err
} }
logSuccess("components manifests pushed") logSuccess("components manifests pushed")
@ -122,74 +133,63 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
} }
// determine if repo synchronization is working // determine if repo synchronization is working
isInstall := shouldInstallGitHub(ctx, kubeClient, namespace) isInstall := shouldInstallManifests(ctx, kubeClient, namespace)
if isInstall { if isInstall {
// apply install manifests // apply install manifests
logAction("installing components in %s namespace", namespace) logAction("installing components in %s namespace", namespace)
command := fmt.Sprintf("kubectl apply -f %s", manifest) if err := applyInstallManifests(ctx, manifest, components); err != nil {
if _, err := utils.execCommand(ctx, ModeOS, command); err != nil { return err
return fmt.Errorf("install failed")
} }
logSuccess("install completed") logSuccess("install completed")
// check installation
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)
}
}
} }
// setup SSH deploy key // setup SSH deploy key
if shouldCreateGitHubDeployKey(ctx, kubeClient, namespace) { if shouldCreateDeployKey(ctx, kubeClient, namespace) {
logAction("configuring deploy key") logAction("configuring deploy key")
u, err := url.Parse(sshURL) u, err := url.Parse(repository.GetSSH())
if err != nil { if err != nil {
return fmt.Errorf("git URL parse failed: %w", err) return fmt.Errorf("git URL parse failed: %w", err)
} }
key, err := generateGitHubDeployKey(ctx, kubeClient, u, namespace) key, err := generateDeployKey(ctx, kubeClient, u, namespace)
if err != nil { if err != nil {
return fmt.Errorf("generating deploy key failed: %w", err) return fmt.Errorf("generating deploy key failed: %w", err)
} }
if err := createGitLabDeployKey(ctx, key, glHostname, glOwner, glRepository, glPath, glToken); err != nil { keyName := "tk"
if glPath != "" {
keyName = fmt.Sprintf("tk-%s", glPath)
}
if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil {
return err return err
} else if changed {
logSuccess("deploy key configured")
} }
logSuccess("deploy key configured")
} }
// configure repo synchronization // configure repo synchronization
if isInstall { if isInstall {
// generate source and kustomization manifests // generate source and kustomization manifests
logAction("generating sync manifests") logAction("generating sync manifests")
if err := generateGitHubKustomization(sshURL, namespace, namespace, glPath, tmpDir, glInterval); err != nil { if err := generateSyncManifests(repository.GetSSH(), namespace, namespace, glPath, tmpDir, glInterval); err != nil {
return err return err
} }
// stage manifests // commit and push manifests
changed, err = commitGitHubManifests(repo, glPath, namespace) if changed, err = repository.Commit(ctx, path.Join(glPath, namespace), "Add manifests"); err != nil {
if err != nil {
return err return err
} } else if changed {
if err := repository.Push(ctx); err != nil {
// push manifests
if changed {
if err := pushGitHubRepository(ctx, repo, glToken); err != nil {
return err return err
} }
logSuccess("sync manifests pushed")
} }
logSuccess("sync manifests pushed")
// apply manifests and waiting for sync // apply manifests and waiting for sync
logAction("applying sync manifests") logAction("applying sync manifests")
if err := applyGitHubKustomization(ctx, kubeClient, namespace, namespace, glPath, tmpDir); err != nil { if err := applySyncManifests(ctx, kubeClient, namespace, namespace, glPath, tmpDir); err != nil {
return err return err
} }
} }
@ -197,128 +197,3 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
logSuccess("bootstrap finished") logSuccess("bootstrap finished")
return nil return nil
} }
func makeGitLabClient(hostname, token string) (*gitlab.Client, error) {
gl, err := gitlab.NewClient(token)
if err != nil {
return nil, err
}
if glHostname != glDefaultHostname {
gl, err = gitlab.NewClient(token, gitlab.WithBaseURL(fmt.Sprintf("https://%s/api/v4", hostname)))
if err != nil {
return nil, err
}
}
return gl, nil
}
func createGitLabRepository(ctx context.Context, hostname, owner, repository, token string, isPrivate, isPersonal bool) error {
gl, err := makeGitLabClient(hostname, token)
if err != nil {
return fmt.Errorf("client error: %w", err)
}
var id *int
if !isPersonal {
groups, _, err := gl.Groups.ListGroups(&gitlab.ListGroupsOptions{Search: gitlab.String(owner)}, gitlab.WithContext(ctx))
if err != nil {
return fmt.Errorf("list groups error: %w", err)
}
if len(groups) > 0 {
id = &groups[0].ID
}
}
visibility := gitlab.PublicVisibility
if isPrivate {
visibility = gitlab.PrivateVisibility
}
projects, _, err := gl.Projects.ListProjects(&gitlab.ListProjectsOptions{Search: gitlab.String(repository)}, gitlab.WithContext(ctx))
if err != nil {
return fmt.Errorf("list projects error: %w", err)
}
if len(projects) == 0 {
p := &gitlab.CreateProjectOptions{
Name: gitlab.String(repository),
NamespaceID: id,
Visibility: &visibility,
InitializeWithReadme: gitlab.Bool(true),
}
project, _, err := gl.Projects.CreateProject(p)
if err != nil {
return fmt.Errorf("create project error: %w", err)
}
logSuccess("project created id: %v", project.ID)
}
return nil
}
func createGitLabDeployKey(ctx context.Context, key, hostname, owner, repository, targetPath, token string) error {
gl, err := makeGitLabClient(hostname, token)
if err != nil {
return fmt.Errorf("client error: %w", err)
}
var projId int
projects, _, err := gl.Projects.ListProjects(&gitlab.ListProjectsOptions{Search: gitlab.String(repository)}, gitlab.WithContext(ctx))
if err != nil {
return fmt.Errorf("list projects error: %w", err)
}
if len(projects) > 0 {
projId = projects[0].ID
} else {
return fmt.Errorf("no project found")
}
keyName := "tk"
if targetPath != "" {
keyName = fmt.Sprintf("tk-%s", targetPath)
}
// check if the key exists
keys, _, err := gl.DeployKeys.ListProjectDeployKeys(projId, &gitlab.ListProjectDeployKeysOptions{})
if err != nil {
return fmt.Errorf("list keys error: %w", err)
}
shouldCreateKey := true
var existingKey *gitlab.DeployKey
for _, k := range keys {
if k.Title == keyName {
if k.Key != key {
existingKey = k
} else {
shouldCreateKey = false
}
break
}
}
// delete existing key if the value differs
if existingKey != nil {
_, err := gl.DeployKeys.DeleteDeployKey(projId, existingKey.ID, gitlab.WithContext(ctx))
if err != nil {
return fmt.Errorf("delete key error: %w", err)
}
}
// create key
if shouldCreateKey {
_, _, err := gl.DeployKeys.AddDeployKey(projId, &gitlab.AddDeployKeyOptions{
Title: gitlab.String(keyName),
Key: gitlab.String(key),
CanPush: gitlab.Bool(false),
}, gitlab.WithContext(ctx))
if err != nil {
return fmt.Errorf("add key error: %w", err)
}
}
return nil
}

@ -2,6 +2,7 @@ package git
import "context" import "context"
// Provider is the interface that a git provider should implement
type Provider interface { type Provider interface {
CreateRepository(ctx context.Context, r *Repository) (bool, error) CreateRepository(ctx context.Context, r *Repository) (bool, error)
AddTeam(ctx context.Context, r *Repository, name, permission string) (bool, error) AddTeam(ctx context.Context, r *Repository, name, permission string) (bool, error)

@ -17,14 +17,15 @@ type Repository struct {
Name string Name string
Owner string Owner string
Host string Host string
Branch string
Token string Token string
AuthorName string AuthorName string
AuthorEmail string AuthorEmail string
repo *git.Repository
} }
// NewRepository returns a git repository wrapper // NewRepository returns a git repository wrapper
func NewRepository(name, owner, host, branch, token, authorName, authorEmail string) (*Repository, error) { func NewRepository(name, owner, host, token, authorName, authorEmail string) (*Repository, error) {
if name == "" { if name == "" {
return nil, fmt.Errorf("name required") return nil, fmt.Errorf("name required")
} }
@ -34,24 +35,20 @@ func NewRepository(name, owner, host, branch, token, authorName, authorEmail str
if host == "" { if host == "" {
return nil, fmt.Errorf("host required") return nil, fmt.Errorf("host required")
} }
if branch == "" {
return nil, fmt.Errorf("branch required")
}
if token == "" { if token == "" {
return nil, fmt.Errorf("token required") return nil, fmt.Errorf("token required")
} }
if authorName == "" { if authorName == "" {
authorName = "tk" return nil, fmt.Errorf("author name required")
} }
if authorEmail == "" { if authorEmail == "" {
authorEmail = "tk@users.noreply.git-scm.com" return nil, fmt.Errorf("author email required")
} }
return &Repository{ return &Repository{
Name: name, Name: name,
Owner: owner, Owner: owner,
Host: host, Host: host,
Branch: branch,
Token: token, Token: token,
AuthorName: authorName, AuthorName: authorName,
AuthorEmail: authorEmail, AuthorEmail: authorEmail,
@ -75,33 +72,38 @@ func (r *Repository) auth() transport.AuthMethod {
} }
} }
// Checkout repository at specified path // Checkout repository branch at specified path
func (r *Repository) Checkout(ctx context.Context, path string) (*git.Repository, error) { func (r *Repository) Checkout(ctx context.Context, branch, path string) error {
repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{ repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
URL: r.GetURL(), URL: r.GetURL(),
Auth: r.auth(), Auth: r.auth(),
RemoteName: git.DefaultRemoteName, RemoteName: git.DefaultRemoteName,
ReferenceName: plumbing.NewBranchReferenceName(r.Branch), ReferenceName: plumbing.NewBranchReferenceName(branch),
SingleBranch: true, SingleBranch: true,
NoCheckout: false, NoCheckout: false,
Progress: nil, Progress: nil,
Tags: git.NoTags, Tags: git.NoTags,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("git clone error: %w", err) return fmt.Errorf("git clone error: %w", err)
} }
_, err = repo.Head() _, err = repo.Head()
if err != nil { if err != nil {
return nil, fmt.Errorf("git resolve HEAD error: %w", err) return fmt.Errorf("git resolve HEAD error: %w", err)
} }
return repo, nil r.repo = repo
return nil
} }
// Commit changes for the specified path, returns false if no changes are made // Commit changes for the specified path, returns false if no changes are detected
func (r *Repository) Commit(ctx context.Context, repo *git.Repository, path, message string) (bool, error) { func (r *Repository) Commit(ctx context.Context, path, message string) (bool, error) {
w, err := repo.Worktree() if r.repo == nil {
return false, fmt.Errorf("repository hasn't been cloned")
}
w, err := r.repo.Worktree()
if err != nil { if err != nil {
return false, err return false, err
} }
@ -133,8 +135,12 @@ func (r *Repository) Commit(ctx context.Context, repo *git.Repository, path, mes
} }
// Push commits to origin // Push commits to origin
func (r *Repository) Push(ctx context.Context, repo *git.Repository) error { func (r *Repository) Push(ctx context.Context) error {
err := repo.PushContext(ctx, &git.PushOptions{ if r.repo == nil {
return fmt.Errorf("repository hasn't been cloned")
}
err := r.repo.PushContext(ctx, &git.PushOptions{
Auth: r.auth(), Auth: r.auth(),
Progress: nil, Progress: nil,
}) })

Loading…
Cancel
Save