1
0
mirror of synced 2026-02-06 19:05:55 +00:00

Move bootstrap package from internal to pkg

Signed-off-by: Philip Laine <philip.laine@gmail.com>
This commit is contained in:
Philip Laine
2022-08-18 21:50:43 +02:00
parent e7847b75db
commit 0343575146
15 changed files with 17 additions and 17 deletions

222
pkg/bootstrap/bootstrap.go Normal file
View File

@@ -0,0 +1,222 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package bootstrap
import (
"context"
"errors"
"fmt"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/flux2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/pkg/manifestgen/sync"
)
var (
ErrReconciledWithWarning = errors.New("reconciled with warning")
)
type Reconciler interface {
// ReconcileComponents reconciles the components by generating the
// manifests with the provided values, committing them to Git and
// pushing to remote if there are any changes, and applying them
// to the cluster.
ReconcileComponents(ctx context.Context, manifestsBase string, options install.Options, secretOpts sourcesecret.Options) error
// ReconcileSourceSecret reconciles the source secret by generating
// a new secret with the provided values if the secret does not
// already exists on the cluster, or if any of the configuration
// options changed.
ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error
// ReconcileSyncConfig reconciles the sync configuration by generating
// the sync manifests with the provided values, committing them to Git
// and pushing to remote if there are any changes.
ReconcileSyncConfig(ctx context.Context, options sync.Options) error
// ReportKustomizationHealth reports about the health of the
// Kustomization synchronizing the components.
ReportKustomizationHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error
// ReportComponentsHealth reports about the health for the components
// and extra components in install.Options.
ReportComponentsHealth(ctx context.Context, options install.Options, timeout time.Duration) error
}
type RepositoryReconciler interface {
// ReconcileRepository reconciles an external Git repository.
ReconcileRepository(ctx context.Context) error
}
type PostGenerateSecretFunc func(ctx context.Context, secret corev1.Secret, options sourcesecret.Options) error
func Run(ctx context.Context, reconciler Reconciler, manifestsBase string,
installOpts install.Options, secretOpts sourcesecret.Options, syncOpts sync.Options,
pollInterval, timeout time.Duration) error {
var err error
if r, ok := reconciler.(RepositoryReconciler); ok {
if err = r.ReconcileRepository(ctx); err != nil && !errors.Is(err, ErrReconciledWithWarning) {
return err
}
}
if err := reconciler.ReconcileComponents(ctx, manifestsBase, installOpts, secretOpts); err != nil {
return err
}
if err := reconciler.ReconcileSourceSecret(ctx, secretOpts); err != nil {
return err
}
if err := reconciler.ReconcileSyncConfig(ctx, syncOpts); err != nil {
return err
}
var healthErrCount int
if err := reconciler.ReportKustomizationHealth(ctx, syncOpts, pollInterval, timeout); err != nil {
healthErrCount++
}
if err := reconciler.ReportComponentsHealth(ctx, installOpts, timeout); err != nil {
healthErrCount++
}
if healthErrCount > 0 {
// Composing a "smart" error message here from the returned
// errors does not result in any useful information for the
// user, as both methods log the failures they run into.
err = fmt.Errorf("bootstrap failed with %d health check failure(s)", healthErrCount)
}
return err
}
func mustInstallManifests(ctx context.Context, kube client.Client, namespace string) bool {
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: namespace,
}
var k kustomizev1.Kustomization
if err := kube.Get(ctx, namespacedName, &k); err != nil {
return true
}
return k.Status.LastAppliedRevision == ""
}
func secretExists(ctx context.Context, kube client.Client, objKey client.ObjectKey) (bool, error) {
if err := kube.Get(ctx, objKey, &corev1.Secret{}); err != nil {
if apierr.IsNotFound(err) {
return false, nil
}
return false, err
}
return true, nil
}
func reconcileSecret(ctx context.Context, kube client.Client, secret corev1.Secret) error {
objKey := client.ObjectKeyFromObject(&secret)
var existing corev1.Secret
err := kube.Get(ctx, objKey, &existing)
if err != nil {
if apierr.IsNotFound(err) {
return kube.Create(ctx, &secret)
}
return err
}
existing.StringData = secret.StringData
return kube.Update(ctx, &existing)
}
func kustomizationPathDiffers(ctx context.Context, kube client.Client, objKey client.ObjectKey, path string) (string, error) {
var k kustomizev1.Kustomization
if err := kube.Get(ctx, objKey, &k); err != nil {
if apierr.IsNotFound(err) {
return "", nil
}
return "", err
}
normalizePath := func(p string) string {
// remove the trailing '/' if the path is not './'
if len(p) > 2 {
p = strings.TrimSuffix(p, "/")
}
return fmt.Sprintf("./%s", strings.TrimPrefix(p, "./"))
}
if normalizePath(path) == normalizePath(k.Spec.Path) {
return "", nil
}
return k.Spec.Path, nil
}
func kustomizationReconciled(ctx context.Context, kube client.Client, objKey client.ObjectKey,
kustomization *kustomizev1.Kustomization, expectRevision string) func() (bool, error) {
return func() (bool, error) {
if err := kube.Get(ctx, objKey, kustomization); err != nil {
return false, err
}
// Detect suspended Kustomization, as this would result in an endless wait
if kustomization.Spec.Suspend {
return false, fmt.Errorf("Kustomization is suspended")
}
// Confirm the state we are observing is for the current generation
if kustomization.Generation != kustomization.Status.ObservedGeneration {
return false, nil
}
// Confirm the given revision has been attempted by the controller
if kustomization.Status.LastAttemptedRevision != expectRevision {
return false, nil
}
// Confirm the resource is healthy
if c := apimeta.FindStatusCondition(kustomization.Status.Conditions, meta.ReadyCondition); c != nil {
switch c.Status {
case metav1.ConditionTrue:
return true, nil
case metav1.ConditionFalse:
return false, fmt.Errorf(c.Message)
}
}
return false, nil
}
}
func retry(retries int, wait time.Duration, fn func() error) (err error) {
for i := 0; ; i++ {
err = fn()
if err == nil {
return
}
if i >= retries {
break
}
time.Sleep(wait)
}
return err
}

View File

@@ -0,0 +1,391 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package bootstrap
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
gogit "github.com/go-git/go-git/v5"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/yaml"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
"github.com/fluxcd/pkg/kustomize/filesys"
runclient "github.com/fluxcd/pkg/runtime/client"
"github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/bootstrap/git"
"github.com/fluxcd/flux2/pkg/log"
"github.com/fluxcd/flux2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/pkg/manifestgen/kustomization"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/pkg/manifestgen/sync"
"github.com/fluxcd/flux2/pkg/status"
)
type PlainGitBootstrapper struct {
url string
branch string
caBundle []byte
author git.Author
commitMessageAppendix string
gpgKeyRingPath string
gpgPassphrase string
gpgKeyID string
restClientGetter genericclioptions.RESTClientGetter
restClientOptions *runclient.Options
postGenerateSecret []PostGenerateSecretFunc
git git.Git
kube client.Client
logger log.Logger
}
type GitOption interface {
applyGit(b *PlainGitBootstrapper)
}
func WithRepositoryURL(url string) GitOption {
return repositoryURLOption(url)
}
type repositoryURLOption string
func (o repositoryURLOption) applyGit(b *PlainGitBootstrapper) {
b.url = string(o)
}
func WithPostGenerateSecretFunc(callback PostGenerateSecretFunc) GitOption {
return postGenerateSecret(callback)
}
type postGenerateSecret PostGenerateSecretFunc
func (o postGenerateSecret) applyGit(b *PlainGitBootstrapper) {
b.postGenerateSecret = append(b.postGenerateSecret, PostGenerateSecretFunc(o))
}
func NewPlainGitProvider(git git.Git, kube client.Client, opts ...GitOption) (*PlainGitBootstrapper, error) {
b := &PlainGitBootstrapper{
git: git,
kube: kube,
}
for _, opt := range opts {
opt.applyGit(b)
}
return b, nil
}
func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifestsBase string, options install.Options, _ sourcesecret.Options) error {
// Clone if not already
if _, err := b.git.Status(); err != nil {
if err != git.ErrNoGitRepository {
return err
}
b.logger.Actionf("cloning branch %q from Git repository %q", b.branch, b.url)
var cloned bool
if err = retry(1, 2*time.Second, func() (err error) {
cloned, err = b.git.Clone(ctx, b.url, b.branch, b.caBundle)
return
}); err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
if cloned {
b.logger.Successf("cloned repository")
}
}
// Generate component manifests
b.logger.Actionf("generating component manifests")
manifests, err := install.Generate(options, manifestsBase)
if err != nil {
return fmt.Errorf("component manifest generation failed: %w", err)
}
b.logger.Successf("generated component manifests")
// Write manifest to Git repository
if err = b.git.Write(manifests.Path, strings.NewReader(manifests.Content)); err != nil {
return fmt.Errorf("failed to write manifest %q: %w", manifests.Path, err)
}
// Git commit generated
gpgOpts := git.WithGpgSigningOption(b.gpgKeyRingPath, b.gpgPassphrase, b.gpgKeyID)
commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version)
if b.commitMessageAppendix != "" {
commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix
}
commit, err := b.git.Commit(git.Commit{
Author: b.author,
Message: commitMsg,
}, gpgOpts)
if err != nil && err != git.ErrNoStagedFiles {
return fmt.Errorf("failed to commit sync manifests: %w", err)
}
if err == nil {
b.logger.Successf("committed sync manifests to %q (%q)", b.branch, commit)
b.logger.Actionf("pushing component manifests to %q", b.url)
if err = b.git.Push(ctx, b.caBundle); err != nil {
return fmt.Errorf("failed to push manifests: %w", err)
}
} else {
b.logger.Successf("component manifests are up to date")
}
// Conditionally install manifests
if mustInstallManifests(ctx, b.kube, options.Namespace) {
b.logger.Actionf("installing components in %q namespace", options.Namespace)
componentsYAML := filepath.Join(b.git.Path(), manifests.Path)
kfile := filepath.Join(filepath.Dir(componentsYAML), konfig.DefaultKustomizationFileName())
if _, err := os.Stat(kfile); err == nil {
// Apply the components and their patches
if _, err := utils.Apply(ctx, b.restClientGetter, b.restClientOptions, b.git.Path(), kfile); err != nil {
return err
}
} else {
// Apply the CRDs and controllers
if _, err := utils.Apply(ctx, b.restClientGetter, b.restClientOptions, b.git.Path(), componentsYAML); err != nil {
return err
}
}
b.logger.Successf("installed components")
}
b.logger.Successf("reconciled components")
return nil
}
func (b *PlainGitBootstrapper) ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error {
// Determine if there is an existing secret
secretKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace}
b.logger.Actionf("determining if source secret %q exists", secretKey)
ok, err := secretExists(ctx, b.kube, secretKey)
if err != nil {
return fmt.Errorf("failed to determine if deploy key secret exists: %w", err)
}
// Return early if exists and no custom config is passed
if ok && len(options.CAFilePath+options.PrivateKeyPath+options.Username+options.Password) == 0 {
b.logger.Successf("source secret up to date")
return nil
}
// Generate source secret
b.logger.Actionf("generating source secret")
manifest, err := sourcesecret.Generate(options)
if err != nil {
return err
}
var secret corev1.Secret
if err := yaml.Unmarshal([]byte(manifest.Content), &secret); err != nil {
return fmt.Errorf("failed to unmarshal generated source secret manifest: %w", err)
}
for _, callback := range b.postGenerateSecret {
if err = callback(ctx, secret, options); err != nil {
return err
}
}
// Apply source secret
b.logger.Actionf("applying source secret %q", secretKey)
if err = reconcileSecret(ctx, b.kube, secret); err != nil {
return err
}
b.logger.Successf("reconciled source secret")
return nil
}
func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options) error {
// Confirm that sync configuration does not overwrite existing config
if curPath, err := kustomizationPathDiffers(ctx, b.kube, client.ObjectKey{Name: options.Name, Namespace: options.Namespace}, options.TargetPath); err != nil {
return fmt.Errorf("failed to determine if sync configuration would overwrite existing Kustomization: %w", err)
} else if curPath != "" {
return fmt.Errorf("sync path configuration (%q) would overwrite path (%q) of existing Kustomization", options.TargetPath, curPath)
}
// Clone if not already
if _, err := b.git.Status(); err != nil {
if err == git.ErrNoGitRepository {
b.logger.Actionf("cloning branch %q from Git repository %q", b.branch, b.url)
var cloned bool
if err = retry(1, 2*time.Second, func() (err error) {
cloned, err = b.git.Clone(ctx, b.url, b.branch, b.caBundle)
return
}); err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
if cloned {
b.logger.Successf("cloned repository")
}
}
return err
}
// Generate sync manifests and write to Git repository
b.logger.Actionf("generating sync manifests")
manifests, err := sync.Generate(options)
if err != nil {
return fmt.Errorf("sync manifests generation failed: %w", err)
}
if err = b.git.Write(manifests.Path, strings.NewReader(manifests.Content)); err != nil {
return fmt.Errorf("failed to write manifest %q: %w", manifests.Path, err)
}
// Create secure Kustomize FS
fs, err := filesys.MakeFsOnDiskSecureBuild(b.git.Path())
if err != nil {
return fmt.Errorf("failed to initialize Kustomize file system: %w", err)
}
// Generate Kustomization
kusManifests, err := kustomization.Generate(kustomization.Options{
FileSystem: fs,
BaseDir: b.git.Path(),
TargetPath: filepath.Dir(manifests.Path),
})
if err != nil {
return fmt.Errorf("%s generation failed: %w", konfig.DefaultKustomizationFileName(), err)
}
if err = b.git.Write(kusManifests.Path, strings.NewReader(kusManifests.Content)); err != nil {
return fmt.Errorf("failed to write manifest %q: %w", kusManifests.Path, err)
}
b.logger.Successf("generated sync manifests")
// Git commit generated
gpgOpts := git.WithGpgSigningOption(b.gpgKeyRingPath, b.gpgPassphrase, b.gpgKeyID)
commitMsg := fmt.Sprintf("Add Flux sync manifests")
if b.commitMessageAppendix != "" {
commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix
}
commit, err := b.git.Commit(git.Commit{
Author: b.author,
Message: commitMsg,
}, gpgOpts)
if err != nil && err != git.ErrNoStagedFiles {
return fmt.Errorf("failed to commit sync manifests: %w", err)
}
if err == nil {
b.logger.Successf("committed sync manifests to %q (%q)", b.branch, commit)
b.logger.Actionf("pushing sync manifests to %q", b.url)
err = b.git.Push(ctx, b.caBundle)
if err != nil {
if strings.HasPrefix(err.Error(), gogit.ErrNonFastForwardUpdate.Error()) {
b.logger.Waitingf("git conflict detected, retrying with a fresh clone")
if err := os.RemoveAll(b.git.Path()); err != nil {
return fmt.Errorf("failed to remove tmp dir: %w", err)
}
if err := os.Mkdir(b.git.Path(), 0o700); err != nil {
return fmt.Errorf("failed to recreate tmp dir: %w", err)
}
if err = retry(1, 2*time.Second, func() (err error) {
_, err = b.git.Clone(ctx, b.url, b.branch, b.caBundle)
return
}); err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
return b.ReconcileSyncConfig(ctx, options)
}
return fmt.Errorf("failed to push sync manifests: %w", err)
}
} else {
b.logger.Successf("sync manifests are up to date")
}
// Apply to cluster
b.logger.Actionf("applying sync manifests")
if _, err := utils.Apply(ctx, b.restClientGetter, b.restClientOptions, b.git.Path(), filepath.Join(b.git.Path(), kusManifests.Path)); err != nil {
return err
}
b.logger.Successf("reconciled sync configuration")
return nil
}
func (b *PlainGitBootstrapper) ReportKustomizationHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error {
head, err := b.git.Head()
if err != nil {
return err
}
objKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace}
b.logger.Waitingf("waiting for Kustomization %q to be reconciled", objKey.String())
expectRevision := fmt.Sprintf("%s/%s", options.Branch, head)
var k kustomizev1.Kustomization
if err := wait.PollImmediate(pollInterval, timeout, kustomizationReconciled(
ctx, b.kube, objKey, &k, expectRevision),
); err != nil {
b.logger.Failuref(err.Error())
return err
}
b.logger.Successf("Kustomization reconciled successfully")
return nil
}
func (b *PlainGitBootstrapper) ReportComponentsHealth(ctx context.Context, install install.Options, timeout time.Duration) error {
cfg, err := utils.KubeConfig(b.restClientGetter, b.restClientOptions)
if err != nil {
return err
}
checker, err := status.NewStatusChecker(cfg, 5*time.Second, timeout, b.logger)
if err != nil {
return err
}
var components = install.Components
components = append(components, install.ComponentsExtra...)
var identifiers []object.ObjMetadata
for _, component := range components {
identifiers = append(identifiers, object.ObjMetadata{
Namespace: install.Namespace,
Name: component,
GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
})
}
b.logger.Actionf("confirming components are healthy")
if err := checker.Assess(identifiers...); err != nil {
return err
}
b.logger.Successf("all components are healthy")
return nil
}

