Skip to content
Snippets Groups Projects
Unverified Commit 7a87d353 authored by Stefan Prodan's avatar Stefan Prodan Committed by GitHub
Browse files

Merge pull request #42 from fluxcd/gitlab

Implement Gitlab bootstrap
parents 34ada411 badd2a10
Branches
No related tags found
No related merge requests found
Showing
with 550 additions and 474 deletions
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 ( ...@@ -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 ( ...@@ -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 ( ...@@ -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() { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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
}
package main
import (
"context"
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"time"
"github.com/spf13/cobra"
"github.com/fluxcd/toolkit/pkg/git"
)
var bootstrapGitLabCmd = &cobra.Command{
Use: "gitlab",
Short: "Bootstrap GitLab repository",
Long: `
The bootstrap command creates the GitHub repository if it doesn't exists and
commits the toolkit components manifests to the master branch.
Then it configure the target cluster to synchronize with the repository.
If the toolkit components are present on the cluster,
the bootstrap command will perform an upgrade if needed.`,
Example: ` # Create a GitLab API token and export it as an env var
export GITLAB_TOKEN=<my-token>
# Run bootstrap for a private repo owned by a GitLab group
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
bootstrap gitlab --owner=<group> --repository=<repo name> --hostname=<domain>
`,
RunE: bootstrapGitLabCmdRun,
}
var (
glOwner string
glRepository string
glInterval time.Duration
glPersonal bool
glPrivate bool
glHostname string
glPath string
)
func init() {
bootstrapGitLabCmd.Flags().StringVar(&glOwner, "owner", "", "GitLab user or organization name")
bootstrapGitLabCmd.Flags().StringVar(&glRepository, "repository", "", "GitLab repository name")
bootstrapGitLabCmd.Flags().BoolVar(&glPersonal, "personal", false, "is personal repository")
bootstrapGitLabCmd.Flags().BoolVar(&glPrivate, "private", true, "is private repository")
bootstrapGitLabCmd.Flags().DurationVar(&glInterval, "interval", time.Minute, "sync interval")
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")
bootstrapCmd.AddCommand(bootstrapGitLabCmd)
}
func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
glToken := os.Getenv(git.GitLabTokenName)
if glToken == "" {
return fmt.Errorf("%s environment variable not found", git.GitLabTokenName)
}
repository, err := git.NewRepository(glRepository, glOwner, glHostname, glToken, "tk", "tk@users.noreply.gitlab.com")
if err != nil {
return err
}
provider := &git.GitLabProvider{
IsPrivate: glPrivate,
IsPersonal: glPersonal,
}
kubeClient, err := utils.kubeClient(kubeconfig)
if err != nil {
return err
}
tmpDir, err := ioutil.TempDir("", namespace)
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// create GitLab project if doesn't exists
logAction("connecting to %s", glHostname)
changed, err := provider.CreateRepository(ctx, repository)
if err != nil {
return err
}
if changed {
logSuccess("repository created")
}
// clone repository and checkout the master branch
if err := repository.Checkout(ctx, bootstrapBranch, tmpDir); err != nil {
return err
}
logSuccess("repository cloned")
// generate install manifests
logGenerate("generating manifests")
manifest, err := generateInstallManifests(glPath, namespace, tmpDir)
if err != nil {
return err
}
// stage install manifests
changed, err = repository.Commit(ctx, path.Join(glPath, namespace), "Add manifests")
if err != nil {
return err
}
// push install manifests
if changed {
if err := repository.Push(ctx); err != nil {
return err
}
logSuccess("components manifests pushed")
} else {
logSuccess("components are up to date")
}
// determine if repo synchronization is working
isInstall := shouldInstallManifests(ctx, kubeClient, namespace)
if isInstall {
// apply install manifests
logAction("installing components in %s namespace", namespace)
if err := applyInstallManifests(ctx, manifest, components); err != nil {
return err
}
logSuccess("install completed")
}
// setup SSH deploy key
if shouldCreateDeployKey(ctx, kubeClient, namespace) {
logAction("configuring deploy key")
u, err := url.Parse(repository.GetSSH())
if err != nil {
return fmt.Errorf("git URL parse failed: %w", err)
}
key, err := generateDeployKey(ctx, kubeClient, u, namespace)
if err != nil {
return fmt.Errorf("generating deploy key failed: %w", err)
}
keyName := "tk"
if glPath != "" {
keyName = fmt.Sprintf("tk-%s", glPath)
}
if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil {
return err
} else if changed {
logSuccess("deploy key configured")
}
}
// configure repo synchronization
if isInstall {
// generate source and kustomization manifests
logAction("generating sync manifests")
if err := generateSyncManifests(repository.GetSSH(), namespace, namespace, glPath, tmpDir, glInterval); err != nil {
return err
}
// commit and push manifests
if changed, err = repository.Commit(ctx, path.Join(glPath, namespace), "Add manifests"); err != nil {
return err
} else if changed {
if err := repository.Push(ctx); err != nil {
return err
}
logSuccess("sync manifests pushed")
}
// apply manifests and waiting for sync
logAction("applying sync manifests")
if err := applySyncManifests(ctx, kubeClient, namespace, namespace, glPath, tmpDir); err != nil {
return err
}
}
logSuccess("bootstrap finished")
return nil
}
...@@ -90,4 +90,4 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way. ...@@ -90,4 +90,4 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.
* [tk sync](tk_sync.md) - Synchronize commands * [tk sync](tk_sync.md) - Synchronize commands
* [tk uninstall](tk_uninstall.md) - Uninstall the toolkit components * [tk uninstall](tk_uninstall.md) - Uninstall the toolkit components
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -27,5 +27,6 @@ Bootstrap commands ...@@ -27,5 +27,6 @@ Bootstrap commands
* [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines * [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines
* [tk bootstrap github](tk_bootstrap_github.md) - Bootstrap GitHub repository * [tk bootstrap github](tk_bootstrap_github.md) - Bootstrap GitHub repository
* [tk bootstrap gitlab](tk_bootstrap_gitlab.md) - Bootstrap GitLab repository
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -24,6 +24,12 @@ tk bootstrap github [flags] ...@@ -24,6 +24,12 @@ tk bootstrap github [flags]
# Run bootstrap for a private repo owned by a GitHub organization # Run bootstrap for a private repo owned by a GitHub organization
bootstrap github --owner=<organization> --repository=<repo name> bootstrap github --owner=<organization> --repository=<repo name>
# Run bootstrap for a private repo and assign organization teams to it
bootstrap github --owner=<organization> --repository=<repo name> --team=<team1 slug> --team=<team2 slug>
# Run bootstrap for a repository path
bootstrap github --owner=<organization> --repository=<repo name> --path=dev-cluster
# Run bootstrap for a public repository on a personal account # Run bootstrap for a public repository on a personal account
bootstrap github --owner=<user> --repository=<repo name> --private=false --personal=true bootstrap github --owner=<user> --repository=<repo name> --private=false --personal=true
...@@ -39,9 +45,11 @@ tk bootstrap github [flags] ...@@ -39,9 +45,11 @@ tk bootstrap github [flags]
--hostname string GitHub hostname (default "github.com") --hostname string GitHub hostname (default "github.com")
--interval duration sync interval (default 1m0s) --interval duration sync interval (default 1m0s)
--owner string GitHub user or organization name --owner string GitHub user or organization name
--path string repository path, when specified the cluster sync will be scoped to this path
--personal is personal repository --personal is personal repository
--private is private repository (default true) --private is private repository (default true)
--repository string GitHub repository name --repository string GitHub repository name
--team stringArray GitHub team to be given maintainer access
``` ```
### Options inherited from parent commands ### Options inherited from parent commands
...@@ -59,4 +67,4 @@ tk bootstrap github [flags] ...@@ -59,4 +67,4 @@ tk bootstrap github [flags]
* [tk bootstrap](tk_bootstrap.md) - Bootstrap commands * [tk bootstrap](tk_bootstrap.md) - Bootstrap commands
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
## tk bootstrap gitlab
Bootstrap GitLab repository
### Synopsis
The bootstrap command creates the GitHub repository if it doesn't exists and
commits the toolkit components manifests to the master branch.
Then it configure the target cluster to synchronize with the repository.
If the toolkit components are present on the cluster,
the bootstrap command will perform an upgrade if needed.
```
tk bootstrap gitlab [flags]
```
### Examples
```
# Create a GitLab API token and export it as an env var
export GITLAB_TOKEN=<my-token>
# Run bootstrap for a private repo owned by a GitLab group
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
bootstrap gitlab --owner=<group> --repository=<repo name> --hostname=<domain>
```
### Options
```
-h, --help help for gitlab
--hostname string GitLab hostname (default "gitlab.com")
--interval duration sync interval (default 1m0s)
--owner string GitLab user or organization name
--path string repository path, when specified the cluster sync will be scoped to this path
--personal is personal repository
--private is private repository (default true)
--repository string GitLab repository name
```
### Options inherited from parent commands
```
--components strings list of components, accepts comma-separated values (default [source-controller,kustomize-controller])
--kubeconfig string path to the kubeconfig file (default "~/.kube/config")
--namespace string the namespace scope for this operation (default "gitops-system")
--timeout duration timeout for this operation (default 5m0s)
--verbose print generated objects
--version string toolkit tag or branch (default "master")
```
### SEE ALSO
* [tk bootstrap](tk_bootstrap.md) - Bootstrap commands
###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -44,4 +44,4 @@ tk check [flags] ...@@ -44,4 +44,4 @@ tk check [flags]
* [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines * [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -44,4 +44,4 @@ To configure your bash shell to load completions for each session add to your ba ...@@ -44,4 +44,4 @@ To configure your bash shell to load completions for each session add to your ba
* [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines * [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -30,4 +30,4 @@ Create commands ...@@ -30,4 +30,4 @@ Create commands
* [tk create kustomization](tk_create_kustomization.md) - Create or update a kustomization resource * [tk create kustomization](tk_create_kustomization.md) - Create or update a kustomization resource
* [tk create source](tk_create_source.md) - Create source commands * [tk create source](tk_create_source.md) - Create source commands
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -78,4 +78,4 @@ tk create kustomization [name] [flags] ...@@ -78,4 +78,4 @@ tk create kustomization [name] [flags]
* [tk create](tk_create.md) - Create commands * [tk create](tk_create.md) - Create commands
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -29,4 +29,4 @@ Create source commands ...@@ -29,4 +29,4 @@ Create source commands
* [tk create](tk_create.md) - Create commands * [tk create](tk_create.md) - Create commands
* [tk create source git](tk_create_source_git.md) - Create or update a git source * [tk create source git](tk_create_source_git.md) - Create or update a git source
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -58,7 +58,7 @@ tk create source git [name] [flags] ...@@ -58,7 +58,7 @@ tk create source git [name] [flags]
--branch string git branch (default "master") --branch string git branch (default "master")
-h, --help help for git -h, --help help for git
-p, --password string basic authentication password -p, --password string basic authentication password
--ssh-ecdsa-curve ecdsaCurve SSH ECDSA public key curve (p521, p256, p384) (default p384) --ssh-ecdsa-curve ecdsaCurve SSH ECDSA public key curve (p256, p384, p521) (default p384)
--ssh-key-algorithm publicKeyAlgorithm SSH public key algorithm (rsa, ecdsa, ed25519) (default rsa) --ssh-key-algorithm publicKeyAlgorithm SSH public key algorithm (rsa, ecdsa, ed25519) (default rsa)
--ssh-rsa-bits rsaKeyBits SSH RSA public key bit size (multiplies of 8) (default 2048) --ssh-rsa-bits rsaKeyBits SSH RSA public key bit size (multiplies of 8) (default 2048)
--tag string git tag --tag string git tag
...@@ -83,4 +83,4 @@ tk create source git [name] [flags] ...@@ -83,4 +83,4 @@ tk create source git [name] [flags]
* [tk create source](tk_create_source.md) - Create source commands * [tk create source](tk_create_source.md) - Create source commands
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -29,4 +29,4 @@ Delete commands ...@@ -29,4 +29,4 @@ Delete commands
* [tk delete kustomization](tk_delete_kustomization.md) - Delete kustomization * [tk delete kustomization](tk_delete_kustomization.md) - Delete kustomization
* [tk delete source](tk_delete_source.md) - Delete sources commands * [tk delete source](tk_delete_source.md) - Delete sources commands
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -31,4 +31,4 @@ tk delete kustomization [name] [flags] ...@@ -31,4 +31,4 @@ tk delete kustomization [name] [flags]
* [tk delete](tk_delete.md) - Delete commands * [tk delete](tk_delete.md) - Delete commands
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -28,4 +28,4 @@ Delete sources commands ...@@ -28,4 +28,4 @@ Delete sources commands
* [tk delete](tk_delete.md) - Delete commands * [tk delete](tk_delete.md) - Delete commands
* [tk delete source git](tk_delete_source_git.md) - Delete git source * [tk delete source git](tk_delete_source_git.md) - Delete git source
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -31,4 +31,4 @@ tk delete source git [name] [flags] ...@@ -31,4 +31,4 @@ tk delete source git [name] [flags]
* [tk delete source](tk_delete_source.md) - Delete sources commands * [tk delete source](tk_delete_source.md) - Delete sources commands
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -29,4 +29,4 @@ Export commands ...@@ -29,4 +29,4 @@ Export commands
* [tk export kustomization](tk_export_kustomization.md) - Export kustomization in YAML format * [tk export kustomization](tk_export_kustomization.md) - Export kustomization in YAML format
* [tk export source](tk_export_source.md) - Export source commands * [tk export source](tk_export_source.md) - Export source commands
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -42,4 +42,4 @@ tk export kustomization [name] [flags] ...@@ -42,4 +42,4 @@ tk export kustomization [name] [flags]
* [tk export](tk_export.md) - Export commands * [tk export](tk_export.md) - Export commands
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
...@@ -29,4 +29,4 @@ Export source commands ...@@ -29,4 +29,4 @@ Export source commands
* [tk export](tk_export.md) - Export commands * [tk export](tk_export.md) - Export commands
* [tk export source git](tk_export_source_git.md) - Export git sources in YAML format * [tk export source git](tk_export_source_git.md) - Export git sources in YAML format
###### Auto generated by spf13/cobra on 9-Jun-2020 ###### Auto generated by spf13/cobra on 18-Jun-2020
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment