image-automation-controller/internal/controller/imageupdateautomation_contr...

711 lines
26 KiB
Go

/*
Copyright 2020 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 controller
import (
"bytes"
"context"
"errors"
"fmt"
"net/url"
"os"
"strings"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
"github.com/ProtonMail/go-crypto/openpgp"
securejoin "github.com/cyphar/filepath-securejoin"
extgogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
kuberecorder "k8s.io/client-go/tools/record"
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"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/ratelimiter"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta2"
apiacl "github.com/fluxcd/pkg/apis/acl"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/fluxcd/pkg/git/repository"
"github.com/fluxcd/pkg/runtime/acl"
helper "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/logger"
"github.com/fluxcd/pkg/runtime/predicates"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
"github.com/fluxcd/image-automation-controller/internal/features"
"github.com/fluxcd/image-automation-controller/pkg/update"
)
const (
originRemote = "origin"
defaultMessageTemplate = `Update from image update automation`
repoRefKey = ".spec.gitRepository"
signingSecretKey = "git.asc"
signingPassphraseKey = "passphrase"
)
// TemplateData is the type of the value given to the commit message
// template.
type TemplateData struct {
AutomationObject types.NamespacedName
Updated update.Result
}
// ImageUpdateAutomationReconciler reconciles a ImageUpdateAutomation object
type ImageUpdateAutomationReconciler struct {
client.Client
EventRecorder kuberecorder.EventRecorder
helper.Metrics
NoCrossNamespaceRef bool
features map[string]bool
}
type ImageUpdateAutomationReconcilerOptions struct {
RateLimiter ratelimiter.RateLimiter
}
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imageupdateautomations,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imageupdateautomations/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories,verbs=get;list;watch
func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := ctrl.LoggerFrom(ctx)
debuglog := log.V(logger.DebugLevel)
tracelog := log.V(logger.TraceLevel)
start := time.Now()
var templateValues TemplateData
var auto imagev1.ImageUpdateAutomation
if err := r.Get(ctx, req.NamespacedName, &auto); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
defer func() {
// Always record suspend, readiness and duration metrics.
r.Metrics.RecordSuspend(ctx, &auto, auto.Spec.Suspend)
r.Metrics.RecordReadiness(ctx, &auto)
r.Metrics.RecordDuration(ctx, &auto, start)
}()
// If the object is under deletion, record the readiness, and remove our finalizer.
if !auto.ObjectMeta.DeletionTimestamp.IsZero() {
controllerutil.RemoveFinalizer(&auto, imagev1.ImageUpdateAutomationFinalizer)
if err := r.Update(ctx, &auto); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
// Add our finalizer if it does not exist.
// Note: Finalizers in general can only be added when the deletionTimestamp
// is not set.
if !controllerutil.ContainsFinalizer(&auto, imagev1.ImageUpdateAutomationFinalizer) {
patch := client.MergeFrom(auto.DeepCopy())
controllerutil.AddFinalizer(&auto, imagev1.ImageUpdateAutomationFinalizer)
if err := r.Patch(ctx, &auto, patch); err != nil {
log.Error(err, "unable to register finalizer")
return ctrl.Result{}, err
}
}
if auto.Spec.Suspend {
log.Info("ImageUpdateAutomation is suspended, skipping automation run")
return ctrl.Result{}, nil
}
templateValues.AutomationObject = req.NamespacedName
// whatever else happens, we've now "seen" the reconcile
// annotation if it's there
if token, ok := meta.ReconcileAnnotationValue(auto.GetAnnotations()); ok {
auto.Status.SetLastHandledReconcileRequest(token)
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
return ctrl.Result{Requeue: true}, err
}
}
// failWithError is a helper for bailing on the reconciliation.
failWithError := func(err error) (ctrl.Result, error) {
r.event(ctx, auto, eventv1.EventSeverityError, err.Error())
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, imagev1.ReconciliationFailedReason, err.Error())
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
log.Error(err, "failed to reconcile")
}
return ctrl.Result{Requeue: true}, err
}
// get the git repository object so it can be checked out
// only GitRepository objects are supported for now
if kind := auto.Spec.SourceRef.Kind; kind != sourcev1.GitRepositoryKind {
return failWithError(fmt.Errorf("source kind '%s' not supported", kind))
}
gitSpec := auto.Spec.GitSpec
if gitSpec == nil {
return failWithError(fmt.Errorf("source kind %s neccessitates field .spec.git", sourcev1.GitRepositoryKind))
}
var origin sourcev1.GitRepository
gitRepoNamespace := req.Namespace
if auto.Spec.SourceRef.Namespace != "" {
gitRepoNamespace = auto.Spec.SourceRef.Namespace
}
originName := types.NamespacedName{
Name: auto.Spec.SourceRef.Name,
Namespace: gitRepoNamespace,
}
debuglog.Info("fetching git repository", "gitrepository", originName)
if r.NoCrossNamespaceRef && gitRepoNamespace != auto.GetNamespace() {
err := acl.AccessDeniedError(fmt.Sprintf("can't access '%s/%s', cross-namespace references have been blocked",
auto.Spec.SourceRef.Kind, originName))
log.Error(err, "access denied to cross-namespaced resource")
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, apiacl.AccessDeniedReason,
err.Error())
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
return ctrl.Result{Requeue: true}, err
}
r.event(ctx, auto, eventv1.EventSeverityError, err.Error())
return ctrl.Result{}, nil
}
if err := r.Get(ctx, originName, &origin); err != nil {
if client.IgnoreNotFound(err) == nil {
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, imagev1.GitNotAvailableReason, "referenced git repository is missing")
log.Error(err, fmt.Sprintf("referenced git repository %s does not exist.", originName.String()))
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
return ctrl.Result{Requeue: true}, err
}
return ctrl.Result{}, nil // and assume we'll hear about it when it arrives
}
return ctrl.Result{}, err
}
// validate the git spec and default any values needed later, before proceeding
var checkoutRef *sourcev1.GitRepositoryRef
if gitSpec.Checkout != nil {
checkoutRef = &gitSpec.Checkout.Reference
tracelog.Info("using git repository ref from .spec.git.checkout", "ref", checkoutRef)
} else if r := origin.Spec.Reference; r != nil {
checkoutRef = r
tracelog.Info("using git repository ref from GitRepository spec", "ref", checkoutRef)
} // else remain as `nil` and git.DefaultBranch will be used.
tmp, err := os.MkdirTemp("", fmt.Sprintf("%s-%s", originName.Namespace, originName.Name))
if err != nil {
return failWithError(err)
}
defer func() {
if err := os.RemoveAll(tmp); err != nil {
log.Error(err, "failed to remove working directory", "path", tmp)
}
}()
// pushBranch contains the branch name the commit needs to be pushed to.
// It takes the value of the push branch if one is specified or, if the push
// config is nil, then it takes the value of the checkout branch if possible.
var pushBranch string
var switchBranch bool
if gitSpec.Push != nil && gitSpec.Push.Branch != "" {
pushBranch = gitSpec.Push.Branch
tracelog.Info("using push branch from .spec.push.branch", "branch", pushBranch)
// We only need to switch branches when a branch has been specified in
// the push spec and it is different than the one in the checkout ref.
if gitSpec.Push.Branch != checkoutRef.Branch {
switchBranch = true
}
} else {
// Here's where it gets constrained. If there's no push branch
// given, then the checkout ref must include a branch, and
// that can be used.
if checkoutRef == nil || checkoutRef.Branch == "" {
return failWithError(
fmt.Errorf("Push spec not provided, and cannot be inferred from .spec.git.checkout.ref or GitRepository .spec.ref"),
)
}
pushBranch = checkoutRef.Branch
tracelog.Info("using push branch from $ref.branch", "branch", pushBranch)
}
authOpts, err := r.getAuthOpts(ctx, &origin)
if err != nil {
return failWithError(err)
}
var proxyOpts *transport.ProxyOptions
if origin.Spec.ProxySecretRef != nil {
proxyOpts, err = r.getProxyOpts(ctx, origin.Spec.ProxySecretRef.Name, origin.GetNamespace())
if err != nil {
return failWithError(err)
}
}
clientOpts := r.getGitClientOpts(authOpts.Transport, proxyOpts, switchBranch)
gitClient, err := gogit.NewClient(tmp, authOpts, clientOpts...)
if err != nil {
return failWithError(err)
}
defer gitClient.Close()
opts := repository.CloneConfig{}
if checkoutRef != nil {
opts.Tag = checkoutRef.Tag
opts.SemVer = checkoutRef.SemVer
opts.Commit = checkoutRef.Commit
opts.Branch = checkoutRef.Branch
}
if enabled, _ := r.features[features.GitShallowClone]; enabled {
opts.ShallowClone = true
}
// Use the git operations timeout for the repo.
cloneCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration)
defer cancel()
debuglog.Info("attempting to clone git repository", "gitrepository", originName, "ref", checkoutRef, "working", tmp)
if _, err := gitClient.Clone(cloneCtx, origin.Spec.URL, opts); err != nil {
return failWithError(err)
}
// When there's a push branch specified, the pushed-to branch is where commits
// shall be made
if switchBranch {
// Use the git operations timeout for the repo.
fetchCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration)
defer cancel()
if err := gitClient.SwitchBranch(fetchCtx, pushBranch); err != nil {
return failWithError(err)
}
}
switch {
case auto.Spec.Update != nil && auto.Spec.Update.Strategy == imagev1.UpdateStrategySetters:
// For setters we first want to compile a list of _all_ the
// policies in the same namespace (maybe in the future this
// could be filtered by the automation object).
var policies imagev1_reflect.ImagePolicyList
if err := r.List(ctx, &policies, &client.ListOptions{Namespace: req.NamespacedName.Namespace}); err != nil {
return failWithError(err)
}
manifestsPath := tmp
if auto.Spec.Update.Path != "" {
tracelog.Info("adjusting update path according to .spec.update.path", "base", tmp, "spec-path", auto.Spec.Update.Path)
p, err := securejoin.SecureJoin(tmp, auto.Spec.Update.Path)
if err != nil {
return failWithError(err)
}
manifestsPath = p
}
debuglog.Info("updating with setters according to image policies", "count", len(policies.Items), "manifests-path", manifestsPath)
if tracelog.Enabled() {
for _, item := range policies.Items {
tracelog.Info("found policy", "namespace", item.Namespace, "name", item.Name, "latest-image", item.Status.LatestImage)
}
}
result, err := updateAccordingToSetters(ctx, tracelog, manifestsPath, manifestsPath, policies.Items)
if err != nil {
return failWithError(err)
}
templateValues.Updated = result
default:
log.Info("no update strategy given in the spec")
// no sense rescheduling until this resource changes
r.event(ctx, auto, eventv1.EventSeverityInfo, "no known update strategy in spec, failing trivially")
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, imagev1.NoStrategyReason, "no known update strategy is given for object")
return ctrl.Result{}, r.patchStatus(ctx, req, auto.Status)
}
debuglog.Info("ran updates to working dir", "working", tmp)
var signingEntity *openpgp.Entity
if gitSpec.Commit.SigningKey != nil {
if signingEntity, err = r.getSigningEntity(ctx, auto); err != nil {
return failWithError(err)
}
}
// construct the commit message from template and values
message, err := templateMsg(gitSpec.Commit.MessageTemplate, &templateValues)
if err != nil {
return failWithError(err)
}
var rev string
if len(templateValues.Updated.Files) > 0 {
// The status message depends on what happens next. Since there's
// more than one way to succeed, there's some if..else below, and
// early returns only on failure.
rev, err = gitClient.Commit(
git.Commit{
Author: git.Signature{
Name: gitSpec.Commit.Author.Name,
Email: gitSpec.Commit.Author.Email,
When: time.Now(),
},
Message: message,
},
repository.WithSigner(signingEntity),
)
} else {
err = extgogit.ErrEmptyCommit
}
var statusMessage strings.Builder
if err != nil {
if !errors.Is(err, git.ErrNoStagedFiles) && !errors.Is(err, extgogit.ErrEmptyCommit) {
return failWithError(err)
}
log.Info("no changes made in working directory; no commit")
statusMessage.WriteString("no updates made")
if auto.Status.LastPushTime != nil && len(auto.Status.LastPushCommit) >= 7 {
statusMessage.WriteString(fmt.Sprintf("; last commit %s at %s",
auto.Status.LastPushCommit[:7], auto.Status.LastPushTime.Format(time.RFC3339)))
}
} else {
// Use the git operations timeout for the repo.
pushCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration)
defer cancel()
var pushConfig repository.PushConfig
if gitSpec.Push != nil {
pushConfig.Options = gitSpec.Push.Options
}
if pushBranch != "" {
// If the force push feature flag is true and we are pushing to a
// different branch than the one we checked out to, then force push
// these changes.
forcePush := r.features[features.GitForcePushBranch]
if forcePush && switchBranch {
pushConfig.Force = true
}
if err := gitClient.Push(pushCtx, pushConfig); err != nil {
return failWithError(err)
}
log.Info("pushed commit to origin", "revision", rev, "branch", pushBranch)
statusMessage.WriteString(fmt.Sprintf("committed and pushed commit '%s' to branch '%s'", rev, pushBranch))
}
if gitSpec.Push != nil && gitSpec.Push.Refspec != "" {
pushConfig.Refspecs = []string{gitSpec.Push.Refspec}
if err := gitClient.Push(pushCtx, pushConfig); err != nil {
return failWithError(err)
}
log.Info("pushed commit to origin", "revision", rev, "refspec", gitSpec.Push.Refspec)
if statusMessage.Len() > 0 {
statusMessage.WriteString(fmt.Sprintf(" and using refspec '%s'", gitSpec.Push.Refspec))
} else {
statusMessage.WriteString(fmt.Sprintf("committed and pushed commit '%s' using refspec '%s'", rev, gitSpec.Push.Refspec))
}
}
r.event(ctx, auto, eventv1.EventSeverityInfo, fmt.Sprintf("%s\n%s", statusMessage.String(), message))
auto.Status.LastPushCommit = rev
auto.Status.LastPushTime = &metav1.Time{Time: start}
}
// Getting to here is a successful run.
auto.Status.LastAutomationRunTime = &metav1.Time{Time: start}
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionTrue, imagev1.ReconciliationSucceededReason, statusMessage.String())
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
return ctrl.Result{Requeue: true}, err
}
// We're either in this method because something changed, or this
// object got requeued. Either way, once successful, we don't need
// to see the object again until Interval has passed, or something
// changes again.
interval := intervalOrDefault(&auto)
return ctrl.Result{RequeueAfter: interval}, nil
}
func (r *ImageUpdateAutomationReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts ImageUpdateAutomationReconcilerOptions) error {
// Index the git repository object that each I-U-A refers to
if err := mgr.GetFieldIndexer().IndexField(ctx, &imagev1.ImageUpdateAutomation{}, repoRefKey, func(obj client.Object) []string {
updater := obj.(*imagev1.ImageUpdateAutomation)
ref := updater.Spec.SourceRef
return []string{ref.Name}
}); err != nil {
return err
}
if r.features == nil {
r.features = features.FeatureGates()
}
return ctrl.NewControllerManagedBy(mgr).
For(&imagev1.ImageUpdateAutomation{}, builder.WithPredicates(
predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}))).
Watches(&sourcev1.GitRepository{}, handler.EnqueueRequestsFromMapFunc(r.automationsForGitRepo)).
Watches(&imagev1_reflect.ImagePolicy{}, handler.EnqueueRequestsFromMapFunc(r.automationsForImagePolicy)).
WithOptions(controller.Options{
RateLimiter: opts.RateLimiter,
}).
Complete(r)
}
func (r *ImageUpdateAutomationReconciler) patchStatus(ctx context.Context,
req ctrl.Request,
newStatus imagev1.ImageUpdateAutomationStatus) error {
var auto imagev1.ImageUpdateAutomation
if err := r.Get(ctx, req.NamespacedName, &auto); err != nil {
return err
}
patch := client.MergeFrom(auto.DeepCopy())
auto.Status = newStatus
return r.Status().Patch(ctx, &auto, patch)
}
// intervalOrDefault gives the interval specified, or if missing, the default
func intervalOrDefault(auto *imagev1.ImageUpdateAutomation) time.Duration {
if auto.Spec.Interval.Duration < time.Second {
return time.Second
}
return auto.Spec.Interval.Duration
}
func (r *ImageUpdateAutomationReconciler) getGitClientOpts(gitTransport git.TransportType, proxyOpts *transport.ProxyOptions,
diffPushBranch bool) []gogit.ClientOption {
clientOpts := []gogit.ClientOption{gogit.WithDiskStorage()}
if gitTransport == git.HTTP {
clientOpts = append(clientOpts, gogit.WithInsecureCredentialsOverHTTP())
}
if proxyOpts != nil {
clientOpts = append(clientOpts, gogit.WithProxy(*proxyOpts))
}
// If the push branch is different from the checkout ref, we need to
// have all the references downloaded at clone time, to ensure that
// SwitchBranch will have access to the target branch state. fluxcd/flux2#3384
//
// To always overwrite the push branch, the feature gate
// GitAllBranchReferences can be set to false, which will cause
// the SwitchBranch operation to ignore the remote branch state.
allReferences := r.features[features.GitAllBranchReferences]
if diffPushBranch {
clientOpts = append(clientOpts, gogit.WithSingleBranch(!allReferences))
}
return clientOpts
}
// automationsForGitRepo fetches all the automations that refer to a
// particular source.GitRepository object.
func (r *ImageUpdateAutomationReconciler) automationsForGitRepo(ctx context.Context, obj client.Object) []reconcile.Request {
var autoList imagev1.ImageUpdateAutomationList
if err := r.List(ctx, &autoList, client.InNamespace(obj.GetNamespace()),
client.MatchingFields{repoRefKey: obj.GetName()}); err != nil {
ctrl.LoggerFrom(ctx).Error(err, "failed to list ImageUpdateAutomations for GitRepository change")
return nil
}
reqs := make([]reconcile.Request, len(autoList.Items), len(autoList.Items))
for i := range autoList.Items {
reqs[i].NamespacedName.Name = autoList.Items[i].GetName()
reqs[i].NamespacedName.Namespace = autoList.Items[i].GetNamespace()
}
return reqs
}
// automationsForImagePolicy fetches all the automation objects that
// might depend on a image policy object. Since the link is via
// markers in the git repo, _any_ automation object in the same
// namespace could be affected.
func (r *ImageUpdateAutomationReconciler) automationsForImagePolicy(ctx context.Context, obj client.Object) []reconcile.Request {
var autoList imagev1.ImageUpdateAutomationList
if err := r.List(ctx, &autoList, client.InNamespace(obj.GetNamespace())); err != nil {
ctrl.LoggerFrom(ctx).Error(err, "failed to list ImageUpdateAutomations for ImagePolicy change")
return nil
}
reqs := make([]reconcile.Request, len(autoList.Items), len(autoList.Items))
for i := range autoList.Items {
reqs[i].NamespacedName.Name = autoList.Items[i].GetName()
reqs[i].NamespacedName.Namespace = autoList.Items[i].GetNamespace()
}
return reqs
}
// getAuthOpts fetches the secret containing the auth options (if specified),
// constructs a git.AuthOptions object using those options along with the provided
// repository's URL and returns it.
func (r *ImageUpdateAutomationReconciler) getAuthOpts(ctx context.Context, repository *sourcev1.GitRepository) (*git.AuthOptions, error) {
var data map[string][]byte
var err error
if repository.Spec.SecretRef != nil {
data, err = r.getSecretData(ctx, repository.Spec.SecretRef.Name, repository.GetNamespace())
if err != nil {
return nil, fmt.Errorf("failed to get auth secret '%s/%s': %w", repository.GetNamespace(), repository.Spec.SecretRef.Name, err)
}
}
u, err := url.Parse(repository.Spec.URL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL '%s': %w", repository.Spec.URL, err)
}
opts, err := git.NewAuthOptions(*u, data)
if err != nil {
return nil, fmt.Errorf("failed to configure authentication options: %w", err)
}
return opts, nil
}
// getProxyOpts fetches the secret containing the proxy settings, constructs a
// transport.ProxyOptions object using those settings and then returns it.
func (r *ImageUpdateAutomationReconciler) getProxyOpts(ctx context.Context, proxySecretName,
proxySecretNamespace string) (*transport.ProxyOptions, error) {
proxyData, err := r.getSecretData(ctx, proxySecretName, proxySecretNamespace)
if err != nil {
return nil, fmt.Errorf("failed to get proxy secret '%s/%s': %w", proxySecretNamespace, proxySecretName, err)
}
address, ok := proxyData["address"]
if !ok {
return nil, fmt.Errorf("invalid proxy secret '%s/%s': key 'address' is missing", proxySecretNamespace, proxySecretName)
}
proxyOpts := &transport.ProxyOptions{
URL: string(address),
Username: string(proxyData["username"]),
Password: string(proxyData["password"]),
}
return proxyOpts, nil
}
func (r *ImageUpdateAutomationReconciler) getSecretData(ctx context.Context, name, namespace string) (map[string][]byte, error) {
key := types.NamespacedName{
Namespace: namespace,
Name: name,
}
var secret corev1.Secret
if err := r.Client.Get(ctx, key, &secret); err != nil {
return nil, err
}
return secret.Data, nil
}
// getSigningEntity retrieves an OpenPGP entity referenced by the
// provided imagev1.ImageUpdateAutomation for git commit signing
func (r *ImageUpdateAutomationReconciler) getSigningEntity(ctx context.Context, auto imagev1.ImageUpdateAutomation) (*openpgp.Entity, error) {
// get kubernetes secret
secretName := types.NamespacedName{
Namespace: auto.GetNamespace(),
Name: auto.Spec.GitSpec.Commit.SigningKey.SecretRef.Name,
}
var secret corev1.Secret
if err := r.Get(ctx, secretName, &secret); err != nil {
return nil, fmt.Errorf("could not find signing key secret '%s': %w", secretName, err)
}
// get data from secret
data, ok := secret.Data[signingSecretKey]
if !ok {
return nil, fmt.Errorf("signing key secret '%s' does not contain a 'git.asc' key", secretName)
}
// read entity from secret value
entities, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("could not read signing key from secret '%s': %w", secretName, err)
}
if len(entities) > 1 {
return nil, fmt.Errorf("multiple entities read from secret '%s', could not determine which signing key to use", secretName)
}
entity := entities[0]
if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
passphrase, ok := secret.Data[signingPassphraseKey]
if !ok {
return nil, fmt.Errorf("can not use passphrase protected signing key without '%s' field present in secret %s",
signingPassphraseKey, secretName)
}
if err = entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
return nil, fmt.Errorf("could not decrypt private key of the signing key present in secret %s: %w", secretName, err)
}
}
return entity, nil
}
// --- events, metrics
func (r *ImageUpdateAutomationReconciler) event(ctx context.Context, auto imagev1.ImageUpdateAutomation, severity, msg string) {
eventtype := "Normal"
if severity == eventv1.EventSeverityError {
eventtype = "Warning"
}
r.EventRecorder.Eventf(&auto, eventtype, severity, msg)
}
// --- updates
// updateAccordingToSetters updates files under the root by treating
// the given image policies as kyaml setters.
func updateAccordingToSetters(ctx context.Context, tracelog logr.Logger, inpath, outpath string, policies []imagev1_reflect.ImagePolicy) (update.Result, error) {
return update.UpdateWithSetters(tracelog, inpath, outpath, policies)
}
// templateMsg renders a msg template, returning the message or an error.
func templateMsg(messageTemplate string, templateValues *TemplateData) (string, error) {
if messageTemplate == "" {
messageTemplate = defaultMessageTemplate
}
// Includes only functions that are guaranteed to always evaluate to the same result for given input.
// This removes the possibility of accidentally relying on where or when the template runs.
// https://github.com/Masterminds/sprig/blob/3ac42c7bc5e4be6aa534e036fb19dde4a996da2e/functions.go#L70
t, err := template.New("commit message").Funcs(sprig.HermeticTxtFuncMap()).Parse(messageTemplate)
if err != nil {
return "", fmt.Errorf("unable to create commit message template from spec: %w", err)
}
b := &strings.Builder{}
if err := t.Execute(b, *templateValues); err != nil {
return "", fmt.Errorf("failed to run template from spec: %w", err)
}
return b.String(), nil
}