View File

@@ -0,0 +1,565 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package bootstrap
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/go-git-providers/gitprovider"
"github.com/fluxcd/flux2/pkg/bootstrap/git"
"github.com/fluxcd/flux2/pkg/bootstrap/provider"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/pkg/manifestgen/sync"
)
type GitProviderBootstrapper struct {
*PlainGitBootstrapper
owner string
repositoryName string
repository gitprovider.UserRepository
personal bool
description string
defaultBranch string
visibility string
reconcile bool
teams map[string]string
readWriteKey bool
bootstrapTransportType string
syncTransportType string
sshHostname string
provider gitprovider.Client
}
func NewGitProviderBootstrapper(git git.Git, provider gitprovider.Client, kube client.Client, opts ...GitProviderOption) (*GitProviderBootstrapper, error) {
b := &GitProviderBootstrapper{
PlainGitBootstrapper: &PlainGitBootstrapper{
git: git,
kube: kube,
},
bootstrapTransportType: "https",
syncTransportType: "ssh",
provider: provider,
}
b.PlainGitBootstrapper.postGenerateSecret = append(b.PlainGitBootstrapper.postGenerateSecret, b.reconcileDeployKey)
for _, opt := range opts {
opt.applyGitProvider(b)
}
return b, nil
}
type GitProviderOption interface {
applyGitProvider(b *GitProviderBootstrapper)
}
func WithProviderRepository(owner, repositoryName string, personal bool) GitProviderOption {
return providerRepositoryOption{
owner: owner,
repositoryName: repositoryName,
personal: personal,
}
}
type providerRepositoryOption struct {
owner string
repositoryName string
personal bool
}
func (o providerRepositoryOption) applyGitProvider(b *GitProviderBootstrapper) {
b.owner = o.owner
b.repositoryName = o.repositoryName
b.personal = o.personal
}
func WithProviderRepositoryConfig(description, defaultBranch, visibility string) GitProviderOption {
return providerRepositoryConfigOption{
description: description,
defaultBranch: defaultBranch,
visibility: visibility,
}
}
type providerRepositoryConfigOption struct {
description string
defaultBranch string
visibility string
}
func (o providerRepositoryConfigOption) applyGitProvider(b *GitProviderBootstrapper) {
b.description = o.description
b.defaultBranch = o.defaultBranch
b.visibility = o.visibility
}
func WithProviderTeamPermissions(teams map[string]string) GitProviderOption {
return providerRepositoryTeamPermissionsOption(teams)
}
type providerRepositoryTeamPermissionsOption map[string]string
func (o providerRepositoryTeamPermissionsOption) applyGitProvider(b *GitProviderBootstrapper) {
b.teams = o
}
func WithReadWriteKeyPermissions(b bool) GitProviderOption {
return withReadWriteKeyPermissionsOption(b)
}
type withReadWriteKeyPermissionsOption bool
func (o withReadWriteKeyPermissionsOption) applyGitProvider(b *GitProviderBootstrapper) {
b.readWriteKey = bool(o)
}
func WithBootstrapTransportType(protocol string) GitProviderOption {
return bootstrapTransportTypeOption(protocol)
}
type bootstrapTransportTypeOption string
func (o bootstrapTransportTypeOption) applyGitProvider(b *GitProviderBootstrapper) {
b.bootstrapTransportType = string(o)
}
func WithSyncTransportType(protocol string) GitProviderOption {
return syncProtocolOption(protocol)
}
type syncProtocolOption string
func (o syncProtocolOption) applyGitProvider(b *GitProviderBootstrapper) {
b.syncTransportType = string(o)
}
func WithSSHHostname(hostname string) GitProviderOption {
return sshHostnameOption(hostname)
}
type sshHostnameOption string
func (o sshHostnameOption) applyGitProvider(b *GitProviderBootstrapper) {
b.sshHostname = string(o)
}
func WithReconcile() GitProviderOption {
return reconcileOption(true)
}
type reconcileOption bool
func (o reconcileOption) applyGitProvider(b *GitProviderBootstrapper) {
b.reconcile = true
}
func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options) error {
if b.repository == nil {
return errors.New("repository is required")
}
if b.url == "" {
bootstrapURL, err := b.getCloneURL(b.repository, gitprovider.TransportType(b.bootstrapTransportType))
if err != nil {
return err
}
WithRepositoryURL(bootstrapURL).applyGit(b.PlainGitBootstrapper)
}
if options.URL == "" {
syncURL, err := b.getCloneURL(b.repository, gitprovider.TransportType(b.syncTransportType))
if err != nil {
return err
}
options.URL = syncURL
}
return b.PlainGitBootstrapper.ReconcileSyncConfig(ctx, options)
}
// ReconcileRepository reconciles an organization or user repository with the
// GitProviderBootstrapper configuration. On success, the URL in the embedded
// PlainGitBootstrapper is set to clone URL for the configured protocol.
//
// When part of the reconciliation fails with a warning without aborting, an
// ErrReconciledWithWarning error is returned.
func (b *GitProviderBootstrapper) ReconcileRepository(ctx context.Context) error {
var repo gitprovider.UserRepository
var err error
if b.personal {
repo, err = b.reconcileUserRepository(ctx)
} else {
repo, err = b.reconcileOrgRepository(ctx)
}
if err != nil && !errors.Is(err, ErrReconciledWithWarning) {
return err
}
cloneURL, err := b.getCloneURL(repo, gitprovider.TransportType(b.bootstrapTransportType))
if err != nil {
return err
}
b.repository = repo
WithRepositoryURL(cloneURL).applyGit(b.PlainGitBootstrapper)
return err
}
func (b *GitProviderBootstrapper) reconcileDeployKey(ctx context.Context, secret corev1.Secret, options sourcesecret.Options) error {
if b.repository == nil {
return errors.New("repository is required")
}
ppk, ok := secret.StringData[sourcesecret.PublicKeySecretKey]
if !ok {
return nil
}
b.logger.Successf("public key: %s", strings.TrimSpace(ppk))
name := deployKeyName(options.Namespace, b.branch, options.Name, options.TargetPath)
deployKeyInfo := newDeployKeyInfo(name, ppk, b.readWriteKey)
_, changed, err := b.repository.DeployKeys().Reconcile(ctx, deployKeyInfo)
if err != nil {
return err
}
if changed {
b.logger.Successf("configured deploy key %q for %q", deployKeyInfo.Name, b.repository.Repository().String())
}
return nil
}
// reconcileOrgRepository reconciles a gitprovider.OrgRepository
// with the GitProviderBootstrapper values, including any
// gitprovider.TeamAccessInfo configurations.
//
// If one of the gitprovider.TeamAccessInfo does not reconcile
// successfully, the gitprovider.UserRepository and an
// ErrReconciledWithWarning error are returned.
func (b *GitProviderBootstrapper) reconcileOrgRepository(ctx context.Context) (gitprovider.UserRepository, error) {
b.logger.Actionf("connecting to %s", b.provider.SupportedDomain())
// Construct the repository and other configuration objects
// go-git-provider likes to work with
subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repositoryName)
orgRef, err := b.getOrganization(ctx, subOrgs)
if err != nil {
return nil, fmt.Errorf("failed to create new Git repository %q: %w", b.repositoryName, err)
}
repoRef := newOrgRepositoryRef(*orgRef, repoName)
repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility)
// Reconcile the organization repository
repo, err := b.provider.OrgRepositories().Get(ctx, repoRef)
if err != nil {
if !errors.Is(err, gitprovider.ErrNotFound) {
return nil, fmt.Errorf("failed to get Git repository %q: %w", repoRef.String(), err)
}
// go-git-providers has at present some issues with the idempotency
// of the available Reconcile methods, and setting e.g. the default
// branch correctly. Resort to Create until this has been resolved.
repo, err = b.provider.OrgRepositories().Create(ctx, repoRef, repoInfo)
if err != nil {
return nil, fmt.Errorf("failed to create new Git repository %q: %w", repoRef.String(), err)
}
b.logger.Successf("repository %q created", repoRef.String())
}
var changed bool
if b.reconcile {
if err = retry(1, 2*time.Second, func() (err error) {
changed, err = repo.Reconcile(ctx)
return
}); err != nil {
return nil, fmt.Errorf("failed to reconcile Git repository %q: %w", repoRef.String(), err)
}
if changed {
b.logger.Successf("repository %q reconciled", repoRef.String())
}
}
// Build the team access config
teamAccessInfo, err := buildTeamAccessInfo(b.teams, gitprovider.RepositoryPermissionVar(gitprovider.RepositoryPermissionMaintain))
if err != nil {
return nil, fmt.Errorf("failed to reconcile repository team access: %w", err)
}
// Reconcile the team access config on best effort (that being:
// record the error as a warning, but continue with the
// reconciliation of the others)
var warning error
if count := len(teamAccessInfo); count > 0 {
b.logger.Actionf("reconciling repository permissions")
for _, i := range teamAccessInfo {
var err error
// Don't reconcile team if team already exists and b.reconcile is false
if team, err := repo.TeamAccess().Get(ctx, i.Name); err == nil && !b.reconcile && team != nil {
continue
}
_, changed, err = repo.TeamAccess().Reconcile(ctx, i)
if err != nil {
warning = fmt.Errorf("failed to grant permissions to team: %w", ErrReconciledWithWarning)
b.logger.Failuref("failed to grant %q permissions to %q: %s", *i.Permission, i.Name, err.Error())
} else if changed {
b.logger.Successf("granted %q permissions to %q", *i.Permission, i.Name)
}
}
b.logger.Successf("reconciled repository permissions")
}
return repo, warning
}
// reconcileUserRepository reconciles a gitprovider.UserRepository
// with the GitProviderBootstrapper values. It returns the reconciled
// gitprovider.UserRepository, or an error.
func (b *GitProviderBootstrapper) reconcileUserRepository(ctx context.Context) (gitprovider.UserRepository, error) {
b.logger.Actionf("connecting to %s", b.provider.SupportedDomain())
// Construct the repository and other metadata objects
// go-git-provider likes to work with.
_, repoName := splitSubOrganizationsFromRepositoryName(b.repositoryName)
userRef := newUserRef(b.provider.SupportedDomain(), b.owner)
repoRef := newUserRepositoryRef(userRef, repoName)
repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility)
// Reconcile the user repository
repo, err := b.provider.UserRepositories().Get(ctx, repoRef)
if err != nil {
if !errors.Is(err, gitprovider.ErrNotFound) {
return nil, fmt.Errorf("failed to get Git repository %q: %w", repoRef.String(), err)
}
// go-git-providers has at present some issues with the idempotency
// of the available Reconcile methods, and setting e.g. the default
// branch correctly. Resort to Create until this has been resolved.
repo, err = b.provider.UserRepositories().Create(ctx, repoRef, repoInfo)
if err != nil {
return nil, fmt.Errorf("failed to create new Git repository %q: %w", repoRef.String(), err)
}
b.logger.Successf("repository %q created", repoRef.String())
}
if b.reconcile {
var changed bool
if err = retry(1, 2*time.Second, func() (err error) {
changed, err = repo.Reconcile(ctx)
return
}); err != nil {
return nil, fmt.Errorf("failed to reconcile Git repository %q: %w", repoRef.String(), err)
}
if changed {
b.logger.Successf("repository %q reconciled", repoRef.String())
}
}
return repo, nil
}
// getOrganization retrieves and returns the gitprovider.Organization
// using the GitProviderBootstrapper values.
func (b *GitProviderBootstrapper) getOrganization(ctx context.Context, subOrgs []string) (*gitprovider.OrganizationRef, error) {
orgRef := newOrganizationRef(b.provider.SupportedDomain(), b.owner, subOrgs)
// With Stash get the organization to be sure to get the correct key
if string(b.provider.ProviderID()) == string(provider.GitProviderStash) {
org, err := b.provider.Organizations().Get(ctx, orgRef)
if err != nil {
return nil, fmt.Errorf("failed to get Git organization: %w", err)
}
orgRef = org.Organization()
return &orgRef, nil
}
return &orgRef, nil
}
// getCloneURL returns the Git clone URL for the given
// gitprovider.UserRepository. If the given transport type is
// gitprovider.TransportTypeSSH and a custom SSH hostname is configured,
// the hostname of the URL will be modified to this hostname.
func (b *GitProviderBootstrapper) getCloneURL(repository gitprovider.UserRepository, transport gitprovider.TransportType) (string, error) {
var url string
if cloner, ok := repository.(gitprovider.CloneableURL); ok {
url = cloner.GetCloneURL("", transport)
} else {
url = repository.Repository().GetCloneURL(transport)
}
var err error
if transport == gitprovider.TransportTypeSSH && b.sshHostname != "" {
if url, err = setHostname(url, b.sshHostname); err != nil {
err = fmt.Errorf("failed to set SSH hostname for URL %q: %w", url, err)
}
}
return url, err
}
// splitSubOrganizationsFromRepositoryName removes any prefixed sub
// organizations from the given repository name by splitting the
// string into a slice by '/'.
// The last (or only) item of the slice result is assumed to be the
// repository name, other items (nested) sub organizations.
func splitSubOrganizationsFromRepositoryName(name string) ([]string, string) {
elements := strings.Split(name, "/")
i := len(elements)
switch i {
case 1:
return nil, name
default:
return elements[:i-1], elements[i-1]
}
}
// buildTeamAccessInfo constructs a gitprovider.TeamAccessInfo slice
// from the given string map of team names to permissions.
//
// Providing a default gitprovider.RepositoryPermission is optional,
// and omitting it will make it default to the go-git-provider default.
//
// An error is returned if any of the given permissions is invalid.
func buildTeamAccessInfo(m map[string]string, defaultPermissions *gitprovider.RepositoryPermission) ([]gitprovider.TeamAccessInfo, error) {
var infos []gitprovider.TeamAccessInfo
if defaultPermissions != nil {
if err := gitprovider.ValidateRepositoryPermission(*defaultPermissions); err != nil {
return nil, fmt.Errorf("invalid default team permission %q", *defaultPermissions)
}
}
for n, p := range m {
permission := defaultPermissions
if p != "" {
p := gitprovider.RepositoryPermission(p)
if err := gitprovider.ValidateRepositoryPermission(p); err != nil {
return nil, fmt.Errorf("invalid permission %q for team %q", p, n)
}
permission = &p
}
i := gitprovider.TeamAccessInfo{
Name: n,
Permission: permission,
}
infos = append(infos, i)
}
return infos, nil
}
// newOrganizationRef constructs a gitprovider.OrganizationRef with the
// given values and returns the result.
func newOrganizationRef(domain, organization string, subOrganizations []string) gitprovider.OrganizationRef {
return gitprovider.OrganizationRef{
Domain: domain,
Organization: organization,
SubOrganizations: subOrganizations,
}
}
// newOrgRepositoryRef constructs a gitprovider.OrgRepositoryRef with
// the given values and returns the result.
func newOrgRepositoryRef(organizationRef gitprovider.OrganizationRef, name string) gitprovider.OrgRepositoryRef {
return gitprovider.OrgRepositoryRef{
OrganizationRef: organizationRef,
RepositoryName: name,
}
}
// newUserRef constructs a gitprovider.UserRef with the given values
// and returns the result.
func newUserRef(domain, login string) gitprovider.UserRef {
return gitprovider.UserRef{
Domain: domain,
UserLogin: login,
}
}
// newUserRepositoryRef constructs a gitprovider.UserRepositoryRef with
// the given values and returns the result.
func newUserRepositoryRef(userRef gitprovider.UserRef, name string) gitprovider.UserRepositoryRef {
return gitprovider.UserRepositoryRef{
UserRef: userRef,
RepositoryName: name,
}
}
// newRepositoryInfo constructs a gitprovider.RepositoryInfo with the
// given values and returns the result.
func newRepositoryInfo(description, defaultBranch, visibility string) gitprovider.RepositoryInfo {
var i gitprovider.RepositoryInfo
if description != "" {
i.Description = gitprovider.StringVar(description)
}
if defaultBranch != "" {
i.DefaultBranch = gitprovider.StringVar(defaultBranch)
}
if visibility != "" {
i.Visibility = gitprovider.RepositoryVisibilityVar(gitprovider.RepositoryVisibility(visibility))
}
return i
}
// newDeployKeyInfo constructs a gitprovider.DeployKeyInfo with the
// given values and returns the result.
func newDeployKeyInfo(name, publicKey string, readWrite bool) gitprovider.DeployKeyInfo {
keyInfo := gitprovider.DeployKeyInfo{
Name: name,
Key: []byte(publicKey),
}
if readWrite {
keyInfo.ReadOnly = gitprovider.BoolVar(false)
}
return keyInfo
}
func deployKeyName(namespace, secretName, branch, path string) string {
var name string
for _, v := range []string{namespace, secretName, branch, path} {
if v == "" {
continue
}
if name == "" {
name = v
} else {
name = name + "-" + v
}
}
return name
}
// setHostname is a helper to replace the hostname of the given URL.
// TODO(hidde): support for this should be added in go-git-providers.
func setHostname(URL, hostname string) (string, error) {
u, err := url.Parse(URL)
if err != nil {
return URL, err
}
u.Host = hostname
return u.String(), nil
}

