302 lines
9.0 KiB
Go
302 lines
9.0 KiB
Go
/*
|
|
Copyright 2020 The Crossplane 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 perimpliedions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package resolver
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
"k8s.io/client-go/kubernetes"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/manager"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
"github.com/crossplane/crossplane-runtime/pkg/errors"
|
|
"github.com/crossplane/crossplane-runtime/pkg/event"
|
|
"github.com/crossplane/crossplane-runtime/pkg/logging"
|
|
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
|
|
|
v1 "github.com/crossplane/crossplane/apis/pkg/v1"
|
|
"github.com/crossplane/crossplane/apis/pkg/v1beta1"
|
|
"github.com/crossplane/crossplane/internal/controller/pkg/controller"
|
|
"github.com/crossplane/crossplane/internal/dag"
|
|
"github.com/crossplane/crossplane/internal/xpkg"
|
|
)
|
|
|
|
const (
|
|
reconcileTimeout = 1 * time.Minute
|
|
|
|
shortWait = 30 * time.Second
|
|
|
|
packageTagFmt = "%s:%s"
|
|
)
|
|
|
|
const (
|
|
finalizer = "lock.pkg.crossplane.io"
|
|
|
|
errGetLock = "cannot get package lock"
|
|
errAddFinalizer = "cannot add lock finalizer"
|
|
errRemoveFinalizer = "cannot remove lock finalizer"
|
|
errBuildDAG = "cannot build DAG"
|
|
errSortDAG = "cannot sort DAG"
|
|
errMissingDependencyFmt = "missing package (%s) is not a dependency"
|
|
errInvalidConstraint = "version constraint on dependency is invalid"
|
|
errInvalidDependency = "dependency package is not valid"
|
|
errFetchTags = "cannot fetch dependency package tags"
|
|
errNoValidVersion = "cannot find a valid version for package constraints"
|
|
errNoValidVersionFmt = "dependency (%s) does not have version in constraints (%s)"
|
|
errInvalidPackageType = "cannot create invalid package dependency type"
|
|
errCreateDependency = "cannot create dependency package"
|
|
)
|
|
|
|
// ReconcilerOption is used to configure the Reconciler.
|
|
type ReconcilerOption func(*Reconciler)
|
|
|
|
// WithLogger specifies how the Reconciler should log messages.
|
|
func WithLogger(log logging.Logger) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.log = log
|
|
}
|
|
}
|
|
|
|
// WithRecorder specifies how the Reconciler should record Kubernetes events.
|
|
func WithRecorder(er event.Recorder) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.record = er
|
|
}
|
|
}
|
|
|
|
// WithFinalizer specifies how the Reconciler should finalize package revisions.
|
|
func WithFinalizer(f resource.Finalizer) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.lock = f
|
|
}
|
|
}
|
|
|
|
// WithNewDagFn specifies how the Reconciler should build its dependency graph.
|
|
func WithNewDagFn(f dag.NewDAGFn) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.newDag = f
|
|
}
|
|
}
|
|
|
|
// WithFetcher specifies how the Reconciler should fetch package tags.
|
|
func WithFetcher(f xpkg.Fetcher) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.fetcher = f
|
|
}
|
|
}
|
|
|
|
// Reconciler reconciles packages.
|
|
type Reconciler struct {
|
|
client client.Client
|
|
log logging.Logger
|
|
record event.Recorder
|
|
lock resource.Finalizer
|
|
newDag dag.NewDAGFn
|
|
fetcher xpkg.Fetcher
|
|
}
|
|
|
|
// Setup adds a controller that reconciles the Lock.
|
|
func Setup(mgr ctrl.Manager, o controller.Options) error {
|
|
name := "packages/" + strings.ToLower(v1beta1.LockGroupKind)
|
|
|
|
cs, err := kubernetes.NewForConfig(mgr.GetConfig())
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to initialize clientset")
|
|
}
|
|
f, err := xpkg.NewK8sFetcher(cs, o.Namespace, o.FetcherOptions...)
|
|
if err != nil {
|
|
return errors.Wrap(err, "cannot build fetcher")
|
|
}
|
|
|
|
r := NewReconciler(mgr,
|
|
WithLogger(o.Logger.WithValues("controller", name)),
|
|
WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))),
|
|
WithFetcher(f),
|
|
)
|
|
|
|
return ctrl.NewControllerManagedBy(mgr).
|
|
Named(name).
|
|
For(&v1beta1.Lock{}).
|
|
Owns(&v1.ConfigurationRevision{}).
|
|
Owns(&v1.ProviderRevision{}).
|
|
Complete(r)
|
|
}
|
|
|
|
// NewReconciler creates a new package revision reconciler.
|
|
func NewReconciler(mgr manager.Manager, opts ...ReconcilerOption) *Reconciler {
|
|
r := &Reconciler{
|
|
client: mgr.GetClient(),
|
|
lock: resource.NewAPIFinalizer(mgr.GetClient(), finalizer),
|
|
log: logging.NewNopLogger(),
|
|
record: event.NewNopRecorder(),
|
|
newDag: dag.NewMapDag,
|
|
fetcher: xpkg.NewNopFetcher(),
|
|
}
|
|
|
|
for _, f := range opts {
|
|
f(r)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// Reconcile package revision.
|
|
func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { // nolint:gocyclo
|
|
log := r.log.WithValues("request", req)
|
|
log.Debug("Reconciling")
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, reconcileTimeout)
|
|
defer cancel()
|
|
|
|
lock := &v1beta1.Lock{}
|
|
if err := r.client.Get(ctx, req.NamespacedName, lock); err != nil {
|
|
// There's no need to requeue if we no longer exist. Otherwise we'll be
|
|
// requeued implicitly because we return an error.
|
|
log.Debug(errGetLock, "error", err)
|
|
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetLock)
|
|
}
|
|
|
|
// If no packages exist in Lock then we remove finalizer and wait until a
|
|
// package is added to reconcile again. This allows for cleanup of the Lock
|
|
// when uninstalling Crossplane after all packages have already been
|
|
// uninstalled.
|
|
if len(lock.Packages) == 0 {
|
|
if err := r.lock.RemoveFinalizer(ctx, lock); err != nil {
|
|
log.Debug(errRemoveFinalizer, "error", err)
|
|
return reconcile.Result{RequeueAfter: shortWait}, nil
|
|
}
|
|
return reconcile.Result{}, nil
|
|
}
|
|
|
|
if err := r.lock.AddFinalizer(ctx, lock); err != nil {
|
|
log.Debug(errAddFinalizer, "error", err)
|
|
return reconcile.Result{RequeueAfter: shortWait}, nil
|
|
}
|
|
|
|
log = log.WithValues(
|
|
"uid", lock.GetUID(),
|
|
"version", lock.GetResourceVersion(),
|
|
"name", lock.GetName(),
|
|
)
|
|
|
|
dag := r.newDag()
|
|
implied, err := dag.Init(v1beta1.ToNodes(lock.Packages...))
|
|
if err != nil {
|
|
return reconcile.Result{}, errors.Wrap(err, errBuildDAG)
|
|
}
|
|
|
|
// Make sure we don't have any cyclical imports. If we do, refuse to install
|
|
// additional packages.
|
|
_, err = dag.Sort()
|
|
if err != nil {
|
|
return reconcile.Result{}, errors.Wrap(err, errSortDAG)
|
|
}
|
|
|
|
if len(implied) == 0 {
|
|
return reconcile.Result{}, nil
|
|
}
|
|
|
|
// If we are missing a node, we want to create it. The resolver never
|
|
// modifies the Lock. We only create the first implied node as we will be
|
|
// requeued when it adds itself to the Lock, at which point we will check
|
|
// for missing nodes again.
|
|
dep, ok := implied[0].(*v1beta1.Dependency)
|
|
if !ok {
|
|
log.Debug(errInvalidDependency, "error", errors.Errorf(errMissingDependencyFmt, dep.Identifier()))
|
|
return reconcile.Result{}, nil
|
|
}
|
|
c, err := semver.NewConstraint(dep.Constraints)
|
|
if err != nil {
|
|
log.Debug(errInvalidConstraint, "error", err)
|
|
return reconcile.Result{}, nil
|
|
}
|
|
ref, err := name.ParseReference(dep.Package)
|
|
if err != nil {
|
|
log.Debug(errInvalidDependency, "error", err)
|
|
return reconcile.Result{}, nil
|
|
}
|
|
|
|
// NOTE(hasheddan): we will be unable to fetch tags for private
|
|
// dependencies because we do not attach any secrets. Consider copying
|
|
// secrets from parent dependencies.
|
|
tags, err := r.fetcher.Tags(ctx, ref)
|
|
if err != nil {
|
|
log.Debug(errFetchTags, "error", err)
|
|
return reconcile.Result{RequeueAfter: shortWait}, nil
|
|
}
|
|
|
|
vs := []*semver.Version{}
|
|
for _, r := range tags {
|
|
v, err := semver.NewVersion(r)
|
|
if err != nil {
|
|
// We skip any tags that are not valid semantic versions.
|
|
continue
|
|
}
|
|
vs = append(vs, v)
|
|
}
|
|
|
|
sort.Sort(semver.Collection(vs))
|
|
var addVer string
|
|
for _, v := range vs {
|
|
if c.Check(v) {
|
|
addVer = v.Original()
|
|
}
|
|
}
|
|
|
|
// NOTE(hasheddan): consider creating event on package revision
|
|
// dictating constraints.
|
|
if addVer == "" {
|
|
log.Debug(errNoValidVersion, errors.Errorf(errNoValidVersionFmt, dep.Identifier(), dep.Constraints))
|
|
return reconcile.Result{}, nil
|
|
}
|
|
|
|
var pack v1.Package
|
|
switch dep.Type {
|
|
case v1beta1.ConfigurationPackageType:
|
|
pack = &v1.Configuration{}
|
|
case v1beta1.ProviderPackageType:
|
|
pack = &v1.Provider{}
|
|
default:
|
|
log.Debug(errInvalidPackageType)
|
|
return reconcile.Result{}, nil
|
|
}
|
|
|
|
// NOTE(hasheddan): packages are currently created with default
|
|
// settings. This means that a dependency must be publicly available as
|
|
// no packagePullSecrets are set. Settings can be modified manually
|
|
// after dependency creation to address this.
|
|
pack.SetName(xpkg.ToDNSLabel(ref.Context().RepositoryStr()))
|
|
pack.SetSource(fmt.Sprintf(packageTagFmt, ref.String(), addVer))
|
|
|
|
// NOTE(hasheddan): consider making the lock the controller of packages
|
|
// it creates.
|
|
if err := r.client.Create(ctx, pack); err != nil {
|
|
log.Debug(errCreateDependency, "error", err)
|
|
return reconcile.Result{RequeueAfter: shortWait}, nil
|
|
}
|
|
|
|
return reconcile.Result{}, nil
|
|
}
|