1
0
mirror of synced 2026-03-18 00:46:56 +00:00

Merge branch 'main' into patch-3

This commit is contained in:
lloydchang
2024-01-19 01:27:15 -08:00
committed by GitHub
528 changed files with 23323 additions and 9621 deletions

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

@@ -0,0 +1,273 @@
/*
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/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
apierrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sync"
)
var (
ErrReconciledWithWarning = errors.New("reconciled with warning")
)
// Reconciler reconciles and reports the health of different
// components and kubernetes resources involved in the installation of Flux.
//
// It is recommended use the `ReconcilerWithSyncCheck` interface that also
// reports the health of the GitRepository.
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
}
// ReconcilerWithSyncCheck extends the Reconciler interface to also report the health of the GitReposiotry
// that syncs Flux on the cluster
type ReconcilerWithSyncCheck interface {
Reconciler
// ReportGitRepoHealth reports about the health of the GitRepository synchronizing the components.
ReportGitRepoHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) 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 errs []error
if r, ok := reconciler.(ReconcilerWithSyncCheck); ok {
if err := r.ReportGitRepoHealth(ctx, syncOpts, pollInterval, timeout); err != nil {
errs = append(errs, err)
}
}
if err := reconciler.ReportKustomizationHealth(ctx, syncOpts, pollInterval, timeout); err != nil {
errs = append(errs, err)
}
if err := reconciler.ReportComponentsHealth(ctx, installOpts, timeout); err != nil {
errs = append(errs, err)
}
if len(errs) > 0 {
err = fmt.Errorf("bootstrap failed with %d health check failure(s): %w", len(errs), apierrors.NewAggregate(errs))
}
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
}
type objectWithConditions interface {
client.Object
GetConditions() []metav1.Condition
}
func objectReconciled(kube client.Client, objKey client.ObjectKey, clientObject objectWithConditions, expectRevision string) wait.ConditionWithContextFunc {
return func(ctx context.Context) (bool, error) {
// for some reason, TypeMeta gets unset after kube.Get so we want to store the GVK and set it after
// ref https://github.com/kubernetes-sigs/controller-runtime/issues/1517#issuecomment-844703142
gvk := clientObject.GetObjectKind().GroupVersionKind()
if err := kube.Get(ctx, objKey, clientObject); err != nil {
return false, err
}
clientObject.GetObjectKind().SetGroupVersionKind(gvk)
kind := gvk.Kind
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(clientObject)
if err != nil {
return false, err
}
// Detect suspended object, as this would result in an endless wait
if suspended, ok, _ := unstructured.NestedBool(obj, "spec", "suspend"); ok && suspended {
return false, fmt.Errorf("%s '%s' is suspended", kind, objKey.String())
}
// Confirm the state we are observing is for the current generation
if generation, ok, _ := unstructured.NestedInt64(obj, "status", "observedGeneration"); ok && generation != clientObject.GetGeneration() {
return false, nil
}
// Confirm the resource is healthy
if c := apimeta.FindStatusCondition(clientObject.GetConditions(), meta.ReadyCondition); c != nil {
switch c.Status {
case metav1.ConditionTrue:
// Confirm the given revision has been attempted by the controller
hasRev, err := hasRevision(kind, obj, expectRevision)
if err != nil {
return false, err
}
return hasRev, nil
case metav1.ConditionFalse:
return false, fmt.Errorf(c.Message)
}
}
return false, nil
}
}
// hasRevision checks that the reconciled revision (for Kustomization this is `.status.lastAttemptedRevision`
// and for Source APIs, it is stored in `.status.artifact.revision`) is the same as the expectedRev
func hasRevision(kind string, obj map[string]interface{}, expectedRev string) (bool, error) {
var rev string
switch kind {
case sourcev1.GitRepositoryKind, sourcev1b2.OCIRepositoryKind, sourcev1b2.BucketKind, sourcev1b2.HelmChartKind:
rev, _, _ = unstructured.NestedString(obj, "status", "artifact", "revision")
case kustomizev1.KustomizationKind:
rev, _, _ = unstructured.NestedString(obj, "status", "lastAttemptedRevision")
default:
return false, fmt.Errorf("cannot get status revision for kind: '%s'", kind)
}
return sourcev1b2.TransformLegacyRevision(rev) == expectedRev, 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,541 @@
/*
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"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
gogit "github.com/go-git/go-git/v5"
corev1 "k8s.io/api/core/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/yaml"
"github.com/fluxcd/cli-utils/pkg/object"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/repository"
"github.com/fluxcd/pkg/kustomize/filesys"
runclient "github.com/fluxcd/pkg/runtime/client"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/log"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/kustomization"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sync"
"github.com/fluxcd/flux2/v2/pkg/status"
)
type PlainGitBootstrapper struct {
url string
branch string
signature git.Signature
commitMessageAppendix string
gpgKeyRing openpgp.EntityList
gpgPassphrase string
gpgKeyID string
restClientGetter genericclioptions.RESTClientGetter
restClientOptions *runclient.Options
postGenerateSecret []PostGenerateSecretFunc
gitClient repository.Client
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 repository.Client, kube client.Client, opts ...GitOption) (*PlainGitBootstrapper, error) {
b := &PlainGitBootstrapper{
gitClient: 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.gitClient.Head(); 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) {
if err = b.cleanGitRepoDir(); err != nil {
b.logger.Warningf(" failed to clean directory for git repo: %w", err)
return
}
_, err = b.gitClient.Clone(ctx, b.url, repository.CloneConfig{
CheckoutStrategy: repository.CheckoutStrategy{
Branch: b.branch,
},
})
if err != nil {
b.logger.Warningf(" clone failure: %s", err)
}
if err == nil {
cloned = true
}
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 generated files and make a commit
var signer *openpgp.Entity
if b.gpgKeyRing != nil {
signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID)
if err != nil {
return fmt.Errorf("failed to generate OpenPGP entity: %w", err)
}
}
commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version)
if b.commitMessageAppendix != "" {
commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix
}
commit, err := b.gitClient.Commit(git.Commit{
Author: b.signature,
Message: commitMsg,
}, repository.WithFiles(map[string]io.Reader{
manifests.Path: strings.NewReader(manifests.Content),
}), repository.WithSigner(signer))
if err != nil && err != git.ErrNoStagedFiles {
return fmt.Errorf("failed to commit component manifests: %w", err)
}
if err == nil {
b.logger.Successf("committed component manifests to %q (%q)", b.branch, commit)
b.logger.Actionf("pushing component manifests to %q", b.url)
if err = b.gitClient.Push(ctx, repository.PushConfig{}); 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.gitClient.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.gitClient.Path(), kfile); err != nil {
return err
}
} else {
// Apply the CRDs and controllers
if _, err := utils.Apply(ctx, b.restClientGetter, b.restClientOptions, b.gitClient.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 && options.Keypair == nil && len(options.CAFile) == 0 && len(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.gitClient.Head(); 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) {
if err = b.cleanGitRepoDir(); err != nil {
b.logger.Warningf(" failed to clean directory for git repo: %w", err)
return
}
_, err = b.gitClient.Clone(ctx, b.url, repository.CloneConfig{
CheckoutStrategy: repository.CheckoutStrategy{
Branch: b.branch,
},
})
if err != nil {
b.logger.Warningf(" clone failure: %s", err)
}
if err == nil {
cloned = true
}
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)
}
// Create secure Kustomize FS
fs, err := filesys.MakeFsOnDiskSecureBuild(b.gitClient.Path())
if err != nil {
return fmt.Errorf("failed to initialize Kustomize file system: %w", err)
}
if err = fs.WriteFile(filepath.Join(b.gitClient.Path(), manifests.Path), []byte(manifests.Content)); err != nil {
return err
}
// Generate Kustomization
kusManifests, err := kustomization.Generate(kustomization.Options{
FileSystem: fs,
BaseDir: b.gitClient.Path(),
TargetPath: filepath.Dir(manifests.Path),
})
if err != nil {
return fmt.Errorf("%s generation failed: %w", konfig.DefaultKustomizationFileName(), err)
}
b.logger.Successf("generated sync manifests")
// Write generated files and make a commit
var signer *openpgp.Entity
if b.gpgKeyRing != nil {
signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID)
if err != nil {
return fmt.Errorf("failed to generate OpenPGP entity: %w", err)
}
}
commitMsg := "Add Flux sync manifests"
if b.commitMessageAppendix != "" {
commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix
}
commit, err := b.gitClient.Commit(git.Commit{
Author: b.signature,
Message: commitMsg,
}, repository.WithFiles(map[string]io.Reader{
kusManifests.Path: strings.NewReader(kusManifests.Content),
}), repository.WithSigner(signer))
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.gitClient.Push(ctx, repository.PushConfig{})
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.gitClient.Path()); err != nil {
return fmt.Errorf("failed to remove tmp dir: %w", err)
}
if err := os.Mkdir(b.gitClient.Path(), 0o700); err != nil {
return fmt.Errorf("failed to recreate tmp dir: %w", err)
}
if err = retry(1, 2*time.Second, func() (err error) {
if err = b.cleanGitRepoDir(); err != nil {
b.logger.Warningf(" failed to clean directory for git repo: %w", err)
return
}
_, err = b.gitClient.Clone(ctx, b.url, repository.CloneConfig{
CheckoutStrategy: repository.CheckoutStrategy{
Branch: b.branch,
},
})
if err != nil {
b.logger.Warningf(" clone failure: %s", err)
}
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.gitClient.Path(), filepath.Join(b.gitClient.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.gitClient.Head()
if err != nil {
return err
}
objKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace}
expectRevision := fmt.Sprintf("%s@%s", options.Branch, git.Hash(head).Digest())
b.logger.Waitingf("waiting for Kustomization %q to be reconciled", objKey.String())
k := &kustomizev1.Kustomization{
TypeMeta: metav1.TypeMeta{
Kind: kustomizev1.KustomizationKind,
},
}
if err := wait.PollUntilContextTimeout(ctx, pollInterval, timeout, true,
objectReconciled(b.kube, objKey, k, expectRevision)); err != nil {
// If the poll timed out, we want to log the ready condition message as
// that likely contains the reason
if errors.Is(err, context.DeadlineExceeded) {
readyCondition := apimeta.FindStatusCondition(k.Status.Conditions, meta.ReadyCondition)
if readyCondition != nil && readyCondition.Status != metav1.ConditionTrue {
err = fmt.Errorf("kustomization '%s' not ready: '%s'", objKey, readyCondition.Message)
}
}
b.logger.Failuref(err.Error())
return fmt.Errorf("error while waiting for Kustomization to be ready: '%s'", err)
}
b.logger.Successf("Kustomization reconciled successfully")
return nil
}
func (b *PlainGitBootstrapper) ReportGitRepoHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error {
head, err := b.gitClient.Head()
if err != nil {
return err
}
objKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace}
b.logger.Waitingf("waiting for GitRepository %q to be reconciled", objKey.String())
expectRevision := fmt.Sprintf("%s@%s", options.Branch, git.Hash(head).Digest())
g := &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
Kind: sourcev1.GitRepositoryKind,
APIVersion: sourcev1.GroupVersion.String(),
},
}
if err := wait.PollUntilContextTimeout(ctx, pollInterval, timeout, true,
objectReconciled(b.kube, objKey, g, expectRevision)); err != nil {
// If the poll timed out, we want to log the ready condition message as
// that likely contains the reason
if errors.Is(err, context.DeadlineExceeded) {
readyCondition := apimeta.FindStatusCondition(g.Status.Conditions, meta.ReadyCondition)
if readyCondition != nil && readyCondition.Status != metav1.ConditionTrue {
err = fmt.Errorf("gitrepository '%s' not ready: '%s'", objKey, readyCondition.Message)
}
}
b.logger.Failuref(err.Error())
return fmt.Errorf("error while waiting for GitRepository to be ready: '%s'", err)
}
b.logger.Successf("GitRepository 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
}
// cleanGitRepoDir cleans the directory meant for the Git repo.
func (b *PlainGitBootstrapper) cleanGitRepoDir() error {
dirs, err := os.ReadDir(b.gitClient.Path())
if err != nil {
return err
}
var errs []error
for _, dir := range dirs {
if err := os.RemoveAll(filepath.Join(b.gitClient.Path(), dir.Name())); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
func getOpenPgpEntity(keyRing openpgp.EntityList, passphrase, keyID string) (*openpgp.Entity, error) {
if len(keyRing) == 0 {
return nil, fmt.Errorf("empty GPG key ring")
}
var entity *openpgp.Entity
if keyID != "" {
keyID = strings.TrimPrefix(keyID, "0x")
if len(keyID) != 16 {
return nil, fmt.Errorf("invalid GPG key id length; expected %d, got %d", 16, len(keyID))
}
keyID = strings.ToUpper(keyID)
for _, ent := range keyRing {
if ent.PrimaryKey.KeyIdString() == keyID {
entity = ent
}
}
if entity == nil {
return nil, fmt.Errorf("no GPG keyring matching key id '%s' found", keyID)
}
if entity.PrivateKey == nil {
return nil, fmt.Errorf("keyring does not contain private key for key id '%s'", keyID)
}
} else {
entity = keyRing[0]
}
err := entity.PrivateKey.Decrypt([]byte(passphrase))
if err != nil {
return nil, fmt.Errorf("unable to decrypt GPG private key: %w", err)
}
return entity, nil
}

View File

@@ -0,0 +1,640 @@
/*
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/v2/pkg/bootstrap/provider"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sync"
"github.com/fluxcd/pkg/git/repository"
)
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
useDeployTokenAuth bool
provider gitprovider.Client
}
func NewGitProviderBootstrapper(git repository.Client, provider gitprovider.Client,
kube client.Client, opts ...GitProviderOption) (*GitProviderBootstrapper, error) {
b := &GitProviderBootstrapper{
PlainGitBootstrapper: &PlainGitBootstrapper{
gitClient: 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 WithDeployTokenAuth() GitProviderOption {
return deployTokenAuthOption(true)
}
type deployTokenAuthOption bool
func (o deployTokenAuthOption) applyGitProvider(b *GitProviderBootstrapper) {
b.useDeployTokenAuth = 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)
}
func (b *GitProviderBootstrapper) ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error {
if b.repository == nil {
return errors.New("repository is required")
}
if b.useDeployTokenAuth {
deployTokenInfo, err := b.reconcileDeployToken(ctx, options)
if err != nil {
return err
}
if deployTokenInfo != nil {
options.Username = deployTokenInfo.Username
options.Password = deployTokenInfo.Token
}
}
return b.PlainGitBootstrapper.ReconcileSourceSecret(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
}
func (b *GitProviderBootstrapper) reconcileDeployToken(ctx context.Context, options sourcesecret.Options) (*gitprovider.DeployTokenInfo, error) {
dts, err := b.repository.DeployTokens()
if err != nil {
return nil, err
}
b.logger.Actionf("checking to reconcile deploy token for source secret")
name := deployTokenName(options.Namespace, b.branch, options.Name, options.TargetPath)
deployTokenInfo := gitprovider.DeployTokenInfo{Name: name}
deployToken, changed, err := dts.Reconcile(ctx, deployTokenInfo)
if err != nil {
return nil, err
}
if changed {
b.logger.Successf("configured deploy token %q for %q", deployTokenInfo.Name, b.repository.Repository().String())
deployTokenInfo := deployToken.Get()
return &deployTokenInfo, nil
}
b.logger.Successf("reconciled deploy token for source secret")
return nil, 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: provider error: %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: provider error: %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 {
var userErr *gitprovider.ErrIncorrectUser
if errors.As(err, &userErr) {
// return a better error message when the wrong owner is set
err = fmt.Errorf("the specified owner '%s' doesn't match the identity associated with the given token", b.owner)
}
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
}
func deployTokenName(namespace, secretName, branch, path string) string {
var elems []string
for _, v := range []string{namespace, secretName, branch, path} {
if v == "" {
continue
}
elems = append(elems, v)
}
return strings.Join(elems, "-")
}
// 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,469 @@
/*
Copyright 2023 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"
"testing"
"github.com/fluxcd/pkg/apis/meta"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/fluxcd/flux2/v2/internal/utils"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
)
func Test_hasRevision(t *testing.T) {
var revision = "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a"
tests := []struct {
name string
obj objectWithConditions
rev string
expectErr bool
expectedBool bool
}{
{
name: "Kustomization revision",
obj: &kustomizev1.Kustomization{
TypeMeta: metav1.TypeMeta{
Kind: kustomizev1.KustomizationKind,
},
Status: kustomizev1.KustomizationStatus{
LastAttemptedRevision: "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a",
},
},
expectedBool: true,
},
{
name: "GitRepository revision",
obj: &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
Kind: sourcev1.GitRepositoryKind,
APIVersion: sourcev1.GroupVersion.String(),
},
Status: sourcev1.GitRepositoryStatus{
Artifact: &sourcev1.Artifact{
Revision: "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a",
},
},
},
expectedBool: true,
},
{
name: "GitRepository revision (wrong revision)",
obj: &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
Kind: sourcev1.GitRepositoryKind,
APIVersion: sourcev1.GroupVersion.String(),
},
Status: sourcev1.GitRepositoryStatus{
Artifact: &sourcev1.Artifact{
Revision: "main@sha1:e7f3a8f9bb0aa5ae8afd6208f43757ab73fc043a",
},
},
},
},
{
name: "Kustomization revision (empty revision)",
obj: &kustomizev1.Kustomization{
TypeMeta: metav1.TypeMeta{
Kind: kustomizev1.KustomizationKind,
},
Status: kustomizev1.KustomizationStatus{
LastAttemptedRevision: "",
},
},
},
{
name: "OCIRepository revision",
obj: &sourcev1b2.OCIRepository{
TypeMeta: metav1.TypeMeta{
Kind: sourcev1b2.OCIRepositoryKind,
},
Status: sourcev1b2.OCIRepositoryStatus{
Artifact: &sourcev1.Artifact{
Revision: "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a",
},
},
},
expectedBool: true,
},
{
name: "Alert revision(Not supported)",
obj: &notificationv1.Alert{
TypeMeta: metav1.TypeMeta{
Kind: notificationv1.AlertKind,
},
Status: notificationv1.AlertStatus{
ObservedGeneration: 1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tt.obj)
g.Expect(err).To(BeNil())
got, err := hasRevision(tt.obj.GetObjectKind().GroupVersionKind().Kind, obj, revision)
if tt.expectErr {
g.Expect(err).To(HaveOccurred())
return
}
g.Expect(got).To(Equal(tt.expectedBool))
})
}
}
func Test_objectReconciled(t *testing.T) {
expectedRev := "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a"
type updateStatus struct {
statusFn func(o client.Object)
expectedErr bool
expectedBool bool
}
tests := []struct {
name string
obj objectWithConditions
statuses []updateStatus
}{
{
name: "GitRepository with no status",
obj: &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
Kind: sourcev1.GitRepositoryKind,
APIVersion: sourcev1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
Namespace: "flux-system",
},
},
statuses: []updateStatus{
{
expectedErr: false,
expectedBool: false,
},
},
},
{
name: "suspended Kustomization",
obj: &kustomizev1.Kustomization{
TypeMeta: metav1.TypeMeta{
Kind: kustomizev1.KustomizationKind,
APIVersion: kustomizev1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
Namespace: "flux-system",
},
Spec: kustomizev1.KustomizationSpec{
Suspend: true,
},
},
statuses: []updateStatus{
{
expectedErr: true,
},
},
},
{
name: "Kustomization - status with old generation",
obj: &kustomizev1.Kustomization{
TypeMeta: metav1.TypeMeta{
Kind: kustomizev1.KustomizationKind,
APIVersion: kustomizev1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
Namespace: "flux-system",
Generation: 1,
},
Status: kustomizev1.KustomizationStatus{
ObservedGeneration: -1,
},
},
statuses: []updateStatus{
{
expectedErr: false,
expectedBool: false,
},
},
},
{
name: "GitRepository - status with same generation but no conditions",
obj: &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
Kind: sourcev1.GitRepositoryKind,
APIVersion: sourcev1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
Namespace: "flux-system",
Generation: 1,
},
Status: sourcev1.GitRepositoryStatus{
ObservedGeneration: 1,
},
},
statuses: []updateStatus{
{
expectedErr: false,
expectedBool: false,
},
},
},
{
name: "GitRepository - status with conditions but no ready condition",
obj: &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
Kind: sourcev1.GitRepositoryKind,
APIVersion: sourcev1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
Namespace: "flux-system",
Generation: 1,
},
Status: sourcev1.GitRepositoryStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{Type: meta.ReconcilingCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Progressing", Message: "Progressing"},
},
},
},
statuses: []updateStatus{
{
expectedErr: false,
expectedBool: false,
},
},
},
{
name: "Kustomization - status with false ready condition",
obj: &kustomizev1.Kustomization{
TypeMeta: metav1.TypeMeta{
Kind: kustomizev1.KustomizationKind,
APIVersion: kustomizev1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
Namespace: "flux-system",
Generation: 1,
},
Status: kustomizev1.KustomizationStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{Type: meta.ReadyCondition, Status: metav1.ConditionFalse, ObservedGeneration: 1, Reason: "Failing", Message: "Failed to clone"},
},
},
},
statuses: []updateStatus{
{
expectedErr: true,
expectedBool: false,
},
},
},
{
name: "Kustomization - status with true ready condition but different revision",
obj: &kustomizev1.Kustomization{
TypeMeta: metav1.TypeMeta{
Kind: kustomizev1.KustomizationKind,
APIVersion: kustomizev1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
Namespace: "flux-system",
Generation: 1,
},
Status: kustomizev1.KustomizationStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Passing", Message: "Applied revision"},
},
LastAttemptedRevision: "main@sha1:e7f3a8f9bb0aa5ae8afd6208f43757ab73fc043a",
},
},
statuses: []updateStatus{
{
expectedErr: false,
expectedBool: false,
},
},
},
{
name: "GitRepository - status with true ready condition but different revision",
obj: &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
Kind: sourcev1.GitRepositoryKind,
APIVersion: sourcev1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
Namespace: "flux-system",
Generation: 1,
},
Status: sourcev1.GitRepositoryStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"},
},
Artifact: &sourcev1.Artifact{
Revision: "main@sha1:e7f3a8f9bb0aa5ae8afd6208f43757ab73fc043a",
},
},
},
statuses: []updateStatus{
{
expectedErr: false,
expectedBool: false,
},
},
},
{
name: "GitRepository - ready with right revision",
obj: &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
Kind: sourcev1.GitRepositoryKind,
APIVersion: sourcev1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
Namespace: "flux-system",
Generation: 1,
},
Status: sourcev1.GitRepositoryStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"},
},
Artifact: &sourcev1.Artifact{
Revision: expectedRev,
},
},
},
statuses: []updateStatus{
{
expectedErr: false,
expectedBool: true,
},
},
},
{
name: "GitRepository - sequence of status updates before ready",
obj: &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
Kind: sourcev1.GitRepositoryKind,
APIVersion: sourcev1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
Namespace: "flux-system",
Generation: 1,
},
},
statuses: []updateStatus{
{
// observed gen different
statusFn: func(o client.Object) {
gitRepo := o.(*sourcev1.GitRepository)
gitRepo.Status = sourcev1.GitRepositoryStatus{
ObservedGeneration: -1,
}
},
},
{
// ready failing
statusFn: func(o client.Object) {
gitRepo := o.(*sourcev1.GitRepository)
gitRepo.Status = sourcev1.GitRepositoryStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{Type: meta.ReadyCondition, Status: metav1.ConditionFalse, ObservedGeneration: 1, Reason: "Not Ready", Message: "Transient connection issue"},
},
}
},
expectedErr: true,
},
{
// updated to a different revision
statusFn: func(o client.Object) {
gitRepo := o.(*sourcev1.GitRepository)
gitRepo.Status = sourcev1.GitRepositoryStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"},
},
Artifact: &sourcev1.Artifact{
Revision: "wrong rev",
},
}
},
},
{
// updated to the expected revision
statusFn: func(o client.Object) {
gitRepo := o.(*sourcev1.GitRepository)
gitRepo.Status = sourcev1.GitRepositoryStatus{
ObservedGeneration: 1,
Conditions: []metav1.Condition{
{Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"},
},
Artifact: &sourcev1.Artifact{
Revision: expectedRev,
},
}
},
expectedBool: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
builder := fake.NewClientBuilder().WithScheme(utils.NewScheme())
builder.WithObjects(tt.obj)
kubeClient := builder.Build()
for _, updates := range tt.statuses {
if updates.statusFn != nil {
updates.statusFn(tt.obj)
g.Expect(kubeClient.Update(context.TODO(), tt.obj)).To(Succeed())
}
waitFunc := objectReconciled(kubeClient, client.ObjectKeyFromObject(tt.obj), tt.obj, expectedRev)
got, err := waitFunc(context.TODO())
g.Expect(err != nil).To(Equal(updates.expectedErr))
g.Expect(got).To(Equal(updates.expectedBool))
}
})
}
}

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

@@ -0,0 +1,161 @@
/*
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 (
"fmt"
"os"
"k8s.io/cli-runtime/pkg/genericclioptions"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/fluxcd/pkg/git"
runclient "github.com/fluxcd/pkg/runtime/client"
"github.com/fluxcd/flux2/v2/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 WithSignature(name, email string) Option {
return signatureOption{
Name: name,
Email: email,
}
}
type signatureOption git.Signature
func (o signatureOption) applyGit(b *PlainGitBootstrapper) {
if o.Name != "" {
b.signature.Name = o.Name
}
if o.Email != "" {
b.signature.Email = o.Email
}
}
func (o signatureOption) applyGitProvider(b *GitProviderBootstrapper) {
o.applyGit(b.PlainGitBootstrapper)
}
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(gpgKeyRing openpgp.EntityList, passphrase, keyID string) Option {
return gitCommitSigningOption{
gpgKeyRing: gpgKeyRing,
gpgPassphrase: passphrase,
gpgKeyID: keyID,
}
}
type gitCommitSigningOption struct {
gpgKeyRing openpgp.EntityList
gpgPassphrase string
gpgKeyID string
}
func (o gitCommitSigningOption) applyGit(b *PlainGitBootstrapper) {
b.gpgKeyRing = o.gpgKeyRing
b.gpgPassphrase = o.gpgPassphrase
b.gpgKeyID = o.gpgKeyID
}
func (o gitCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) {
o.applyGit(b.PlainGitBootstrapper)
}
func LoadEntityListFromPath(path string) (openpgp.EntityList, error) {
if path == "" {
return nil, nil
}
r, err := os.Open(path)
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, fmt.Errorf("unable to read GPG key ring: %w", err)
}
return entityList, nil
}

View File

@@ -0,0 +1,88 @@
/*
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/gitea"
"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 GitProviderGitea:
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 = gitea.NewClient(config.Token, 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,48 @@
/*
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"
GitProviderGitea GitProvider = "gitea"
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
}

31
pkg/log/nop.go Normal file
View File

@@ -0,0 +1,31 @@
/*
Copyright 2022 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 log
type NopLogger struct{}
func (NopLogger) Actionf(format string, a ...interface{}) {}
func (NopLogger) Generatef(format string, a ...interface{}) {}
func (NopLogger) Waitingf(format string, a ...interface{}) {}
func (NopLogger) Successf(format string, a ...interface{}) {}
func (NopLogger) Warningf(format string, a ...interface{}) {}
func (NopLogger) Failuref(format string, a ...interface{}) {}

View File

@@ -27,8 +27,9 @@ import (
"time"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/hashicorp/go-cleanhttp"
"github.com/fluxcd/flux2/pkg/manifestgen"
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
)
// Generate returns the install manifests as a multi-doc YAML.
@@ -54,7 +55,7 @@ func Generate(options Options, manifestsBase string) (*manifestgen.Manifest, err
} else {
// download the manifests base from GitHub
if manifestsBase == "" {
manifestsBase, err = os.MkdirTemp("", options.Namespace)
manifestsBase, err = manifestgen.MkdirTempAbs("", options.Namespace)
if err != nil {
return nil, fmt.Errorf("temp dir error: %w", err)
}
@@ -91,7 +92,7 @@ func Generate(options Options, manifestsBase string) (*manifestgen.Manifest, err
// GetLatestVersion calls the GitHub API and returns the latest released version.
func GetLatestVersion() (string, error) {
ghURL := "https://api.github.com/repos/fluxcd/flux2/releases/latest"
c := http.DefaultClient
c := cleanhttp.DefaultClient()
c.Timeout = 15 * time.Second
res, err := c.Get(ghURL)
@@ -121,7 +122,7 @@ func ExistingVersion(version string) (bool, error) {
}
ghURL := fmt.Sprintf("https://api.github.com/repos/fluxcd/flux2/releases/tags/%s", version)
c := http.DefaultClient
c := cleanhttp.DefaultClient()
c.Timeout = 15 * time.Second
res, err := c.Get(ghURL)

View File

@@ -26,10 +26,12 @@ import (
"path/filepath"
"strings"
"github.com/fluxcd/pkg/untar"
"sigs.k8s.io/kustomize/api/filesys"
"github.com/hashicorp/go-cleanhttp"
"github.com/fluxcd/flux2/pkg/manifestgen/kustomization"
"github.com/fluxcd/pkg/kustomize/filesys"
"github.com/fluxcd/pkg/tar"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/kustomization"
)
func fetch(ctx context.Context, url, version, dir string) error {
@@ -44,7 +46,7 @@ func fetch(ctx context.Context, url, version, dir string) error {
}
// download
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
resp, err := cleanhttp.DefaultClient().Do(req.WithContext(ctx))
if err != nil {
return fmt.Errorf("failed to download manifests.tar.gz from %s, error: %w", ghURL, err)
}
@@ -56,7 +58,7 @@ func fetch(ctx context.Context, url, version, dir string) error {
}
// extract
if _, err = untar.Untar(resp.Body, dir); err != nil {
if err = tar.Untar(resp.Body, dir, tar.WithMaxUntarSize(-1)); err != nil {
return fmt.Errorf("failed to untar manifests.tar.gz from %s, error: %w", ghURL, err)
}
@@ -71,9 +73,9 @@ func generate(base string, options Options) error {
// In such environments they normally add `.cluster.local` and `.local`
// suffixes to `no_proxy` variable in order to prevent cluster-local
// traffic from going through http proxy. Without fully specified
// domain they need to mention `notifications-controller` explicity in
// domain they need to mention `notifications-controller` explicitly in
// `no_proxy` variable after debugging http proxy logs.
options.EventsAddr = fmt.Sprintf("http://%s.%s.svc.%s/", options.NotificationController, options.Namespace, options.ClusterDomain)
options.EventsAddr = fmt.Sprintf("http://%s.%s.svc.%s./", options.NotificationController, options.Namespace, options.ClusterDomain)
}
if err := execTemplate(options, namespaceTmpl, path.Join(base, "namespace.yaml")); err != nil {
@@ -126,8 +128,12 @@ func build(base, output string) error {
return err
}
fs := filesys.MakeFsOnDisk()
if err := fs.WriteFile(output, resources); err != nil {
outputBase := filepath.Dir(strings.TrimSuffix(output, string(filepath.Separator)))
fs, err := filesys.MakeFsOnDiskSecure(outputBase)
if err != nil {
return err
}
if err = fs.WriteFile(output, resources); err != nil {
return err
}

View File

@@ -51,8 +51,6 @@ patches:
- path: node-selector.yaml
target:
kind: Deployment
patchesJson6902:
{{- range $i, $component := .Components }}
{{- if eq $component "notification-controller" }}
- target:
@@ -165,6 +163,9 @@ apiVersion: v1
kind: Namespace
metadata:
name: {{.Namespace}}
labels:
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
`
func execTemplate(obj interface{}, tmpl, filename string) error {

View File

@@ -20,20 +20,23 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/provider"
kustypes "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/pkg/manifestgen"
"github.com/fluxcd/pkg/kustomize/filesys"
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
)
// Generate scans the given directory for Kubernetes manifests and creates a kustomization.yaml
// including all discovered manifests as resources.
// Generate scans the given directory for Kubernetes manifests and creates a
// konfig.DefaultKustomizationFileName file, including all discovered manifests
// as resources.
func Generate(options Options) (*manifestgen.Manifest, error) {
kfile := filepath.Join(options.TargetPath, konfig.DefaultKustomizationFileName())
abskfile := filepath.Join(options.BaseDir, kfile)
@@ -50,7 +53,7 @@ func Generate(options Options) (*manifestgen.Manifest, error) {
return nil
}
if info.IsDir() {
// If a sub-directory contains an existing Kustomization file add the
// If a sub-directory contains an existing Kustomization file, add the
// directory as a resource and do not decent into it.
for _, kfilename := range konfig.RecognizedKustomizationFileNames() {
if options.FileSystem.Exists(filepath.Join(path, kfilename)) {
@@ -88,7 +91,9 @@ func Generate(options Options) (*manifestgen.Manifest, error) {
if err != nil {
return nil, err
}
f.Close()
if err = f.Close(); err != nil {
return nil, err
}
kus := kustypes.Kustomization{
TypeMeta: kustypes.TypeMeta{
@@ -128,20 +133,40 @@ func Generate(options Options) (*manifestgen.Manifest, error) {
}, nil
}
// kustomizeBuildMutex is a workaround for a concurrent map read and map write bug.
// TODO(stefan): https://github.com/kubernetes-sigs/kustomize/issues/3659
var kustomizeBuildMutex sync.Mutex
// Build takes a Kustomize overlays and returns the resulting manifests as multi-doc YAML.
// Build takes the path to a directory with a konfig.RecognizedKustomizationFileNames,
// builds it, and returns the resulting manifests as multi-doc YAML. It restricts the
// Kustomize file system to the parent directory of the base.
func Build(base string) ([]byte, error) {
// TODO(stefan): temporary workaround for concurrent map read and map write bug
// https://github.com/kubernetes-sigs/kustomize/issues/3659
// TODO(hidde): drop this when consumers have moved away to BuildWithRoot.
parent := filepath.Dir(strings.TrimSuffix(base, string(filepath.Separator)))
return BuildWithRoot(parent, base)
}
// BuildWithRoot takes the path to a directory with a konfig.RecognizedKustomizationFileNames,
// builds it, and returns the resulting manifests as multi-doc YAML.
// The Kustomize file system is restricted to root.
func BuildWithRoot(root, base string) ([]byte, error) {
kustomizeBuildMutex.Lock()
defer kustomizeBuildMutex.Unlock()
kfile := filepath.Join(base, konfig.DefaultKustomizationFileName())
fs, err := filesys.MakeFsOnDiskSecureBuild(root)
if err != nil {
return nil, err
}
fs := filesys.MakeFsOnDisk()
if !fs.Exists(kfile) {
return nil, fmt.Errorf("%s not found", kfile)
var kfile string
for _, f := range konfig.RecognizedKustomizationFileNames() {
if kf := filepath.Join(base, f); fs.Exists(kf) {
kfile = kf
break
}
}
if kfile == "" {
return nil, fmt.Errorf("%s not found", konfig.DefaultKustomizationFileName())
}
// TODO(hidde): work around for a bug in kustomize causing it to
@@ -161,11 +186,8 @@ func Build(base string) ([]byte, error) {
}
buildOptions := &krusty.Options{
DoLegacyResourceSort: true,
LoadRestrictions: kustypes.LoadRestrictionsNone,
AddManagedbyLabel: false,
DoPrune: false,
PluginConfig: kustypes.DisabledPluginConfig(),
LoadRestrictions: kustypes.LoadRestrictionsNone,
PluginConfig: kustypes.DisabledPluginConfig(),
}
k := krusty.MakeKustomizer(buildOptions)

View File

@@ -16,7 +16,7 @@ limitations under the License.
package kustomization
import "sigs.k8s.io/kustomize/api/filesys"
import "sigs.k8s.io/kustomize/kyaml/filesys"
type Options struct {
FileSystem filesys.FileSystem
@@ -25,6 +25,8 @@ type Options struct {
}
func MakeDefaultOptions() Options {
// TODO(hidde): switch MakeFsOnDisk to MakeFsOnDiskSecureBuild when we
// break API.
return Options{
FileSystem: filesys.MakeFsOnDisk(),
BaseDir: "",

View File

@@ -34,7 +34,7 @@ type Manifest struct {
Content string
}
// WriteFile writes the YAML content to a file inside the the root path.
// WriteFile writes the YAML content to a file inside the root path.
// If the file does not exist, WriteFile creates it with permissions perm,
// otherwise WriteFile overwrites the file, without changing permissions.
func (m *Manifest) WriteFile(rootDir string) (string, error) {

View File

@@ -18,6 +18,8 @@ package sourcesecret
import (
"crypto/elliptic"
"github.com/fluxcd/pkg/ssh"
)
type PrivateKeyAlgorithm string
@@ -31,30 +33,53 @@ const (
const (
UsernameSecretKey = "username"
PasswordSecretKey = "password"
CAFileSecretKey = "caFile"
CertFileSecretKey = "certFile"
KeyFileSecretKey = "keyFile"
CACrtSecretKey = "ca.crt"
TLSCrtSecretKey = "tls.crt"
TLSKeySecretKey = "tls.key"
PrivateKeySecretKey = "identity"
PublicKeySecretKey = "identity.pub"
KnownHostsSecretKey = "known_hosts"
BearerTokenKey = "bearerToken"
// Deprecated: Replaced by CACrtSecretKey, but kept for backwards
// compatibility with deprecated TLS flags.
CAFileSecretKey = "caFile"
// Deprecated: Replaced by TLSCrtSecretKey, but kept for backwards
// compatibility with deprecated TLS flags.
CertFileSecretKey = "certFile"
// Deprecated: Replaced by TLSKeySecretKey, but kept for backwards
// compatibility with deprecated TLS flags.
KeyFileSecretKey = "keyFile"
)
type Options struct {
Name string
Namespace string
Labels map[string]string
Registry string
SSHHostname string
PrivateKeyAlgorithm PrivateKeyAlgorithm
RSAKeyBits int
ECDSACurve elliptic.Curve
PrivateKeyPath string
Keypair *ssh.KeyPair
Username string
Password string
CAFilePath string
CertFilePath string
KeyFilePath string
CACrt []byte
TLSCrt []byte
TLSKey []byte
TargetPath string
ManifestFile string
BearerToken string
// Deprecated: Replaced by CACrt, but kept for backwards compatibility
// with deprecated TLS flags.
CAFile []byte
// Deprecated: Replaced by TLSCrt, but kept for backwards compatibility
// with deprecated TLS flags.
CertFile []byte
// Deprecated: Replaced by TLSKey, but kept for backwards compatibility
// with deprecated TLS flags.
KeyFile []byte
}
func MakeDefaultOptions() Options {
@@ -63,12 +88,12 @@ func MakeDefaultOptions() Options {
Namespace: "flux-system",
Labels: map[string]string{},
PrivateKeyAlgorithm: RSAPrivateKeyAlgorithm,
PrivateKeyPath: "",
Username: "",
Password: "",
CAFilePath: "",
CertFilePath: "",
KeyFilePath: "",
CAFile: []byte{},
CertFile: []byte{},
KeyFile: []byte{},
ManifestFile: "secret.yaml",
BearerToken: "",
}
}

View File

@@ -18,6 +18,8 @@ package sourcesecret
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"os"
@@ -31,11 +33,32 @@ import (
"github.com/fluxcd/pkg/ssh"
"github.com/fluxcd/flux2/pkg/manifestgen"
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
)
const defaultSSHPort = 22
// types gotten from https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/create/create_secret_docker.go#L64-L84
// DockerConfigJSON represents a local docker auth config file
// for pulling images.
type DockerConfigJSON struct {
Auths DockerConfig `json:"auths"`
}
// DockerConfig represents the config file used by the docker CLI.
// This config that represents the credentials that should be used
// when pulling images from specific image repositories.
type DockerConfig map[string]DockerConfigEntry
// DockerConfigEntry holds the user information that grant the access to docker registry
type DockerConfigEntry struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Email string `json:"email,omitempty"`
Auth string `json:"auth,omitempty"`
}
func Generate(options Options) (*manifestgen.Manifest, error) {
var err error
@@ -43,10 +66,8 @@ func Generate(options Options) (*manifestgen.Manifest, error) {
switch {
case options.Username != "" && options.Password != "":
// noop
case len(options.PrivateKeyPath) > 0:
if keypair, err = loadKeyPair(options.PrivateKeyPath, options.Password); err != nil {
return nil, err
}
case options.Keypair != nil:
keypair = options.Keypair
case len(options.PrivateKeyAlgorithm) > 0:
if keypair, err = generateKeyPair(options); err != nil {
return nil, err
@@ -55,29 +76,20 @@ func Generate(options Options) (*manifestgen.Manifest, error) {
var hostKey []byte
if keypair != nil {
if hostKey, err = scanHostKey(options.SSHHostname); err != nil {
if hostKey, err = ScanHostKey(options.SSHHostname); err != nil {
return nil, err
}
}
var caFile []byte
if options.CAFilePath != "" {
if caFile, err = os.ReadFile(options.CAFilePath); err != nil {
return nil, fmt.Errorf("failed to read CA file: %w", err)
var dockerCfgJson []byte
if options.Registry != "" {
dockerCfgJson, err = generateDockerConfigJson(options.Registry, options.Username, options.Password)
if err != nil {
return nil, fmt.Errorf("failed to generate json for docker config: %w", err)
}
}
var certFile, keyFile []byte
if options.CertFilePath != "" && options.KeyFilePath != "" {
if certFile, err = os.ReadFile(options.CertFilePath); err != nil {
return nil, fmt.Errorf("failed to read cert file: %w", err)
}
if keyFile, err = os.ReadFile(options.KeyFilePath); err != nil {
return nil, fmt.Errorf("failed to read key file: %w", err)
}
}
secret := buildSecret(keypair, hostKey, caFile, certFile, keyFile, options)
secret := buildSecret(keypair, hostKey, dockerCfgJson, options)
b, err := yaml.Marshal(secret)
if err != nil {
return nil, err
@@ -89,7 +101,36 @@ func Generate(options Options) (*manifestgen.Manifest, error) {
}, nil
}
func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte, options Options) (secret corev1.Secret) {
func LoadKeyPairFromPath(path, password string) (*ssh.KeyPair, error) {
if path == "" {
return nil, nil
}
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to open private key file: %w", err)
}
return LoadKeyPair(b, password)
}
func LoadKeyPair(privateKey []byte, password string) (*ssh.KeyPair, error) {
var ppk cryptssh.Signer
var err error
if password != "" {
ppk, err = cryptssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(password))
} else {
ppk, err = cryptssh.ParsePrivateKey(privateKey)
}
if err != nil {
return nil, err
}
return &ssh.KeyPair{
PublicKey: cryptssh.MarshalAuthorizedKey(ppk.PublicKey()),
PrivateKey: privateKey,
}, nil
}
func buildSecret(keypair *ssh.KeyPair, hostKey, dockerCfg []byte, options Options) (secret corev1.Secret) {
secret.TypeMeta = metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
@@ -101,21 +142,36 @@ func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte
secret.Labels = options.Labels
secret.StringData = map[string]string{}
if dockerCfg != nil {
secret.Type = corev1.SecretTypeDockerConfigJson
secret.StringData[corev1.DockerConfigJsonKey] = string(dockerCfg)
return
}
if options.Username != "" && options.Password != "" {
secret.StringData[UsernameSecretKey] = options.Username
secret.StringData[PasswordSecretKey] = options.Password
}
if caFile != nil {
secret.StringData[CAFileSecretKey] = string(caFile)
if options.BearerToken != "" {
secret.StringData[BearerTokenKey] = options.BearerToken
}
if certFile != nil && keyFile != nil {
secret.StringData[CertFileSecretKey] = string(certFile)
secret.StringData[KeyFileSecretKey] = string(keyFile)
if len(options.CACrt) != 0 {
secret.StringData[CACrtSecretKey] = string(options.CACrt)
} else if len(options.CAFile) != 0 {
secret.StringData[CAFileSecretKey] = string(options.CAFile)
}
if keypair != nil && hostKey != nil {
if len(options.TLSCrt) != 0 && len(options.TLSKey) != 0 {
secret.Type = corev1.SecretTypeTLS
secret.StringData[TLSCrtSecretKey] = string(options.TLSCrt)
secret.StringData[TLSKeySecretKey] = string(options.TLSKey)
} else if len(options.CertFile) != 0 && len(options.KeyFile) != 0 {
secret.StringData[CertFileSecretKey] = string(options.CertFile)
secret.StringData[KeyFileSecretKey] = string(options.KeyFile)
}
if keypair != nil && len(hostKey) != 0 {
secret.StringData[PrivateKeySecretKey] = string(keypair.PrivateKey)
secret.StringData[PublicKeySecretKey] = string(keypair.PublicKey)
secret.StringData[KnownHostsSecretKey] = string(hostKey)
@@ -128,29 +184,6 @@ func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte
return
}
func loadKeyPair(path string, password string) (*ssh.KeyPair, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to open private key file: %w", err)
}
var ppk cryptssh.Signer
if password != "" {
ppk, err = cryptssh.ParsePrivateKeyWithPassphrase(b, []byte(password))
} else {
ppk, err = cryptssh.ParsePrivateKey(b)
}
if err != nil {
return nil, err
}
return &ssh.KeyPair{
PublicKey: cryptssh.MarshalAuthorizedKey(ppk.PublicKey()),
PrivateKey: b,
}, nil
}
func generateKeyPair(options Options) (*ssh.KeyPair, error) {
var keyGen ssh.KeyPairGenerator
switch options.PrivateKeyAlgorithm {
@@ -170,14 +203,14 @@ func generateKeyPair(options Options) (*ssh.KeyPair, error) {
return pair, nil
}
func scanHostKey(host string) ([]byte, error) {
func ScanHostKey(host string) ([]byte, error) {
if _, _, err := net.SplitHostPort(host); err != nil {
// Assume we are dealing with a hostname without a port,
// append the default SSH port as this is required for
// host key scanning to work.
host = fmt.Sprintf("%s:%d", host, defaultSSHPort)
}
hostKey, err := ssh.ScanHostKey(host, 30*time.Second)
hostKey, err := ssh.ScanHostKey(host, 30*time.Second, []string{}, false)
if err != nil {
return nil, fmt.Errorf("SSH key scan for host %s failed, error: %w", host, err)
}
@@ -189,3 +222,19 @@ func resourceToString(data []byte) string {
data = bytes.Replace(data, []byte("status: {}\n"), []byte(""), 1)
return string(data)
}
func generateDockerConfigJson(url, username, password string) ([]byte, error) {
cred := fmt.Sprintf("%s:%s", username, password)
auth := base64.StdEncoding.EncodeToString([]byte(cred))
cfg := DockerConfigJSON{
Auths: map[string]DockerConfigEntry{
url: {
Username: username,
Password: password,
Auth: auth,
},
},
}
return json.Marshal(cfg)
}

View File

@@ -1,3 +1,4 @@
//go:build !e2e
// +build !e2e
/*
@@ -47,7 +48,7 @@ func Test_passwordLoadKeyPair(t *testing.T) {
pk, _ := os.ReadFile(tt.privateKeyPath)
ppk, _ := os.ReadFile(tt.publicKeyPath)
got, err := loadKeyPair(tt.privateKeyPath, tt.password)
got, err := LoadKeyPair(pk, tt.password)
if err != nil {
t.Errorf("loadKeyPair() error = %v", err)
return
@@ -66,24 +67,13 @@ func Test_passwordLoadKeyPair(t *testing.T) {
func Test_PasswordlessLoadKeyPair(t *testing.T) {
for algo, privateKey := range testdata.PEMBytes {
t.Run(algo, func(t *testing.T) {
f, err := os.CreateTemp("", "test-private-key-")
if err != nil {
t.Fatalf("unable to create temporary file. err: %s", err)
}
defer os.Remove(f.Name())
if _, err = f.Write(privateKey); err != nil {
t.Fatalf("unable to write private key to file. err: %s", err)
}
got, err := loadKeyPair(f.Name(), "")
got, err := LoadKeyPair(privateKey, "")
if err != nil {
t.Errorf("loadKeyPair() error = %v", err)
return
}
pk, _ := os.ReadFile(f.Name())
if !reflect.DeepEqual(got.PrivateKey, pk) {
if !reflect.DeepEqual(got.PrivateKey, privateKey) {
t.Errorf("PrivateKey %s != %s", got.PrivateKey, string(privateKey))
}

View File

@@ -32,20 +32,18 @@ type Options struct {
Secret string
TargetPath string
ManifestFile string
GitImplementation string
RecurseSubmodules bool
}
func MakeDefaultOptions() Options {
return Options{
Interval: 1 * time.Minute,
URL: "",
Name: "flux-system",
Namespace: "flux-system",
Branch: "main",
Secret: "flux-system",
ManifestFile: "gotk-sync.yaml",
TargetPath: "",
GitImplementation: "",
Interval: 1 * time.Minute,
URL: "",
Name: "flux-system",
Namespace: "flux-system",
Branch: "main",
Secret: "flux-system",
ManifestFile: "gotk-sync.yaml",
TargetPath: "",
}
}

View File

@@ -26,11 +26,11 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/pkg/manifestgen"
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
)
func Generate(options Options) (*manifestgen.Manifest, error) {
@@ -67,7 +67,6 @@ func Generate(options Options) (*manifestgen.Manifest, error) {
SecretRef: &meta.LocalObjectReference{
Name: options.Secret,
},
GitImplementation: options.GitImplementation,
RecurseSubmodules: options.RecurseSubmodules,
},
}

View File

@@ -1,3 +1,4 @@
//go:build !e2e
// +build !e2e
/*
@@ -23,8 +24,8 @@ import (
"strings"
"testing"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
)
func TestGenerate(t *testing.T) {

38
pkg/manifestgen/tmpdir.go Normal file
View File

@@ -0,0 +1,38 @@
/*
Copyright 2022 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 manifestgen
import (
"fmt"
"os"
"path/filepath"
)
// MkdirTempAbs creates a tmp dir and returns the absolute path to the dir.
// This is required since certain OSes like MacOS create temporary files in
// e.g. `/private/var`, to which `/var` is a symlink.
func MkdirTempAbs(dir, pattern string) (string, error) {
tmpDir, err := os.MkdirTemp(dir, pattern)
if err != nil {
return "", err
}
tmpDir, err = filepath.EvalSymlinks(tmpDir)
if err != nil {
return "", fmt.Errorf("error evaluating symlink: %w", err)
}
return tmpDir, nil
}

56
pkg/printers/dyff.go Normal file
View File

@@ -0,0 +1,56 @@
/*
Copyright 2022 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 printers
import (
"fmt"
"io"
"github.com/homeport/dyff/pkg/dyff"
)
// DyffPrinter is a printer that prints dyff reports.
type DyffPrinter struct {
OmitHeader bool
}
// NewDyffPrinter returns a new DyffPrinter.
func NewDyffPrinter() *DyffPrinter {
return &DyffPrinter{
OmitHeader: true,
}
}
// Print prints the given args to the given writer.
func (p *DyffPrinter) Print(w io.Writer, args ...interface{}) error {
for _, arg := range args {
switch arg := arg.(type) {
case dyff.Report:
reportWriter := &dyff.HumanReport{
Report: arg,
OmitHeader: p.OmitHeader,
}
if err := reportWriter.WriteReport(w); err != nil {
return fmt.Errorf("failed to print report: %w", err)
}
default:
return fmt.Errorf("unsupported type %T", arg)
}
}
return nil
}

33
pkg/printers/interface.go Normal file
View File

@@ -0,0 +1,33 @@
/*
Copyright 2022 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 printers
import "io"
// Printer is an interface for printing Flux cmd outputs.
type Printer interface {
// Print prints the given args to the given writer.
Print(io.Writer, ...interface{}) error
}
// PrinterFunc is a function that can print args to a writer.
type PrinterFunc func(w io.Writer, args ...interface{}) error
// Print implements Printer
func (fn PrinterFunc) Print(w io.Writer, args ...interface{}) error {
return fn(w, args)
}

View File

@@ -0,0 +1,63 @@
/*
Copyright 2022 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 printers
import (
"fmt"
"io"
"github.com/olekukonko/tablewriter"
)
// TablePrinter is a printer that prints Flux cmd outputs.
func TablePrinter(header []string) PrinterFunc {
return func(w io.Writer, args ...interface{}) error {
var rows [][]string
for _, arg := range args {
switch arg := arg.(type) {
case []interface{}:
for _, v := range arg {
s, ok := v.([][]string)
if !ok {
return fmt.Errorf("unsupported type %T", v)
}
rows = append(rows, s...)
}
default:
return fmt.Errorf("unsupported type %T", arg)
}
}
table := tablewriter.NewWriter(w)
table.SetHeader(header)
table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetTablePadding("\t")
table.SetNoWhiteSpace(true)
table.AppendBulk(rows)
table.Render()
return nil
}
}

View File

@@ -24,16 +24,17 @@ import (
"time"
"k8s.io/client-go/rest"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/aggregator"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/collector"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"github.com/fluxcd/flux2/pkg/log"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/aggregator"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
"github.com/fluxcd/cli-utils/pkg/object"
runtimeclient "github.com/fluxcd/pkg/runtime/client"
"github.com/fluxcd/flux2/v2/pkg/log"
)
type StatusChecker struct {
@@ -44,8 +45,18 @@ type StatusChecker struct {
logger log.Logger
}
func NewStatusCheckerWithClient(c client.Client, pollInterval time.Duration, timeout time.Duration, log log.Logger) (*StatusChecker, error) {
return &StatusChecker{
pollInterval: pollInterval,
timeout: timeout,
client: c,
statusPoller: polling.NewStatusPoller(c, c.RESTMapper(), polling.Options{}),
logger: log,
}, nil
}
func NewStatusChecker(kubeConfig *rest.Config, pollInterval time.Duration, timeout time.Duration, log log.Logger) (*StatusChecker, error) {
restMapper, err := apiutil.NewDynamicRESTMapper(kubeConfig)
restMapper, err := runtimeclient.NewDynamicRESTMapper(kubeConfig)
if err != nil {
return nil, err
}
@@ -54,20 +65,14 @@ func NewStatusChecker(kubeConfig *rest.Config, pollInterval time.Duration, timeo
return nil, err
}
return &StatusChecker{
pollInterval: pollInterval,
timeout: timeout,
client: c,
statusPoller: polling.NewStatusPoller(c, restMapper),
logger: log,
}, nil
return NewStatusCheckerWithClient(c, pollInterval, timeout, log)
}
func (sc *StatusChecker) Assess(identifiers ...object.ObjMetadata) error {
ctx, cancel := context.WithTimeout(context.Background(), sc.timeout)
defer cancel()
opts := polling.Options{PollInterval: sc.pollInterval, UseCache: true}
opts := polling.PollOptions{PollInterval: sc.pollInterval}
eventsChan := sc.statusPoller.Poll(ctx, identifiers, opts)
coll := collector.NewResourceStatusCollector(identifiers)
@@ -93,7 +98,7 @@ func (sc *StatusChecker) Assess(identifiers ...object.ObjMetadata) error {
}
if coll.Error != nil || ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("timed out waiting for condition")
return fmt.Errorf("timed out waiting for all resources to be ready")
}
return nil
}

395
pkg/uninstall/uninstall.go Normal file
View File

@@ -0,0 +1,395 @@
/*
Copyright 2022 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 uninstall
import (
"context"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1 "k8s.io/api/rbac/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta2"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/flux2/v2/pkg/log"
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
)
// Components removes all Kubernetes components that are part of Flux excluding the CRDs and namespace.
func Components(ctx context.Context, logger log.Logger, kubeClient client.Client, namespace string, dryRun bool) error {
var aggregateErr []error
opts, dryRunStr := getDeleteOptions(dryRun)
selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue}
{
var list appsv1.DeploymentList
if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil {
for i := range list.Items {
r := list.Items[i]
if err := kubeClient.Delete(ctx, &r, opts); err != nil {
logger.Failuref("Deployment/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("Deployment/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list corev1.ServiceList
if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil {
for i := range list.Items {
r := list.Items[i]
if err := kubeClient.Delete(ctx, &r, opts); err != nil {
logger.Failuref("Service/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("Service/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list networkingv1.NetworkPolicyList
if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil {
for i := range list.Items {
r := list.Items[i]
if err := kubeClient.Delete(ctx, &r, opts); err != nil {
logger.Failuref("NetworkPolicy/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("NetworkPolicy/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list corev1.ServiceAccountList
if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil {
for i := range list.Items {
r := list.Items[i]
if err := kubeClient.Delete(ctx, &r, opts); err != nil {
logger.Failuref("ServiceAccount/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("ServiceAccount/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list rbacv1.ClusterRoleList
if err := kubeClient.List(ctx, &list, selector); err == nil {
for i := range list.Items {
r := list.Items[i]
if err := kubeClient.Delete(ctx, &r, opts); err != nil {
logger.Failuref("ClusterRole/%s deletion failed: %s", r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("ClusterRole/%s deleted %s", r.Name, dryRunStr)
}
}
}
}
{
var list rbacv1.ClusterRoleBindingList
if err := kubeClient.List(ctx, &list, selector); err == nil {
for i := range list.Items {
r := list.Items[i]
if err := kubeClient.Delete(ctx, &r, opts); err != nil {
logger.Failuref("ClusterRoleBinding/%s deletion failed: %s", r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("ClusterRoleBinding/%s deleted %s", r.Name, dryRunStr)
}
}
}
}
return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr)))
}
// Finalizers removes all finalizes on Kubernetes components that have been added by a Flux controller.
func Finalizers(ctx context.Context, logger log.Logger, kubeClient client.Client, dryRun bool) error {
var aggregateErr []error
opts, dryRunStr := getUpdateOptions(dryRun)
{
var list sourcev1.GitRepositoryList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list sourcev1b2.OCIRepositoryList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list sourcev1b2.HelmRepositoryList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list sourcev1b2.HelmChartList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list sourcev1b2.BucketList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list kustomizev1.KustomizationList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list helmv2.HelmReleaseList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list notificationv1b3.AlertList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list notificationv1b3.ProviderList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list notificationv1.ReceiverList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list imagev1.ImagePolicyList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list imagev1.ImageRepositoryList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
{
var list autov1.ImageUpdateAutomationList
if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil {
for i := range list.Items {
r := list.Items[i]
r.Finalizers = []string{}
if err := kubeClient.Update(ctx, &r, opts); err != nil {
logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr)
}
}
}
}
return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr)))
}
// CustomResourceDefinitions removes all Kubernetes CRDs that are a part of Flux.
func CustomResourceDefinitions(ctx context.Context, logger log.Logger, kubeClient client.Client, dryRun bool) error {
var aggregateErr []error
opts, dryRunStr := getDeleteOptions(dryRun)
selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue}
{
var list apiextensionsv1.CustomResourceDefinitionList
if err := kubeClient.List(ctx, &list, selector); err == nil {
for i := range list.Items {
r := list.Items[i]
if err := kubeClient.Delete(ctx, &r, opts); err != nil {
logger.Failuref("CustomResourceDefinition/%s deletion failed: %s", r.Name, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("CustomResourceDefinition/%s deleted %s", r.Name, dryRunStr)
}
}
}
}
return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr)))
}
// Namespace removes the namespace Flux is installed in.
func Namespace(ctx context.Context, logger log.Logger, kubeClient client.Client, namespace string, dryRun bool) error {
var aggregateErr []error
opts, dryRunStr := getDeleteOptions(dryRun)
ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}
if err := kubeClient.Delete(ctx, &ns, opts); err != nil {
logger.Failuref("Namespace/%s deletion failed: %s", namespace, err.Error())
aggregateErr = append(aggregateErr, err)
} else {
logger.Successf("Namespace/%s deleted %s", namespace, dryRunStr)
}
return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr)))
}
func getDeleteOptions(dryRun bool) (*client.DeleteOptions, string) {
opts := &client.DeleteOptions{}
var dryRunStr string
if dryRun {
client.DryRunAll.ApplyToDelete(opts)
dryRunStr = "(dry run)"
}
return opts, dryRunStr
}
func getUpdateOptions(dryRun bool) (*client.UpdateOptions, string) {
opts := &client.UpdateOptions{}
var dryRunStr string
if dryRun {
client.DryRunAll.ApplyToUpdate(opts)
dryRunStr = "(dry run)"
}
return opts, dryRunStr
}