View File

@@ -0,0 +1,42 @@
package git
// Option is a some configuration that modifies options for a commit.
type Option interface {
// ApplyToCommit applies this configuration to a given commit option.
ApplyToCommit(*CommitOptions)
}
// CommitOptions contains options for making a commit.
type CommitOptions struct {
*GPGSigningInfo
}
// GPGSigningInfo contains information for signing a commit.
type GPGSigningInfo struct {
KeyRingPath string
Passphrase string
KeyID string
}
type GpgSigningOption struct {
*GPGSigningInfo
}
func (w GpgSigningOption) ApplyToCommit(in *CommitOptions) {
in.GPGSigningInfo = w.GPGSigningInfo
}
func WithGpgSigningOption(path, passphrase, keyID string) Option {
// Return nil if no path is set, even if other options are configured.
if path == "" {
return GpgSigningOption{}
}
return GpgSigningOption{
GPGSigningInfo: &GPGSigningInfo{
KeyRingPath: path,
Passphrase: passphrase,
KeyID: keyID,
},
}
}

52
pkg/bootstrap/git/git.go Normal file
View File

@@ -0,0 +1,52 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package git
import (
"context"
"errors"
"io"
)
var (
ErrNoGitRepository = errors.New("no git repository")
ErrNoStagedFiles = errors.New("no staged files")
)
type Author struct {
Name string
Email string
}
type Commit struct {
Author
Hash string
Message string
}
// Git is an interface for basic Git operations on a single branch of a
// remote repository.
type Git interface {
Init(url, branch string) (bool, error)
Clone(ctx context.Context, url, branch string, caBundle []byte) (bool, error)
Write(path string, reader io.Reader) error
Commit(message Commit, options ...Option) (string, error)
Push(ctx context.Context, caBundle []byte) error
Status() (bool, error)
Head() (string, error)
Path() string
}

