gitrepo: Add more reconciler design improvements

- Remove ArtifactUnavailable condition and use Reconciling condition to
  convey the same.
- Make Reconciling condition affect the ready condition.
- Introduce summarizeAndPatch() to calculate the final status conditions
  and patch them.
- Introduce reconcile() to iterate through the sub-reconcilers and
  execute them.

Signed-off-by: Sunny <darkowlzz@protonmail.com>
This commit is contained in:
Sunny 2021-12-21 01:57:06 +05:30
parent ea45903dd4
commit 8b43f6d7a7
4 changed files with 372 additions and 287 deletions

View File

@ -18,6 +18,7 @@ package controllers
import (
"context"
"errors"
"fmt"
"os"
"strings"
@ -26,7 +27,7 @@ import (
securejoin "github.com/cyphar/filepath-securejoin"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
kerrors "k8s.io/apimachinery/pkg/util/errors"
kuberecorder "k8s.io/client-go/tools/record"
@ -40,16 +41,48 @@ import (
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/events"
"github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/pkg/runtime/predicates"
"github.com/fluxcd/source-controller/pkg/sourceignore"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
serror "github.com/fluxcd/source-controller/internal/error"
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
"github.com/fluxcd/source-controller/pkg/git"
"github.com/fluxcd/source-controller/pkg/git/strategy"
)
// Status conditions owned by the GitRepository reconciler.
var gitRepoOwnedConditions = []string{
sourcev1.SourceVerifiedCondition,
sourcev1.FetchFailedCondition,
sourcev1.IncludeUnavailableCondition,
sourcev1.ArtifactOutdatedCondition,
meta.ReadyCondition,
meta.ReconcilingCondition,
meta.StalledCondition,
}
// Conditions that Ready condition is influenced by in descending order of their
// priority.
var gitRepoReadyDeps = []string{
sourcev1.IncludeUnavailableCondition,
sourcev1.SourceVerifiedCondition,
sourcev1.FetchFailedCondition,
sourcev1.ArtifactOutdatedCondition,
meta.StalledCondition,
meta.ReconcilingCondition,
}
// Negative conditions that Ready condition is influenced by.
var gitRepoReadyDepsNegative = []string{
sourcev1.FetchFailedCondition,
sourcev1.IncludeUnavailableCondition,
sourcev1.ArtifactOutdatedCondition,
meta.StalledCondition,
meta.ReconcilingCondition,
}
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories/finalizers,verbs=get;create;update;patch;delete
@ -71,6 +104,10 @@ type GitRepositoryReconcilerOptions struct {
DependencyRequeueInterval time.Duration
}
// gitRepoReconcilerFunc is the function type for all the Git repository
// reconciler functions.
type gitRepoReconcilerFunc func(ctx context.Context, obj *sourcev1.GitRepository, artifact *sourcev1.Artifact, includes *artifactSet, dir string) (sreconcile.Result, error)
func (r *GitRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error {
return r.SetupWithManagerAndOptions(mgr, GitRepositoryReconcilerOptions{})
}
@ -111,74 +148,14 @@ func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Reques
return ctrl.Result{}, err
}
var recResult sreconcile.Result
// Always attempt to patch the object and status after each reconciliation
// NOTE: This deferred block only modifies the named return error. The
// result from the reconciliation remains the same. Any requeue attributes
// set in the result will continue to be effective.
defer func() {
// Record the value of the reconciliation request, if any
if v, ok := meta.ReconcileAnnotationValue(obj.GetAnnotations()); ok {
obj.Status.SetLastHandledReconcileRequest(v)
}
// Summarize the Ready condition based on abnormalities that may have been observed.
conditions.SetSummary(obj,
meta.ReadyCondition,
conditions.WithConditions(
sourcev1.IncludeUnavailableCondition,
sourcev1.SourceVerifiedCondition,
sourcev1.FetchFailedCondition,
sourcev1.ArtifactOutdatedCondition,
sourcev1.ArtifactUnavailableCondition,
),
conditions.WithNegativePolarityConditions(
sourcev1.ArtifactUnavailableCondition,
sourcev1.FetchFailedCondition,
sourcev1.IncludeUnavailableCondition,
sourcev1.ArtifactOutdatedCondition,
),
)
// Patch the object, ignoring conflicts on the conditions owned by this controller
patchOpts := []patch.Option{
patch.WithOwnedConditions{
Conditions: []string{
sourcev1.ArtifactUnavailableCondition,
sourcev1.SourceVerifiedCondition,
sourcev1.FetchFailedCondition,
sourcev1.IncludeUnavailableCondition,
sourcev1.ArtifactOutdatedCondition,
meta.ReadyCondition,
meta.ReconcilingCondition,
meta.StalledCondition,
},
},
}
// Determine if the resource is still being reconciled, or if it has stalled, and record this observation
if retErr == nil && (result.IsZero() || !result.Requeue) {
// We are no longer reconciling
conditions.Delete(obj, meta.ReconcilingCondition)
// We have now observed this generation
patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{})
readyCondition := conditions.Get(obj, meta.ReadyCondition)
switch readyCondition.Status {
case metav1.ConditionFalse:
// As we are no longer reconciling and the end-state is not ready, the reconciliation has stalled
conditions.MarkStalled(obj, readyCondition.Reason, readyCondition.Message)
case metav1.ConditionTrue:
// As we are no longer reconciling and the end-state is ready, the reconciliation is no longer stalled
conditions.Delete(obj, meta.StalledCondition)
}
}
// Finally, patch the resource
if err := patchHelper.Patch(ctx, obj, patchOpts...); err != nil {
// Ignore patch error "not found" when the object is being deleted.
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
err = kerrors.FilterOut(err, func(e error) bool { return apierrors.IsNotFound(e) })
}
retErr = kerrors.NewAggregate([]error{retErr, err})
}
retErr = r.summarizeAndPatch(ctx, obj, patchHelper, recResult, retErr)
// Always record readiness and duration metrics
r.Metrics.RecordReadiness(ctx, obj)
@ -189,55 +166,103 @@ func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Reques
// between init and delete
if !controllerutil.ContainsFinalizer(obj, sourcev1.SourceFinalizer) {
controllerutil.AddFinalizer(obj, sourcev1.SourceFinalizer)
recResult = sreconcile.ResultRequeue
return ctrl.Result{Requeue: true}, nil
}
// Examine if the object is under deletion
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
return r.reconcileDelete(ctx, obj)
res, err := r.reconcileDelete(ctx, obj)
return sreconcile.BuildRuntimeResult(ctx, r.EventRecorder, obj, res, err)
}
// Reconcile actual object
return r.reconcile(ctx, obj)
reconcilers := []gitRepoReconcilerFunc{
r.reconcileStorage,
r.reconcileSource,
r.reconcileInclude,
r.reconcileArtifact,
}
recResult, err = r.reconcile(ctx, obj, reconcilers)
return sreconcile.BuildRuntimeResult(ctx, r.EventRecorder, obj, recResult, err)
}
// reconcile steps through the actual reconciliation tasks for the object, it returns early on the first step that
// produces an error.
func (r *GitRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.GitRepository) (ctrl.Result, error) {
// Mark the resource as under reconciliation
conditions.MarkReconciling(obj, meta.ProgressingReason, "")
// Reconcile the storage data
if result, err := r.reconcileStorage(ctx, obj); err != nil || result.IsZero() {
return result, err
// summarizeAndPatch analyzes the object conditions to create a summary of the
// status conditions and patches the object with the calculated summary.
func (r *GitRepositoryReconciler) summarizeAndPatch(ctx context.Context, obj *sourcev1.GitRepository, patchHelper *patch.Helper, res sreconcile.Result, recErr error) error {
// Record the value of the reconciliation request if any.
if v, ok := meta.ReconcileAnnotationValue(obj.GetAnnotations()); ok {
obj.Status.SetLastHandledReconcileRequest(v)
}
// Compute the reconcile results, obtain patch options and reconcile error.
var patchOpts []patch.Option
patchOpts, recErr = sreconcile.ComputeReconcileResult(obj, res, recErr, gitRepoOwnedConditions)
// Summarize the Ready condition based on abnormalities that may have been observed.
conditions.SetSummary(obj,
meta.ReadyCondition,
conditions.WithConditions(
gitRepoReadyDeps...,
),
conditions.WithNegativePolarityConditions(
gitRepoReadyDepsNegative...,
),
)
// Finally, patch the resource.
if err := patchHelper.Patch(ctx, obj, patchOpts...); err != nil {
// Ignore patch error "not found" when the object is being deleted.
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
err = kerrors.FilterOut(err, func(e error) bool { return apierrors.IsNotFound(e) })
}
recErr = kerrors.NewAggregate([]error{recErr, err})
}
return recErr
}
// reconcile steps iterates through the actual reconciliation tasks for objec,
// it returns early on the first step that returns ResultRequeue or produces an
// error.
func (r *GitRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.GitRepository, reconcilers []gitRepoReconcilerFunc) (sreconcile.Result, error) {
if obj.Generation != obj.Status.ObservedGeneration {
conditions.MarkReconciling(obj, "NewGeneration", "reconciling new generation %d", obj.Generation)
}
var artifact sourcev1.Artifact
var includes artifactSet
// Create temp dir for Git clone
tmpDir, err := os.MkdirTemp("", fmt.Sprintf("%s-%s-%s-", obj.Kind, obj.Namespace, obj.Name))
if err != nil {
r.Eventf(obj, events.EventSeverityError, sourcev1.StorageOperationFailedReason, "Failed to create temporary directory: %s", err)
return ctrl.Result{}, err
return sreconcile.ResultEmpty, &serror.Event{
Err: fmt.Errorf("failed to create temporary directory: %w", err),
Reason: sourcev1.StorageOperationFailedReason,
}
}
defer os.RemoveAll(tmpDir)
// Reconcile the source from upstream
var artifact sourcev1.Artifact
if result, err := r.reconcileSource(ctx, obj, &artifact, tmpDir); err != nil || result.IsZero() {
return ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, err
// Run the sub-reconcilers and build the result of reconciliation.
var res sreconcile.Result
var resErr error
for _, rec := range reconcilers {
recResult, err := rec(ctx, obj, &artifact, &includes, tmpDir)
// Exit immediately on ResultRequeue.
if recResult == sreconcile.ResultRequeue {
return sreconcile.ResultRequeue, nil
}
// If an error is received, prioritize the returned results because an
// error also means immediate requeue.
if err != nil {
resErr = err
res = recResult
break
}
// Prioritize requeue request in the result.
res = sreconcile.LowestRequeuingResult(res, recResult)
}
// Reconcile includes from the storage
var includes artifactSet
if result, err := r.reconcileInclude(ctx, obj, tmpDir); err != nil || result.IsZero() {
return ctrl.Result{RequeueAfter: r.requeueDependency}, err
}
// Reconcile the artifact to storage
if result, err := r.reconcileArtifact(ctx, obj, artifact, includes, tmpDir); err != nil || result.IsZero() {
return result, err
}
return ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
return res, resErr
}
// reconcileStorage ensures the current state of the storage matches the desired and previously observed state.
@ -246,9 +271,7 @@ func (r *GitRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.G
// If the artifact in the Status object of the resource disappeared from storage, it is removed from the object.
// If the object does not have an artifact in its Status object, a v1beta1.ArtifactUnavailableCondition is set.
// If the hostname of any of the URLs on the object do not match the current storage server hostname, they are updated.
//
// The caller should assume a failure if an error is returned, or the Result is zero.
func (r *GitRepositoryReconciler) reconcileStorage(ctx context.Context, obj *sourcev1.GitRepository) (ctrl.Result, error) {
func (r *GitRepositoryReconciler) reconcileStorage(ctx context.Context, obj *sourcev1.GitRepository, artifact *sourcev1.Artifact, includes *artifactSet, dir string) (sreconcile.Result, error) {
// Garbage collect previous advertised artifact(s) from storage
_ = r.garbageCollect(ctx, obj)
@ -260,17 +283,16 @@ func (r *GitRepositoryReconciler) reconcileStorage(ctx context.Context, obj *sou
// Record that we do not have an artifact
if obj.GetArtifact() == nil {
conditions.MarkTrue(obj, sourcev1.ArtifactUnavailableCondition, "NoArtifact", "No artifact for resource in storage")
return ctrl.Result{Requeue: true}, nil
conditions.MarkReconciling(obj, "NoArtifact", "no artifact for resource in storage")
return sreconcile.ResultSuccess, nil
}
conditions.Delete(obj, sourcev1.ArtifactUnavailableCondition)
// Always update URLs to ensure hostname is up-to-date
// TODO(hidde): we may want to send out an event only if we notice the URL has changed
r.Storage.SetArtifactURL(obj.GetArtifact())
obj.Status.URL = r.Storage.SetHostname(obj.Status.URL)
return ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
return sreconcile.ResultSuccess, nil
}
// reconcileSource ensures the upstream Git repository can be reached and checked out using the declared configuration,
@ -284,10 +306,8 @@ func (r *GitRepositoryReconciler) reconcileStorage(ctx context.Context, obj *sou
// signature can not be verified or the verification fails, the Condition=False and it returns early.
// If both the checkout and signature verification are successful, the given artifact pointer is set to a new artifact
// with the available metadata.
//
// The caller should assume a failure if an error is returned, or the Result is zero.
func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
obj *sourcev1.GitRepository, artifact *sourcev1.Artifact, dir string) (ctrl.Result, error) {
obj *sourcev1.GitRepository, artifact *sourcev1.Artifact, includes *artifactSet, dir string) (sreconcile.Result, error) {
// Configure authentication strategy to access the source
var authOpts *git.AuthOptions
var err error
@ -299,12 +319,13 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
}
var secret corev1.Secret
if err := r.Client.Get(ctx, name, &secret); err != nil {
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason,
"Failed to get secret '%s': %s", name.String(), err.Error())
r.Eventf(obj, events.EventSeverityError, sourcev1.AuthenticationFailedReason,
"Failed to get secret '%s': %s", name.String(), err.Error())
e := &serror.Event{
Err: fmt.Errorf("failed to get secret '%s': %w", name.String(), err),
Reason: sourcev1.AuthenticationFailedReason,
}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, e.Err.Error())
// Return error as the world as observed may change
return ctrl.Result{}, err
return sreconcile.ResultEmpty, e
}
// Configure strategy with secret
@ -314,12 +335,13 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
authOpts, err = git.AuthOptionsWithoutSecret(obj.Spec.URL)
}
if err != nil {
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason,
"Failed to configure auth strategy for Git implementation '%s': %s", obj.Spec.GitImplementation, err)
r.Eventf(obj, events.EventSeverityError, sourcev1.AuthenticationFailedReason,
"Failed to configure auth strategy for Git implementation '%s': %s", obj.Spec.GitImplementation, err)
e := &serror.Event{
Err: fmt.Errorf("failed to configure auth strategy for Git implementation '%s': %w", obj.Spec.GitImplementation, err),
Reason: sourcev1.AuthenticationFailedReason,
}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, e.Err.Error())
// Return error as the contents of the secret may change
return ctrl.Result{}, err
return sreconcile.ResultEmpty, e
}
// Configure checkout strategy
@ -333,11 +355,13 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
checkoutStrategy, err := strategy.CheckoutStrategyForImplementation(ctx,
git.Implementation(obj.Spec.GitImplementation), checkoutOpts)
if err != nil {
ctrl.LoggerFrom(ctx).Error(err, fmt.Sprintf("Failed to configure checkout strategy for Git implementation '%s'", obj.Spec.GitImplementation))
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason,
"Failed to configure checkout strategy for Git implementation '%s': %s", obj.Spec.GitImplementation, err)
e := &serror.Stalling{
Err: fmt.Errorf("failed to configure checkout strategy for Git implementation '%s': %w", obj.Spec.GitImplementation, err),
Reason: sourcev1.GitOperationFailedReason,
}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, e.Err.Error())
// Do not return err as recovery without changes is impossible
return ctrl.Result{}, nil
return sreconcile.ResultEmpty, e
}
// Checkout HEAD of reference in object
@ -345,19 +369,20 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
defer cancel()
commit, err := checkoutStrategy.Checkout(gitCtx, dir, obj.Spec.URL, authOpts)
if err != nil {
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason,
"Failed to checkout and determine revision: %s", err)
r.Eventf(obj, events.EventSeverityError, sourcev1.GitOperationFailedReason,
"Failed to checkout and determine revision: %s", err)
e := &serror.Event{
Err: fmt.Errorf("failed to checkout and determine revision: %w", err),
Reason: sourcev1.GitOperationFailedReason,
}
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, e.Err.Error())
// Coin flip on transient or persistent error, return error and hope for the best
return ctrl.Result{}, err
return sreconcile.ResultEmpty, e
}
r.Eventf(obj, events.EventSeverityInfo, sourcev1.GitOperationSucceedReason,
"Cloned repository '%s' and checked out revision '%s'", obj.Spec.URL, commit.String())
r.eventLogf(ctx, obj, corev1.EventTypeNormal, sourcev1.GitOperationSucceedReason,
"cloned repository '%s' and checked out revision '%s'", obj.Spec.URL, commit.String())
conditions.Delete(obj, sourcev1.FetchFailedCondition)
// Verify commit signature
if result, err := r.verifyCommitSignature(ctx, obj, *commit); err != nil || result.IsZero() {
if result, err := r.verifyCommitSignature(ctx, obj, *commit); err != nil || result == sreconcile.ResultEmpty {
return result, err
}
@ -366,9 +391,11 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
// Mark observations about the revision on the object
if !obj.GetArtifact().HasRevision(commit.String()) {
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", "New upstream revision '%s'", commit.String())
message := fmt.Sprintf("new upstream revision '%s'", commit.String())
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", message)
conditions.MarkReconciling(obj, "NewRevision", message)
}
return ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
return sreconcile.ResultSuccess, nil
}
// reconcileArtifact archives a new artifact to the storage, if the current observation on the object does not match the
@ -380,84 +407,96 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
// Source ignore patterns are loaded, and the given directory is archived.
// On a successful archive, the artifact and includes in the status of the given object are set, and the symlink in the
// storage is updated to its path.
//
// The caller should assume a failure if an error is returned, or the Result is zero.
func (r *GitRepositoryReconciler) reconcileArtifact(ctx context.Context, obj *sourcev1.GitRepository, artifact sourcev1.Artifact, includes artifactSet, dir string) (ctrl.Result, error) {
func (r *GitRepositoryReconciler) reconcileArtifact(ctx context.Context, obj *sourcev1.GitRepository, artifact *sourcev1.Artifact, includes *artifactSet, dir string) (sreconcile.Result, error) {
// Always restore the Ready condition in case it got removed due to a transient error
defer func() {
if obj.GetArtifact() != nil {
conditions.Delete(obj, sourcev1.ArtifactUnavailableCondition)
}
if obj.GetArtifact().HasRevision(artifact.Revision) && !includes.Diff(obj.Status.IncludedArtifacts) {
conditions.Delete(obj, sourcev1.ArtifactOutdatedCondition)
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason,
"Stored artifact for revision '%s'", artifact.Revision)
"stored artifact for revision '%s'", artifact.Revision)
}
}()
// The artifact is up-to-date
if obj.GetArtifact().HasRevision(artifact.Revision) && !includes.Diff(obj.Status.IncludedArtifacts) {
ctrl.LoggerFrom(ctx).Info(fmt.Sprintf("Already up to date, current revision '%s'", artifact.Revision))
return ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
r.eventLogf(ctx, obj, corev1.EventTypeNormal, meta.SucceededReason, "already up to date, current revision '%s'", artifact.Revision)
return sreconcile.ResultSuccess, nil
}
// Mark reconciling because the artifact and remote source are different.
// and they have to be reconciled.
conditions.MarkReconciling(obj, "NewRevision", "new upstream revision '%s'", artifact.Revision)
// Ensure target path exists and is a directory
if f, err := os.Stat(dir); err != nil {
err = fmt.Errorf("failed to stat target path: %w", err)
return ctrl.Result{}, err
e := &serror.Event{
Err: fmt.Errorf("failed to stat target path: %w", err),
Reason: sourcev1.StorageOperationFailedReason,
}
return sreconcile.ResultEmpty, e
} else if !f.IsDir() {
err = fmt.Errorf("invalid target path: '%s' is not a directory", dir)
return ctrl.Result{}, err
e := &serror.Event{
Err: fmt.Errorf("invalid target path: '%s' is not a directory", dir),
Reason: sourcev1.StorageOperationFailedReason,
}
return sreconcile.ResultEmpty, e
}
// Ensure artifact directory exists and acquire lock
if err := r.Storage.MkdirAll(artifact); err != nil {
err = fmt.Errorf("failed to create artifact directory: %w", err)
return ctrl.Result{}, err
if err := r.Storage.MkdirAll(*artifact); err != nil {
e := &serror.Event{
Err: fmt.Errorf("failed to create artifact directory: %w", err),
Reason: sourcev1.StorageOperationFailedReason,
}
return sreconcile.ResultEmpty, e
}
unlock, err := r.Storage.Lock(artifact)
unlock, err := r.Storage.Lock(*artifact)
if err != nil {
err = fmt.Errorf("failed to acquire lock for artifact: %w", err)
return ctrl.Result{}, err
return sreconcile.ResultEmpty, &serror.Event{
Err: fmt.Errorf("failed to acquire lock for artifact: %w", err),
Reason: meta.FailedReason,
}
}
defer unlock()
// Load ignore rules for archiving
ps, err := sourceignore.LoadIgnorePatterns(dir, nil)
if err != nil {
r.Eventf(obj, events.EventSeverityError,
"SourceIgnoreError", "Failed to load source ignore patterns from repository: %s", err)
return ctrl.Result{}, err
return sreconcile.ResultEmpty, &serror.Event{
Err: fmt.Errorf("failed to load source ignore patterns from repository: %w", err),
Reason: "SourceIgnoreError",
}
}
if obj.Spec.Ignore != nil {
ps = append(ps, sourceignore.ReadPatterns(strings.NewReader(*obj.Spec.Ignore), nil)...)
}
// Archive directory to storage
if err := r.Storage.Archive(&artifact, dir, SourceIgnoreFilter(ps, nil)); err != nil {
r.Eventf(obj, events.EventSeverityError, sourcev1.StorageOperationFailedReason,
"Unable to archive artifact to storage: %s", err)
return ctrl.Result{}, err
if err := r.Storage.Archive(artifact, dir, SourceIgnoreFilter(ps, nil)); err != nil {
return sreconcile.ResultEmpty, &serror.Event{
Err: fmt.Errorf("unable to archive artifact to storage: %w", err),
Reason: sourcev1.StorageOperationFailedReason,
}
}
r.AnnotatedEventf(obj, map[string]string{
"revision": artifact.Revision,
"checksum": artifact.Checksum,
}, events.EventSeverityInfo, "NewArtifact", "Stored artifact for revision '%s'", artifact.Revision)
}, corev1.EventTypeNormal, "NewArtifact", "stored artifact for revision '%s'", artifact.Revision)
// Record it on the object
obj.Status.Artifact = artifact.DeepCopy()
obj.Status.IncludedArtifacts = includes
obj.Status.IncludedArtifacts = *includes
// Update symlink on a "best effort" basis
url, err := r.Storage.Symlink(artifact, "latest.tar.gz")
url, err := r.Storage.Symlink(*artifact, "latest.tar.gz")
if err != nil {
r.Eventf(obj, events.EventSeverityError, sourcev1.StorageOperationFailedReason,
r.eventLogf(ctx, obj, corev1.EventTypeWarning, sourcev1.StorageOperationFailedReason,
"Failed to update status URL symlink: %s", err)
}
if url != "" {
obj.Status.URL = url
}
return ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
return sreconcile.ResultSuccess, nil
}
// reconcileInclude reconciles the declared includes from the object by copying their artifact (sub)contents to the
@ -466,42 +505,49 @@ func (r *GitRepositoryReconciler) reconcileArtifact(ctx context.Context, obj *so
// If an include is unavailable, it marks the object with v1beta1.IncludeUnavailableCondition and returns early.
// If the copy operations are successful, it deletes the v1beta1.IncludeUnavailableCondition from the object.
// If the artifactSet differs from the current set, it marks the object with v1beta1.ArtifactOutdatedCondition.
//
// The caller should assume a failure if an error is returned, or the Result is zero.
func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context, obj *sourcev1.GitRepository, dir string) (ctrl.Result, error) {
func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context, obj *sourcev1.GitRepository, artifact *sourcev1.Artifact, includes *artifactSet, dir string) (sreconcile.Result, error) {
artifacts := make(artifactSet, len(obj.Spec.Include))
for i, incl := range obj.Spec.Include {
// Do this first as it is much cheaper than copy operations
toPath, err := securejoin.SecureJoin(dir, incl.GetToPath())
if err != nil {
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "IllegalPath",
"Path calculation for include '%s' failed: %s", incl.GitRepositoryRef.Name, err.Error())
return ctrl.Result{}, err
e := &serror.Event{
Err: fmt.Errorf("path calculation for include '%s' failed: %w", incl.GitRepositoryRef.Name, err),
Reason: "IllegalPath",
}
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "IllegalPath", e.Err.Error())
return sreconcile.ResultEmpty, e
}
// Retrieve the included GitRepository
dep := &sourcev1.GitRepository{}
if err := r.Get(ctx, types.NamespacedName{Namespace: obj.Namespace, Name: incl.GitRepositoryRef.Name}, dep); err != nil {
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "NotFound",
"Could not get resource for include '%s': %s", incl.GitRepositoryRef.Name, err.Error())
return ctrl.Result{}, err
e := &serror.Event{
Err: fmt.Errorf("could not get resource for include '%s': %w", incl.GitRepositoryRef.Name, err),
Reason: "NotFound",
}
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "NotFound", e.Err.Error())
return sreconcile.ResultEmpty, err
}
// Confirm include has an artifact
if dep.GetArtifact() == nil {
ctrl.LoggerFrom(ctx).Error(nil, fmt.Sprintf("No artifact available for include '%s'", incl.GitRepositoryRef.Name))
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "NoArtifact",
"No artifact available for include '%s'", incl.GitRepositoryRef.Name)
return ctrl.Result{}, nil
e := &serror.Stalling{
Err: fmt.Errorf("no artifact available for include '%s'", incl.GitRepositoryRef.Name),
Reason: "NoArtifact",
}
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "NoArtifact", e.Err.Error())
return sreconcile.ResultEmpty, e
}
// Copy artifact (sub)contents to configured directory
if err := r.Storage.CopyToPath(dep.GetArtifact(), incl.GetFromPath(), toPath); err != nil {
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "CopyFailure",
"Failed to copy '%s' include from %s to %s: %s", incl.GitRepositoryRef.Name, incl.GetFromPath(), incl.GetToPath(), err.Error())
r.Eventf(obj, events.EventSeverityError, sourcev1.IncludeUnavailableCondition,
"Failed to copy '%s' include from %s to %s: %s", incl.GitRepositoryRef.Name, incl.GetFromPath(), incl.GetToPath(), err.Error())
return ctrl.Result{}, err
e := &serror.Event{
Err: fmt.Errorf("Failed to copy '%s' include from %s to %s: %w", incl.GitRepositoryRef.Name, incl.GetFromPath(), incl.GetToPath(), err),
Reason: "CopyFailure",
}
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "CopyFailure", e.Err.Error())
return sreconcile.ResultEmpty, e
}
artifacts[i] = dep.GetArtifact().DeepCopy()
}
@ -511,33 +557,34 @@ func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context, obj *sou
// Observe if the artifacts still match the previous included ones
if artifacts.Diff(obj.Status.IncludedArtifacts) {
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "IncludeChange", "Included artifacts differ from last observed includes")
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "IncludeChange",
"included artifacts differ from last observed includes")
}
return ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
return sreconcile.ResultSuccess, nil
}
// reconcileDelete handles the delete of an object. It first garbage collects all artifacts for the object from the
// artifact storage, if successful, the finalizer is removed from the object.
func (r *GitRepositoryReconciler) reconcileDelete(ctx context.Context, obj *sourcev1.GitRepository) (ctrl.Result, error) {
func (r *GitRepositoryReconciler) reconcileDelete(ctx context.Context, obj *sourcev1.GitRepository) (sreconcile.Result, error) {
// Garbage collect the resource's artifacts
if err := r.garbageCollect(ctx, obj); err != nil {
// Return the error so we retry the failed garbage collection
return ctrl.Result{}, err
return sreconcile.ResultEmpty, err
}
// Remove our finalizer from the list
controllerutil.RemoveFinalizer(obj, sourcev1.SourceFinalizer)
// Stop reconciliation as the object is being deleted
return ctrl.Result{}, nil
return sreconcile.ResultEmpty, nil
}
// verifyCommitSignature verifies the signature of the given commit if a verification mode is configured on the object.
func (r *GitRepositoryReconciler) verifyCommitSignature(ctx context.Context, obj *sourcev1.GitRepository, commit git.Commit) (ctrl.Result, error) {
func (r *GitRepositoryReconciler) verifyCommitSignature(ctx context.Context, obj *sourcev1.GitRepository, commit git.Commit) (sreconcile.Result, error) {
// Check if there is a commit verification is configured and remove any old observations if there is none
if obj.Spec.Verification == nil || obj.Spec.Verification.Mode == "" {
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
return ctrl.Result{RequeueAfter: obj.GetRequeueAfter()}, nil
return sreconcile.ResultSuccess, nil
}
// Get secret with GPG data
@ -547,9 +594,12 @@ func (r *GitRepositoryReconciler) verifyCommitSignature(ctx context.Context, obj
}
secret := &corev1.Secret{}
if err := r.Client.Get(ctx, publicKeySecret, secret); err != nil {
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, meta.FailedReason, "PGP public keys secret error: %s", err.Error())
r.Eventf(obj, events.EventSeverityError, "VerificationError", "PGP public keys secret error: %s", err.Error())
return ctrl.Result{}, err
e := &serror.Event{
Err: fmt.Errorf("PGP public keys secret error: %w", err),
Reason: "VerificationError",
}
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, meta.FailedReason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
var keyRings []string
@ -558,15 +608,20 @@ func (r *GitRepositoryReconciler) verifyCommitSignature(ctx context.Context, obj
}
// Verify commit with GPG data from secret
if _, err := commit.Verify(keyRings...); err != nil {
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, meta.FailedReason, "Signature verification of commit '%s' failed: %s", commit.Hash.String(), err)
r.Eventf(obj, events.EventSeverityError, "InvalidCommitSignature", "Signature verification of commit '%s' failed: %s", commit.Hash.String(), err)
e := &serror.Event{
Err: fmt.Errorf("signature verification of commit '%s' failed: %w", commit.Hash.String(), err),
Reason: "InvalidCommitSignature",
}
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, meta.FailedReason, e.Err.Error())
// Return error in the hope the secret changes
return ctrl.Result{}, err
return sreconcile.ResultEmpty, e
}
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "Verified signature of commit '%s'", commit.Hash.String())
r.Eventf(obj, events.EventSeverityInfo, "VerifiedCommit", "Verified signature of commit '%s'", commit.Hash.String())
return ctrl.Result{RequeueAfter: obj.Spec.Interval.Duration}, nil
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason,
"verified signature of commit '%s'", commit.Hash.String())
r.eventLogf(ctx, obj, corev1.EventTypeNormal, "VerifiedCommit",
"verified signature of commit '%s'", commit.Hash.String())
return sreconcile.ResultSuccess, nil
}
// garbageCollect performs a garbage collection for the given v1beta1.GitRepository. It removes all but the current
@ -575,23 +630,40 @@ func (r *GitRepositoryReconciler) verifyCommitSignature(ctx context.Context, obj
func (r *GitRepositoryReconciler) garbageCollect(ctx context.Context, obj *sourcev1.GitRepository) error {
if !obj.DeletionTimestamp.IsZero() {
if err := r.Storage.RemoveAll(r.Storage.NewArtifactFor(obj.Kind, obj.GetObjectMeta(), "", "*")); err != nil {
r.Eventf(obj, events.EventSeverityError, "GarbageCollectionFailed",
"Garbage collection for deleted resource failed: %s", err)
return err
return &serror.Event{
Err: fmt.Errorf("garbage collection for deleted resource failed: %w", err),
Reason: "GarbageCollectionFailed",
}
}
obj.Status.Artifact = nil
// TODO(hidde): we should only push this event if we actually garbage collected something
r.Eventf(obj, events.EventSeverityInfo, "GarbageCollectionSucceeded",
"Garbage collected artifacts for deleted resource")
r.eventLogf(ctx, obj, corev1.EventTypeNormal, "GarbageCollectionSucceeded",
"garbage collected artifacts for deleted resource")
return nil
}
if obj.GetArtifact() != nil {
if err := r.Storage.RemoveAllButCurrent(*obj.GetArtifact()); err != nil {
r.Eventf(obj, events.EventSeverityError, "GarbageCollectionFailed", "Garbage collection of old artifacts failed: %s", err)
return err
return &serror.Event{
Err: fmt.Errorf("garbage collection of old artifacts failed: %w", err),
}
}
// TODO(hidde): we should only push this event if we actually garbage collected something
r.Eventf(obj, events.EventSeverityInfo, "GarbageCollectionSucceeded", "Garbage collected old artifacts")
r.eventLogf(ctx, obj, corev1.EventTypeNormal, "GarbageCollectionSucceeded",
"garbage collected old artifacts")
}
return nil
}
// eventLog records event and logs at the same time. This log is different from
// the debug log in the event recorder in the sense that this is a simple log,
// the event recorder debug log contains complete details about the event.
func (r *GitRepositoryReconciler) eventLogf(ctx context.Context, obj runtime.Object, eventType string, reason string, messageFmt string, args ...interface{}) {
msg := fmt.Sprintf(messageFmt, args...)
// Log and emit event.
if eventType == corev1.EventTypeWarning {
ctrl.LoggerFrom(ctx).Error(errors.New(reason), msg)
} else {
ctrl.LoggerFrom(ctx).Info(msg)
}
r.Eventf(obj, eventType, reason, msg)
}

View File

@ -25,6 +25,7 @@ import (
"testing"
"time"
"github.com/darkowlzz/controller-check/status"
"github.com/go-git/go-billy/v5/memfs"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
@ -53,6 +54,7 @@ import (
"github.com/fluxcd/pkg/testserver"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
"github.com/fluxcd/source-controller/pkg/git"
)
@ -189,6 +191,11 @@ func TestGitRepositoryReconciler_Reconcile(t *testing.T) {
obj.Generation == obj.Status.ObservedGeneration
}, timeout).Should(BeTrue())
// Check if the object status is valid.
condns := &status.Conditions{NegativePolarity: gitRepoReadyDepsNegative}
checker := status.NewChecker(testEnv.Client, testEnv.GetScheme(), condns)
checker.CheckErr(ctx, obj)
g.Expect(testEnv.Delete(ctx, obj)).To(Succeed())
// Wait for GitRepository to be deleted
@ -216,16 +223,17 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
server options
secret *corev1.Secret
beforeFunc func(obj *sourcev1.GitRepository)
want ctrl.Result
want sreconcile.Result
wantErr bool
assertConditions []metav1.Condition
}{
{
name: "HTTP without secretRef makes ArtifactOutdated=True",
protocol: "http",
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "New upstream revision 'master/<commit>'"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new upstream revision 'master/<commit>'"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'master/<commit>'"),
},
},
{
@ -247,9 +255,10 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "basic-auth"}
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "New upstream revision 'master/<commit>'"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new upstream revision 'master/<commit>'"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'master/<commit>'"),
},
},
{
@ -271,9 +280,10 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "ca-file"}
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "New upstream revision 'master/<commit>'"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new upstream revision 'master/<commit>'"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'master/<commit>'"),
},
},
{
@ -323,7 +333,7 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "Failed to checkout and determine revision: unable to clone: Certificate"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "failed to checkout and determine revision: unable to clone '<url>': Certificate"),
},
},
{
@ -344,9 +354,10 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "private-key"}
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "New upstream revision 'master/<commit>'"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new upstream revision 'master/<commit>'"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'master/<commit>'"),
},
},
{
@ -368,9 +379,10 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "private-key"}
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "New upstream revision 'master/<commit>'"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new upstream revision 'master/<commit>'"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'master/<commit>'"),
},
},
{
@ -384,7 +396,7 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "Failed to get secret '/non-existing': secrets \"non-existing\" not found"),
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get secret '/non-existing': secrets \"non-existing\" not found"),
},
},
}
@ -486,9 +498,10 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
}
var artifact sourcev1.Artifact
var includes artifactSet
dlog := log.NewDelegatingLogSink(log.NullLogSink{})
nullLogger := logr.New(dlog)
got, err := r.reconcileSource(logr.NewContext(ctx, nullLogger), obj, &artifact, tmpDir)
got, err := r.reconcileSource(logr.NewContext(ctx, nullLogger), obj, &artifact, &includes, tmpDir)
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
g.Expect(err != nil).To(Equal(tt.wantErr))
g.Expect(got).To(Equal(tt.want))
@ -509,13 +522,13 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
name string
skipForImplementation string
reference *sourcev1.GitRepositoryRef
want ctrl.Result
want sreconcile.Result
wantErr bool
wantRevision string
}{
{
name: "Nil reference (default branch)",
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
wantRevision: "master/<commit>",
},
{
@ -523,7 +536,7 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
reference: &sourcev1.GitRepositoryRef{
Branch: "staging",
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
wantRevision: "staging/<commit>",
},
{
@ -531,7 +544,7 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
reference: &sourcev1.GitRepositoryRef{
Tag: "v0.1.0",
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
wantRevision: "v0.1.0/<commit>",
},
{
@ -541,7 +554,7 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
Branch: "staging",
Commit: "<commit>",
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
wantRevision: "staging/<commit>",
},
{
@ -551,7 +564,7 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
Branch: "staging",
Commit: "<commit>",
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
wantRevision: "HEAD/<commit>",
},
{
@ -559,7 +572,7 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
reference: &sourcev1.GitRepositoryRef{
SemVer: "*",
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
wantRevision: "v2.0.0/<commit>",
},
{
@ -567,7 +580,7 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
reference: &sourcev1.GitRepositoryRef{
SemVer: "<v0.2.1",
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
wantRevision: "0.2.0/<commit>",
},
{
@ -576,7 +589,7 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
SemVer: ">=1.0.0-0 <1.1.0-0",
},
wantRevision: "v1.0.0-alpha/<commit>",
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
},
}
@ -641,7 +654,8 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
obj.Spec.GitImplementation = i
var artifact sourcev1.Artifact
got, err := r.reconcileSource(ctx, obj, &artifact, tmpDir)
var includes artifactSet
got, err := r.reconcileSource(ctx, obj, &artifact, &includes, tmpDir)
if err != nil {
println(err.Error())
}
@ -665,7 +679,7 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) {
includes artifactSet
beforeFunc func(obj *sourcev1.GitRepository)
afterFunc func(t *WithT, obj *sourcev1.GitRepository, artifact sourcev1.Artifact)
want ctrl.Result
want sreconcile.Result
wantErr bool
assertConditions []metav1.Condition
}{
@ -679,9 +693,10 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) {
t.Expect(obj.GetArtifact()).ToNot(BeNil())
t.Expect(obj.Status.URL).ToNot(BeEmpty())
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'main/revision'"),
},
},
{
@ -697,9 +712,10 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) {
t.Expect(obj.Status.IncludedArtifacts).ToNot(BeEmpty())
t.Expect(obj.Status.URL).ToNot(BeEmpty())
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'main/revision'"),
},
},
{
@ -714,9 +730,9 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) {
afterFunc: func(t *WithT, obj *sourcev1.GitRepository, artifact sourcev1.Artifact) {
t.Expect(obj.Status.URL).To(BeEmpty())
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision 'main/revision'"),
},
},
{
@ -730,26 +746,10 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) {
t.Expect(obj.GetArtifact()).ToNot(BeNil())
t.Expect(obj.GetArtifact().Checksum).To(Equal("dc95ae14c19d335b693bbba58ae2a562242b0cf33893baffd1b7605ba578e0d6"))
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Stored artifact for revision 'main/revision'"),
},
},
{
name: "Removes ArtifactUnavailableCondition after creating artifact",
dir: "testdata/git/repository",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
conditions.MarkTrue(obj, sourcev1.ArtifactUnavailableCondition, "Foo", "")
},
afterFunc: func(t *WithT, obj *sourcev1.GitRepository, artifact sourcev1.Artifact) {
t.Expect(obj.GetArtifact()).ToNot(BeNil())
t.Expect(obj.GetArtifact().Checksum).To(Equal("ef9c34eab0584035ac8b8a4070876954ea46f270250d60648672feef3e943426"))
t.Expect(obj.Status.URL).ToNot(BeEmpty())
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'main/revision'"),
},
},
{
@ -764,9 +764,10 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) {
t.Expect(obj.GetArtifact().Checksum).To(Equal("ef9c34eab0584035ac8b8a4070876954ea46f270250d60648672feef3e943426"))
t.Expect(obj.Status.URL).ToNot(BeEmpty())
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'main/revision'"),
},
},
{
@ -784,20 +785,27 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) {
t.Expect(err).NotTo(HaveOccurred())
t.Expect(localPath).To(Equal(targetFile))
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision 'main/revision'"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'main/revision'"),
},
},
{
name: "Target path does not exists",
dir: "testdata/git/foo",
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'main/revision'"),
},
},
{
name: "Target path is not a directory",
dir: "testdata/git/repository/foo.txt",
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new upstream revision 'main/revision'"),
},
},
}
@ -824,7 +832,7 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) {
artifact := testStorage.NewArtifactFor(obj.Kind, obj, "main/revision", "checksum.tar.gz")
got, err := r.reconcileArtifact(ctx, obj, artifact, tt.includes, tt.dir)
got, err := r.reconcileArtifact(ctx, obj, &artifact, &tt.includes, tt.dir)
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
g.Expect(err != nil).To(Equal(tt.wantErr))
g.Expect(got).To(Equal(tt.want))
@ -865,7 +873,7 @@ func TestGitRepositoryReconciler_reconcileInclude(t *testing.T) {
dependencies []dependency
includes []include
beforeFunc func(obj *sourcev1.GitRepository)
want ctrl.Result
want sreconcile.Result
wantErr bool
assertConditions []metav1.Condition
}{
@ -891,9 +899,9 @@ func TestGitRepositoryReconciler_reconcileInclude(t *testing.T) {
{name: "a", toPath: "a/", shouldExist: true},
{name: "b", toPath: "b/", shouldExist: true},
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "IncludeChange", "Included artifacts differ from last observed includes"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "IncludeChange", "included artifacts differ from last observed includes"),
},
},
{
@ -903,7 +911,7 @@ func TestGitRepositoryReconciler_reconcileInclude(t *testing.T) {
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.IncludeUnavailableCondition, "NotFound", "Could not get resource for include 'a': gitrepositories.source.toolkit.fluxcd.io \"a\" not found"),
*conditions.TrueCondition(sourcev1.IncludeUnavailableCondition, "NotFound", "could not get resource for include 'a': gitrepositories.source.toolkit.fluxcd.io \"a\" not found"),
},
},
{
@ -920,8 +928,9 @@ func TestGitRepositoryReconciler_reconcileInclude(t *testing.T) {
includes: []include{
{name: "a", toPath: "a/"},
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.IncludeUnavailableCondition, "NoArtifact", "No artifact available for include 'a'"),
*conditions.TrueCondition(sourcev1.IncludeUnavailableCondition, "NoArtifact", "no artifact available for include 'a'"),
},
},
{
@ -945,7 +954,7 @@ func TestGitRepositoryReconciler_reconcileInclude(t *testing.T) {
beforeFunc: func(obj *sourcev1.GitRepository) {
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "NoArtifact", "")
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{},
},
}
@ -980,12 +989,8 @@ func TestGitRepositoryReconciler_reconcileInclude(t *testing.T) {
}
r := &GitRepositoryReconciler{
Client: builder.Build(),
EventRecorder: record.NewFakeRecorder(32),
// Events: helper.Events{
// Scheme: testEnv.GetScheme(),
// EventRecorder: record.NewFakeRecorder(32),
// },
Client: builder.Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: storage,
requeueDependency: dependencyInterval,
}
@ -1018,7 +1023,10 @@ func TestGitRepositoryReconciler_reconcileInclude(t *testing.T) {
g.Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(tmpDir)
got, err := r.reconcileInclude(ctx, obj, tmpDir)
var artifact sourcev1.Artifact
var includes artifactSet
got, err := r.reconcileInclude(ctx, obj, &artifact, &includes, tmpDir)
g.Expect(obj.GetConditions()).To(conditions.MatchConditions(tt.assertConditions))
g.Expect(err != nil).To(Equal(tt.wantErr))
g.Expect(got).To(Equal(tt.want))
@ -1065,7 +1073,7 @@ func TestGitRepositoryReconciler_reconcileDelete(t *testing.T) {
got, err := r.reconcileDelete(ctx, obj)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(got).To(Equal(ctrl.Result{}))
g.Expect(got).To(Equal(sreconcile.ResultEmpty))
g.Expect(controllerutil.ContainsFinalizer(obj, sourcev1.SourceFinalizer)).To(BeFalse())
g.Expect(obj.Status.Artifact).To(BeNil())
}
@ -1076,7 +1084,7 @@ func TestGitRepositoryReconciler_verifyCommitSignature(t *testing.T) {
secret *corev1.Secret
commit git.Commit
beforeFunc func(obj *sourcev1.GitRepository)
want ctrl.Result
want sreconcile.Result
wantErr bool
assertConditions []metav1.Condition
}{
@ -1104,9 +1112,9 @@ func TestGitRepositoryReconciler_verifyCommitSignature(t *testing.T) {
},
}
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "Verified signature of commit 'shasum'"),
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of commit 'shasum'"),
},
},
{
@ -1132,7 +1140,7 @@ func TestGitRepositoryReconciler_verifyCommitSignature(t *testing.T) {
},
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, meta.FailedReason, "Signature verification of commit 'shasum' failed: failed to verify commit with any of the given key rings"),
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, meta.FailedReason, "signature verification of commit 'shasum' failed: failed to verify commit with any of the given key rings"),
},
},
{
@ -1157,7 +1165,7 @@ func TestGitRepositoryReconciler_verifyCommitSignature(t *testing.T) {
obj.Spec.Interval = metav1.Duration{Duration: interval}
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Foo", "")
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{},
},
{
@ -1167,7 +1175,7 @@ func TestGitRepositoryReconciler_verifyCommitSignature(t *testing.T) {
obj.Spec.Verification = &sourcev1.GitRepositoryVerification{}
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Foo", "")
},
want: ctrl.Result{RequeueAfter: interval},
want: sreconcile.ResultSuccess,
assertConditions: []metav1.Condition{},
},
}
@ -1233,7 +1241,7 @@ func TestGitRepositoryReconciler_ConditionsUpdate(t *testing.T) {
name: "no condition",
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "Stored artifact for revision"),
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
},
},
{
@ -1243,7 +1251,7 @@ func TestGitRepositoryReconciler_ConditionsUpdate(t *testing.T) {
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "Stored artifact for revision"),
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
},
},
{
@ -1253,7 +1261,7 @@ func TestGitRepositoryReconciler_ConditionsUpdate(t *testing.T) {
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "Stored artifact for revision"),
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
},
},
{
@ -1263,11 +1271,10 @@ func TestGitRepositoryReconciler_ConditionsUpdate(t *testing.T) {
conditions.MarkTrue(obj, sourcev1.IncludeUnavailableCondition, "Foo", "")
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Foo", "")
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "Foo", "")
conditions.MarkTrue(obj, sourcev1.ArtifactUnavailableCondition, "Foo", "")
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "Stored artifact for revision"),
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
},
},
{
@ -1278,7 +1285,7 @@ func TestGitRepositoryReconciler_ConditionsUpdate(t *testing.T) {
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "Stored artifact for revision"),
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
},
},
{
@ -1289,7 +1296,7 @@ func TestGitRepositoryReconciler_ConditionsUpdate(t *testing.T) {
},
want: ctrl.Result{RequeueAfter: interval},
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "Stored artifact for revision"),
*conditions.TrueCondition(meta.ReadyCondition, "Succeeded", "stored artifact for revision"),
},
},
}

2
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7
github.com/cyphar/filepath-securejoin v0.2.2
github.com/darkowlzz/controller-check v0.0.0-20220119215126-648356cef22c
github.com/elazarl/goproxy v0.0.0-20211114080932-d06c3be7c11b
github.com/fluxcd/pkg/apis/meta v0.11.0-rc.3
github.com/fluxcd/pkg/gittestserver v0.5.0
@ -115,6 +116,7 @@ require (
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/klauspost/compress v1.13.5 // indirect
github.com/klauspost/cpuid v1.3.1 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lib/pq v1.10.0 // indirect

4
go.sum
View File

@ -231,6 +231,8 @@ github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1S
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
github.com/darkowlzz/controller-check v0.0.0-20220119215126-648356cef22c h1:pyp/Dvd1gYP/D3z1zs46h0YhYzFp0hjxw0XVIO9+vh4=
github.com/darkowlzz/controller-check v0.0.0-20220119215126-648356cef22c/go.mod h1:haYO9UW76kUUKpIBbv3ydaU5wZ/7r0yqp61PGzVRSYU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -617,6 +619,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=