keel/provider/kubernetes/approvals.go

147 lines
3.8 KiB
Go

package kubernetes
import (
"fmt"
"strconv"
"time"
"github.com/keel-hq/keel/pkg/store"
"github.com/keel-hq/keel/types"
log "github.com/sirupsen/logrus"
)
func getApprovalIdentifier(resourceIdentifier, version string) string {
return resourceIdentifier + ":" + version
}
// checkForApprovals - filters out deployments and only passes forward approved ones
func (p *Provider) checkForApprovals(event *types.Event, plans []*UpdatePlan) (approvedPlans []*UpdatePlan) {
approvedPlans = []*UpdatePlan{}
for _, plan := range plans {
approved, err := p.isApproved(event, plan)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"name": plan.Resource.Name,
"namespace": plan.Resource.Namespace,
}).Error("provider.kubernetes: failed to check approval status for deployment")
continue
}
if approved {
approvedPlans = append(approvedPlans, plan)
}
}
return approvedPlans
}
// updateComplete is called after we successfully update resource
func (p *Provider) updateComplete(plan *UpdatePlan) error {
return p.approvalManager.Archive(getApprovalIdentifier(plan.Resource.Identifier, plan.NewVersion))
}
func getInt(key string, labels map[string]string, annotations map[string]string) (int, error) {
var (
valStr string
ok bool
)
valStr, ok = labels[key]
if ok {
valInt, err := strconv.Atoi(valStr)
if err != nil {
return 0, err
}
return valInt, nil
}
valStr, ok = annotations[key]
if ok {
valInt, err := strconv.Atoi(valStr)
if err != nil {
return 0, err
}
return valInt, nil
}
return 0, nil
}
func (p *Provider) isApproved(event *types.Event, plan *UpdatePlan) (bool, error) {
minApprovals, err := getInt(types.KeelMinimumApprovalsLabel, plan.Resource.GetLabels(), plan.Resource.GetAnnotations())
if err != nil {
return false, err
}
if minApprovals == 0 {
return true, nil
}
// deadline
deadline := types.KeelApprovalDeadlineDefault
d, err := getInt(types.KeelApprovalDeadlineLabel, plan.Resource.GetLabels(), plan.Resource.GetAnnotations())
if err != nil {
log.WithFields(log.Fields{
"error": err,
"resource": plan.Resource.GetName(),
}).Warn("failed to parse approvals deadline, using default value")
} else if d != 0 {
deadline = d
}
identifier := getApprovalIdentifier(plan.Resource.Identifier, plan.NewVersion)
// checking for existing approval
existing, err := p.approvalManager.Get(identifier)
if err != nil {
if err == store.ErrRecordNotFound {
// if approval doesn't exist and trigger wasn't existing approval fulfillment -
// create a new one, otherwise if several deployments rely on the same image, it would just be
// requesting approvals in a loop
if event.TriggerName == types.TriggerTypeApproval.String() {
return false, nil
}
// creating new one
approval := &types.Approval{
Provider: types.ProviderTypeKubernetes,
Identifier: identifier,
Event: event,
CurrentVersion: plan.CurrentVersion,
NewVersion: plan.NewVersion,
VotesRequired: minApprovals,
VotesReceived: 0,
Rejected: false,
Deadline: time.Now().Add(time.Duration(deadline) * time.Hour),
}
approval.Message = fmt.Sprintf("New image is available for resource %s/%s (%s).",
plan.Resource.Namespace,
plan.Resource.Name,
approval.Delta(),
)
return false, p.approvalManager.Create(approval)
}
return false, err
}
// if event.Repository.Digest != "" && event.Repository.Digest != existing.Digest {
// err = p.approvalManager.Reset(existing)
// if err != nil {
// return false, fmt.Errorf("failed to reset approval after changed digest, error %s", err)
// }
// return false, nil
// }
// log.WithFields(log.Fields{
// "previous": existing.Digest,
// "new": event.Repository.Digest,
// }).Info("digests match")
return existing.Status() == types.ApprovalStatusApproved, nil
}