View File

@@ -0,0 +1,296 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package gogit
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"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"
"github.com/fluxcd/flux2/pkg/bootstrap/git"
)
type GoGit struct {
path string
auth transport.AuthMethod
repository *gogit.Repository
}
type CommitOptions struct {
GpgKeyPath string
GpgKeyPassphrase string
KeyID string
}
func New(path string, auth transport.AuthMethod) *GoGit {
return &GoGit{
path: path,
auth: auth,
}
}
func (g *GoGit) Init(url, branch string) (bool, error) {
if g.repository != nil {
return false, nil
}
r, err := gogit.PlainInit(g.path, false)
if err != nil {
return false, err
}
if _, err = r.CreateRemote(&config.RemoteConfig{
Name: gogit.DefaultRemoteName,
URLs: []string{url},
}); err != nil {
return false, err
}
branchRef := plumbing.NewBranchReferenceName(branch)
if err = r.CreateBranch(&config.Branch{
Name: branch,
Remote: gogit.DefaultRemoteName,
Merge: branchRef,
}); err != nil {
return false, err
}
// PlainInit assumes the initial branch to always be master, we can
// overwrite this by setting the reference of the Storer to a new
// symbolic reference (as there are no commits yet) that points
// the HEAD to our new branch.
if err = r.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, branchRef)); err != nil {
return false, err
}
g.repository = r
return true, nil
}
func (g *GoGit) Clone(ctx context.Context, url, branch string, caBundle []byte) (bool, error) {
branchRef := plumbing.NewBranchReferenceName(branch)
r, err := gogit.PlainCloneContext(ctx, g.path, false, &gogit.CloneOptions{
URL: url,
Auth: g.auth,
RemoteName: gogit.DefaultRemoteName,
ReferenceName: branchRef,
SingleBranch: true,
NoCheckout: false,
Progress: nil,
Tags: gogit.NoTags,
CABundle: caBundle,
})
if err != nil {
if err == transport.ErrEmptyRemoteRepository || isRemoteBranchNotFoundErr(err, branchRef.String()) {
return g.Init(url, branch)
}
return false, err
}
g.repository = r
return true, nil
}
func (g *GoGit) Write(path string, reader io.Reader) error {
if g.repository == nil {
return git.ErrNoGitRepository
}
wt, err := g.repository.Worktree()
if err != nil {
return err
}
f, err := wt.Filesystem.Create(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, reader)
return err
}
func (g *GoGit) Commit(message git.Commit, opts ...git.Option) (string, error) {
if g.repository == nil {
return "", git.ErrNoGitRepository
}
wt, err := g.repository.Worktree()
if err != nil {
return "", err
}
status, err := wt.Status()
if err != nil {
return "", err
}
// apply the options
options := &git.CommitOptions{}
for _, opt := range opts {
opt.ApplyToCommit(options)
}
// go-git has [a bug](https://github.com/go-git/go-git/issues/253)
// whereby it thinks broken symlinks to absolute paths are
// modified. There's no circumstance in which we want to commit a
// change to a broken symlink: so, detect and skip those.
var changed bool
for file, _ := range status {
abspath := filepath.Join(g.path, file)
info, err := os.Lstat(abspath)
if err != nil {
return "", fmt.Errorf("checking if %s is a symlink: %w", file, err)
}
if info.Mode()&os.ModeSymlink > 0 {
// symlinks are OK; broken symlinks are probably a result
// of the bug mentioned above, but not of interest in any
// case.
if _, err := os.Stat(abspath); os.IsNotExist(err) {
continue
}
}
_, _ = wt.Add(file)
changed = true
}
if !changed {
head, err := g.repository.Head()
if err != nil {
return "", err
}
return head.Hash().String(), git.ErrNoStagedFiles
}
commitOpts := &gogit.CommitOptions{
Author: &object.Signature{
Name: message.Name,
Email: message.Email,
When: time.Now(),
},
}
if options.GPGSigningInfo != nil {
entity, err := getOpenPgpEntity(*options.GPGSigningInfo)
if err != nil {
return "", err
}
commitOpts.SignKey = entity
}
commit, err := wt.Commit(message.Message, commitOpts)
if err != nil {
return "", err
}
return commit.String(), nil
}
func (g *GoGit) Push(ctx context.Context, caBundle []byte) error {
if g.repository == nil {
return git.ErrNoGitRepository
}
return g.repository.PushContext(ctx, &gogit.PushOptions{
RemoteName: gogit.DefaultRemoteName,
Auth: g.auth,
Progress: nil,
CABundle: caBundle,
})
}
func (g *GoGit) Status() (bool, error) {
if g.repository == nil {
return false, git.ErrNoGitRepository
}
wt, err := g.repository.Worktree()
if err != nil {
return false, err
}
status, err := wt.Status()
if err != nil {
return false, err
}
return status.IsClean(), nil
}
func (g *GoGit) Head() (string, error) {
if g.repository == nil {
return "", git.ErrNoGitRepository
}
head, err := g.repository.Head()
if err != nil {
return "", err
}
return head.Hash().String(), nil
}
func (g *GoGit) Path() string {
return g.path
}
func isRemoteBranchNotFoundErr(err error, ref string) bool {
return strings.Contains(err.Error(), fmt.Sprintf("couldn't find remote ref %q", ref))
}
func getOpenPgpEntity(info git.GPGSigningInfo) (*openpgp.Entity, error) {
r, err := os.Open(info.KeyRingPath)
if err != nil {
return nil, fmt.Errorf("unable to open GPG key ring: %w", err)
}
entityList, err := openpgp.ReadKeyRing(r)
if err != nil {
return nil, err
}
if len(entityList) == 0 {
return nil, fmt.Errorf("empty GPG key ring")
}
var entity *openpgp.Entity
if info.KeyID != "" {
for _, ent := range entityList {
if ent.PrimaryKey.KeyIdString() == info.KeyID {
entity = ent
}
}
if entity == nil {
return nil, fmt.Errorf("no GPG private key matching key id '%s' found", info.KeyID)
}
} else {
entity = entityList[0]
}
err = entity.PrivateKey.Decrypt([]byte(info.Passphrase))
if err != nil {
return nil, fmt.Errorf("unable to decrypt GPG private key: %w", err)
}
return entity, nil
}

