Skip to content

Commit

Permalink
Add include property to GitRepositories
Browse files Browse the repository at this point in the history
  • Loading branch information
Philip Laine committed Apr 22, 2021
1 parent 14cf348 commit 50d7109
Show file tree
Hide file tree
Showing 10 changed files with 515 additions and 12 deletions.
16 changes: 15 additions & 1 deletion api/v1beta1/gitrepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ type GitRepositorySpec struct {
// This option is available only when using the 'go-git' GitImplementation.
// +optional
RecurseSubmodules bool `json:"recurseSubmodules,omitempty"`

// Extra repositories to map into the repository
Include []GitRepositoryInclude `json:"include,omitempty"`
}

//
type GitRepositoryInclude struct {
GitRepository meta.NamespacedObjectReference `json:"repository"`
Path string `json:"path"`
}

// GitRepositoryRef defines the Git ref used for pull and checkout operations.
Expand Down Expand Up @@ -138,6 +147,10 @@ type GitRepositoryStatus struct {
// +optional
Artifact *Artifact `json:"artifact,omitempty"`

// IncludeArtifacts represents the included artifacts from the last successful repository sync.
// +optional
IncludeArtifacts []*Artifact `json:"includeArtifcats,omitempty"`

meta.ReconcileRequestStatus `json:",inline"`
}

