Merge branch 'main' into patch-3
This commit is contained in:
273
pkg/bootstrap/bootstrap.go
Normal file
273
pkg/bootstrap/bootstrap.go
Normal 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
|
||||
}
|
||||
541
pkg/bootstrap/bootstrap_plain_git.go
Normal file
541
pkg/bootstrap/bootstrap_plain_git.go
Normal 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
|
||||
}
|
||||
640
pkg/bootstrap/bootstrap_provider.go
Normal file
640
pkg/bootstrap/bootstrap_provider.go
Normal 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
|
||||
}
|
||||
469
pkg/bootstrap/bootstrap_test.go
Normal file
469
pkg/bootstrap/bootstrap_test.go
Normal 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: ¬ificationv1.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
161
pkg/bootstrap/options.go
Normal 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
|
||||
}
|
||||
88
pkg/bootstrap/provider/factory.go
Normal file
88
pkg/bootstrap/provider/factory.go
Normal 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
|
||||
}
|
||||
48
pkg/bootstrap/provider/provider.go
Normal file
48
pkg/bootstrap/provider/provider.go
Normal 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
31
pkg/log/nop.go
Normal 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{}) {}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
38
pkg/manifestgen/tmpdir.go
Normal 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
56
pkg/printers/dyff.go
Normal 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
33
pkg/printers/interface.go
Normal 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)
|
||||
}
|
||||
63
pkg/printers/table_printer.go
Normal file
63
pkg/printers/table_printer.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
395
pkg/uninstall/uninstall.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user