View File

@@ -0,0 +1,67 @@
//go:build unit
// +build unit
package gogit
import (
"testing"
"github.com/fluxcd/flux2/pkg/bootstrap/git"
)
func TestGetOpenPgpEntity(t *testing.T) {
tests := []struct {
name string
keyPath string
passphrase string
id string
expectErr bool
}{
{
name: "no default key id given",
keyPath: "testdata/private.key",
passphrase: "flux",
id: "",
expectErr: false,
},
{
name: "key id given",
keyPath: "testdata/private.key",
passphrase: "flux",
id: "0619327DBD777415",
expectErr: false,
},
{
name: "wrong key id",
keyPath: "testdata/private.key",
passphrase: "flux",
id: "0619327DBD777416",
expectErr: true,
},
{
name: "wrong password",
keyPath: "testdata/private.key",
passphrase: "fluxe",
id: "0619327DBD777415",
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gpgInfo := git.GPGSigningInfo{
KeyRingPath: tt.keyPath,
Passphrase: tt.passphrase,
KeyID: tt.id,
}
_, err := getOpenPgpEntity(gpgInfo)
if err != nil && !tt.expectErr {
t.Errorf("unexpected error: %s", err)
}
if err == nil && tt.expectErr {
t.Errorf("expected error when %s", tt.name)
}
})
}
}

