mirror of
				https://github.com/fluxcd/flux2.git
				synced 2025-10-31 08:17:19 +08:00 
			
		
		
		
	 7481c6beb0
			
		
	
	7481c6beb0
	
	
	
		
			
			We have observed that the code at times outperforms GitHub mechanics, resulting in not found errors that are only true for a millisecond. Retrying those actions once with a 2 second delay should be more friendly to users. Signed-off-by: Hidde Beydals <hello@hidde.co>
		
			
				
	
	
		
			331 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			331 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| 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"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	corev1 "k8s.io/api/core/v1"
 | |
| 	"k8s.io/apimachinery/pkg/runtime/schema"
 | |
| 	"k8s.io/apimachinery/pkg/util/wait"
 | |
| 	"sigs.k8s.io/cli-utils/pkg/object"
 | |
| 	"sigs.k8s.io/controller-runtime/pkg/client"
 | |
| 	"sigs.k8s.io/kustomize/api/filesys"
 | |
| 	"sigs.k8s.io/yaml"
 | |
| 
 | |
| 	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
 | |
| 
 | |
| 	"github.com/fluxcd/flux2/internal/bootstrap/git"
 | |
| 
 | |
| 	"github.com/fluxcd/flux2/internal/utils"
 | |
| 	"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
 | |
| 
 | |
| 	author                git.Author
 | |
| 	commitMessageAppendix string
 | |
| 
 | |
| 	kubeconfig  string
 | |
| 	kubecontext string
 | |
| 
 | |
| 	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) 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)
 | |
| 			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
 | |
| 	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,
 | |
| 	})
 | |
| 	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); 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)
 | |
| 		kubectlArgs := []string{"apply", "-f", filepath.Join(b.git.Path(), manifests.Path)}
 | |
| 		if _, err = utils.ExecKubectlCommand(ctx, utils.ModeStderrOS, b.kubeconfig, b.kubecontext, kubectlArgs...); 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, pollInterval, timeout time.Duration) 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)
 | |
| 				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)
 | |
| 	}
 | |
| 	kusManifests, err := kustomization.Generate(kustomization.Options{
 | |
| 		FileSystem: filesys.MakeFsOnDisk(),
 | |
| 		BaseDir:    b.git.Path(),
 | |
| 		TargetPath: filepath.Dir(manifests.Path),
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("kustomization.yaml generation failed: %w", 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
 | |
| 	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,
 | |
| 	})
 | |
| 	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)
 | |
| 		if err = b.git.Push(ctx); err != nil {
 | |
| 			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")
 | |
| 	kubectlArgs := []string{"apply", "-k", filepath.Join(b.git.Path(), filepath.Dir(kusManifests.Path))}
 | |
| 	if _, err = utils.ExecKubectlCommand(ctx, utils.ModeStderrOS, b.kubeconfig, b.kubecontext, kubectlArgs...); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	b.logger.Successf("applied sync manifests")
 | |
| 
 | |
| 	// Wait till Kustomization is reconciled
 | |
| 	var k kustomizev1.Kustomization
 | |
| 	expectRevision := fmt.Sprintf("%s/%s", options.Branch, commit)
 | |
| 	if err := wait.PollImmediate(pollInterval, timeout, kustomizationReconciled(
 | |
| 		ctx, b.kube, client.ObjectKey{Name: options.Name, Namespace: options.Namespace}, &k, expectRevision),
 | |
| 	); err != nil {
 | |
| 		return fmt.Errorf("failed waiting for Kustomization: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	b.logger.Successf("reconciled sync configuration")
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (b *PlainGitBootstrapper) ConfirmHealthy(ctx context.Context, install install.Options, timeout time.Duration) error {
 | |
| 	cfg, err := utils.KubeConfig(b.kubeconfig, b.kubecontext)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	checker, err := status.NewStatusChecker(cfg, 2*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
 | |
| }
 |