Expand Down Expand Up @@ -166,8 +179,9 @@ func GitRepositoryProgressing(repository GitRepository) GitRepository {
// GitRepositoryReady sets the given Artifact and URL on the GitRepository and
// sets the meta.ReadyCondition to 'True', with the given reason and message. It
// returns the modified GitRepository.
func GitRepositoryReady(repository GitRepository, artifact Artifact, url, reason, message string) GitRepository {
func GitRepositoryReady(repository GitRepository, artifact Artifact, includeArtifacts []*Artifact, url, reason, message string) GitRepository {
repository.Status.Artifact = &artifact
repository.Status.IncludeArtifacts = includeArtifacts
repository.Status.URL = url
meta.SetResourceCondition(&repository, meta.ReadyCondition, metav1.ConditionTrue, reason, message)
return repository
Expand Down
32 changes: 32 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_gitrepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,31 @@ spec:
a default will be used, consult the documentation for your version
to find out what those are.
type: string
include:
description: Extra repositories to map into the repository
items:
properties:
path:
type: string
repository:
description: NamespacedObjectReference contains enough information
to let you locate the referenced object in any namespace
properties:
name:
description: Name of the referent
type: string
namespace:
description: Namespace of the referent, when not specified
it acts as LocalObjectReference
type: string
required:
- name
type: object
required:
- path
- repository
type: object
type: array
interval:
description: The interval at which to check for repository updates.
type: string
Expand Down Expand Up @@ -245,6 +270,36 @@ spec:
- type
type: object
type: array
includeArtifcats:
description: IncludeArtifacts represents the included artifacts from
the last successful repository sync.
items:
description: Artifact represents the output of a source synchronisation.
properties:
checksum:
description: Checksum is the SHA1 checksum of the artifact.
type: string
lastUpdateTime:
description: LastUpdateTime is the timestamp corresponding to
the last update of this artifact.
format: date-time
type: string
path:
description: Path is the relative file path of this artifact.
type: string
revision:
description: Revision is a human readable identifier traceable
in the origin source system. It can be a Git commit SHA, Git
tag, a Helm index timestamp, a Helm chart version, etc.
type: string
url:
description: URL is the HTTP address of this artifact.
type: string
required:
- path
- url
type: object
type: array
lastHandledReconcileAt:
description: LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change can be detected.
Expand Down
103 changes: 95 additions & 8 deletions controllers/gitrepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"

Expand All @@ -33,6 +34,7 @@ import (
kuberecorder "k8s.io/client-go/tools/record"
"k8s.io/client-go/tools/reference"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
Expand All @@ -57,6 +59,7 @@ import (
// GitRepositoryReconciler reconciles a GitRepository object
type GitRepositoryReconciler struct {
client.Client
requeueDependency time.Duration
Scheme *runtime.Scheme
Storage *Storage
EventRecorder kuberecorder.EventRecorder
Expand All @@ -65,17 +68,21 @@ type GitRepositoryReconciler struct {
}

type GitRepositoryReconcilerOptions struct {
MaxConcurrentReconciles int
MaxConcurrentReconciles int
DependencyRequeueInterval time.Duration
}

func (r *GitRepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error {
return r.SetupWithManagerAndOptions(mgr, GitRepositoryReconcilerOptions{})
}

func (r *GitRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts GitRepositoryReconcilerOptions) error {
r.requeueDependency = opts.DependencyRequeueInterval

return ctrl.NewControllerManagedBy(mgr).
For(&sourcev1.GitRepository{}).
WithEventFilter(predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{})).
For(&sourcev1.GitRepository{}, builder.WithPredicates(
predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}),
)).
WithOptions(controller.Options{MaxConcurrentReconciles: opts.MaxConcurrentReconciles}).
Complete(r)
}
Expand Down Expand Up @@ -112,6 +119,25 @@ func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Reques
return ctrl.Result{}, nil
}

// check dependencies
if len(repository.Spec.Include) > 0 {
if err := r.checkDependencies(repository); err != nil {
repository = sourcev1.GitRepositoryNotReady(repository, meta.DependencyNotReadyReason, err.Error())
if err := r.updateStatus(ctx, req, repository.Status); err != nil {
log.Error(err, "unable to update status for dependency not ready")
return ctrl.Result{Requeue: true}, err
}
// we can't rely on exponential backoff because it will prolong the execution too much,
// instead we requeue on a fix interval.
msg := fmt.Sprintf("Dependencies do not meet ready condition, retrying in %s", r.requeueDependency.String())
log.Info(msg)
r.event(ctx, repository, events.EventSeverityInfo, msg)
r.recordReadiness(ctx, repository)
return ctrl.Result{RequeueAfter: r.requeueDependency}, nil
}
log.Info("All dependencies area ready, proceeding with reconciliation")
}

// record reconciliation duration
if r.MetricsRecorder != nil {
objRef, err := reference.GetReference(r.Scheme, &repository)
Expand Down Expand Up @@ -173,6 +199,30 @@ func (r *GitRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Reques
return ctrl.Result{RequeueAfter: repository.GetInterval().Duration}, nil
}

func (r *GitRepositoryReconciler) checkDependencies(repository sourcev1.GitRepository) error {
for _, d := range repository.Spec.Include {
if d.GitRepository.Namespace == "" {
d.GitRepository.Namespace = repository.GetNamespace()
}
dName := types.NamespacedName{Name: d.GitRepository.Name, Namespace: d.GitRepository.Namespace}
var gr sourcev1.GitRepository
err := r.Get(context.Background(), dName, &gr)
if err != nil {
return fmt.Errorf("unable to get '%s' dependency: %w", dName, err)
}

if len(gr.Status.Conditions) == 0 || gr.Generation != gr.Status.ObservedGeneration {
return fmt.Errorf("dependency '%s' is not ready", dName)
}

if !apimeta.IsStatusConditionTrue(gr.Status.Conditions, meta.ReadyCondition) {
return fmt.Errorf("dependency '%s' is not ready", dName)
}
}

return nil
}

func (r *GitRepositoryReconciler) reconcile(ctx context.Context, repository sourcev1.GitRepository) (sourcev1.GitRepository, error) {
// create tmp dir for the Git clone
tmpGit, err := ioutil.TempDir("", repository.Name)
Expand Down Expand Up @@ -219,18 +269,36 @@ func (r *GitRepositoryReconciler) reconcile(ctx context.Context, repository sour
git.CheckoutOptions{
GitImplementation: repository.Spec.GitImplementation,
RecurseSubmodules: repository.Spec.RecurseSubmodules,
})
},
)
if err != nil {
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
commit, revision, err := checkoutStrategy.Checkout(ctx, tmpGit, repository.Spec.URL, auth)
if err != nil {
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}

// return early on unchanged revision
artifact := r.Storage.NewArtifactFor(repository.Kind, repository.GetObjectMeta(), revision, fmt.Sprintf("%s.tar.gz", commit.Hash()))
if apimeta.IsStatusConditionTrue(repository.Status.Conditions, meta.ReadyCondition) && repository.GetArtifact().HasRevision(artifact.Revision) {

// copy all included repository into the artifact
includeArtifcats := []*sourcev1.Artifact{}
for _, incl := range repository.Spec.Include {
dName := types.NamespacedName{Name: incl.GitRepository.Name, Namespace: incl.GitRepository.Namespace}
var gr sourcev1.GitRepository
err := r.Get(context.Background(), dName, &gr)
if err != nil {
return sourcev1.GitRepositoryNotReady(repository, meta.DependencyNotReadyReason, err.Error()), err
}
includeArtifcats = append(includeArtifcats, gr.GetArtifact())

err = r.Storage.CopyToPath(gr.GetArtifact(), filepath.Join(tmpGit, incl.Path))
if err != nil {
return sourcev1.GitRepositoryNotReady(repository, meta.DependencyNotReadyReason, err.Error()), err
}
}

// return early on unchanged revision and unchanged included repositories
if apimeta.IsStatusConditionTrue(repository.Status.Conditions, meta.ReadyCondition) && repository.GetArtifact().HasRevision(artifact.Revision) && !hasArtifactUpdated(repository.Status.IncludeArtifacts, includeArtifcats) {
if artifact.URL != repository.GetArtifact().URL {
r.Storage.SetArtifactURL(repository.GetArtifact())
repository.Status.URL = r.Storage.SetHostname(repository.Status.URL)
Expand Down Expand Up @@ -293,7 +361,26 @@ func (r *GitRepositoryReconciler) reconcile(ctx context.Context, repository sour
}

message := fmt.Sprintf("Fetched revision: %s", artifact.Revision)
return sourcev1.GitRepositoryReady(repository, artifact, url, sourcev1.GitOperationSucceedReason, message), nil
return sourcev1.GitRepositoryReady(repository, artifact, includeArtifcats, url, sourcev1.GitOperationSucceedReason, message), nil
}

// hasArtifactUpdated returns true if any of the revisions in the current artifacts
// does not match any of the artifacts in the updated artifacts
func hasArtifactUpdated(current []*sourcev1.Artifact, updated []*sourcev1.Artifact) bool {
if len(current) != len(updated) {
return true
}

for _, c := range current {
for _, u := range updated {
if u.HasRevision(c.Revision) {
continue
}
}
return true
}

return false
}

func (r *GitRepositoryReconciler) reconcileDelete(ctx context.Context, repository sourcev1.GitRepository) (ctrl.Result, error) {
Expand Down
Loading

0 comments on commit 50d7109

Please sign in to comment.