Binary file not shown.

156
pkg/bootstrap/options.go Normal file
View File

@@ -0,0 +1,156 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package bootstrap
import (
"k8s.io/cli-runtime/pkg/genericclioptions"
runclient "github.com/fluxcd/pkg/runtime/client"
"github.com/fluxcd/flux2/pkg/bootstrap/git"
"github.com/fluxcd/flux2/pkg/log"
)
type Option interface {
GitOption
GitProviderOption
}
func WithBranch(branch string) Option {
return branchOption(branch)
}
type branchOption string
func (o branchOption) applyGit(b *PlainGitBootstrapper) {
b.branch = string(o)
}
func (o branchOption) applyGitProvider(b *GitProviderBootstrapper) {
o.applyGit(b.PlainGitBootstrapper)
}
func WithAuthor(name, email string) Option {
return authorOption{
Name: name,
Email: email,
}
}
type authorOption git.Author
func (o authorOption) applyGit(b *PlainGitBootstrapper) {
if o.Name != "" {
b.author.Name = o.Name
}
if o.Email != "" {
b.author.Email = o.Email
}
}
func (o authorOption) applyGitProvider(b *GitProviderBootstrapper) {
o.applyGit(b.PlainGitBootstrapper)
}
func WithCABundle(b []byte) Option {
return caBundleOption(b)
}
type caBundleOption []byte
func (o caBundleOption) applyGit(b *PlainGitBootstrapper) {
b.caBundle = o
}
func (o caBundleOption) applyGitProvider(b *GitProviderBootstrapper) {
b.caBundle = o
}
func WithCommitMessageAppendix(appendix string) Option {
return commitMessageAppendixOption(appendix)
}
type commitMessageAppendixOption string
func (o commitMessageAppendixOption) applyGit(b *PlainGitBootstrapper) {
b.commitMessageAppendix = string(o)
}
func (o commitMessageAppendixOption) applyGitProvider(b *GitProviderBootstrapper) {
o.applyGit(b.PlainGitBootstrapper)
}
func WithKubeconfig(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) Option {
return kubeconfigOption{
rcg: rcg,
opts: opts,
}
}
type kubeconfigOption struct {
rcg genericclioptions.RESTClientGetter
opts *runclient.Options
}
func (o kubeconfigOption) applyGit(b *PlainGitBootstrapper) {
b.restClientGetter = o.rcg
b.restClientOptions = o.opts
}
func (o kubeconfigOption) applyGitProvider(b *GitProviderBootstrapper) {
o.applyGit(b.PlainGitBootstrapper)
}
func WithLogger(logger log.Logger) Option {
return loggerOption{logger}
}
type loggerOption struct {
logger log.Logger
}
func (o loggerOption) applyGit(b *PlainGitBootstrapper) {
b.logger = o.logger
}
func (o loggerOption) applyGitProvider(b *GitProviderBootstrapper) {
b.logger = o.logger
}
func WithGitCommitSigning(path, passphrase, keyID string) Option {
return gitCommitSigningOption{
gpgKeyRingPath: path,
gpgPassphrase: passphrase,
gpgKeyID: keyID,
}
}
type gitCommitSigningOption struct {
gpgKeyRingPath string
gpgPassphrase string
gpgKeyID string
}
func (o gitCommitSigningOption) applyGit(b *PlainGitBootstrapper) {
b.gpgKeyRingPath = o.gpgKeyRingPath
b.gpgPassphrase = o.gpgPassphrase
b.gpgKeyID = o.gpgKeyID
}
func (o gitCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) {
o.applyGit(b.PlainGitBootstrapper)
}

View File

@@ -0,0 +1,76 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
import (
"fmt"
"github.com/fluxcd/go-git-providers/github"
"github.com/fluxcd/go-git-providers/gitlab"
"github.com/fluxcd/go-git-providers/gitprovider"
"github.com/fluxcd/go-git-providers/stash"
)
// BuildGitProvider builds a gitprovider.Client for the provided
// Config. It returns an error if the Config.Provider
// is not supported, or if the construction of the client fails.
func BuildGitProvider(config Config) (gitprovider.Client, error) {
var client gitprovider.Client
var err error
switch config.Provider {
case GitProviderGitHub:
opts := []gitprovider.ClientOption{
gitprovider.WithOAuth2Token(config.Token),
}
if config.Hostname != "" {
opts = append(opts, gitprovider.WithDomain(config.Hostname))
}
if config.CaBundle != nil {
opts = append(opts, gitprovider.WithCustomCAPostChainTransportHook(config.CaBundle))
}
if client, err = github.NewClient(opts...); err != nil {
return nil, err
}
case GitProviderGitLab:
opts := []gitprovider.ClientOption{
gitprovider.WithConditionalRequests(true),
}
if config.Hostname != "" {
opts = append(opts, gitprovider.WithDomain(config.Hostname))
}
if config.CaBundle != nil {
opts = append(opts, gitprovider.WithCustomCAPostChainTransportHook(config.CaBundle))
}
if client, err = gitlab.NewClient(config.Token, "", opts...); err != nil {
return nil, err
}
case GitProviderStash:
opts := []gitprovider.ClientOption{}
if config.Hostname != "" {
opts = append(opts, gitprovider.WithDomain(config.Hostname))
}
if config.CaBundle != nil {
opts = append(opts, gitprovider.WithCustomCAPostChainTransportHook(config.CaBundle))
}
if client, err = stash.NewStashClient(config.Username, config.Token, opts...); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported Git provider '%s'", config.Provider)
}
return client, err
}

View File

@@ -0,0 +1,47 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
// GitProvider holds a Git provider definition.
type GitProvider string
const (
GitProviderGitHub GitProvider = "github"
GitProviderGitLab GitProvider = "gitlab"
GitProviderStash GitProvider = "stash"
)
// Config defines the configuration for connecting to a GitProvider.
type Config struct {
// Provider defines the GitProvider.
Provider GitProvider
// Hostname is the HTTP/S hostname of the Provider,
// e.g. github.example.com.
Hostname string
// Username contains the username used to authenticate with
// the Provider.
Username string
// Token contains the token used to authenticate with the
// Provider.
Token string
// CABunle contains the CA bundle to use for the client.
CaBundle []byte
}