civ/pkg/usecases/verifyLabels.go

353 lines
10 KiB
Go

package usecases
import (
"context"
"fmt"
"regexp"
"strings"
"sync"
"git.cryptic.systems/volker.raschek/civ/pkg/domain"
"github.com/Masterminds/semver/v3"
)
// ContainerRuntime is an interface for different container runtimes to return labels
// based on their full qualified container image name. For example:
//
// imageLabels, err := Load(ctx, "docker.io/library/alpine:latest")
// imageLabels, err := Load(ctx, "docker.io/library/busybox:latest")
type ContainerRuntime interface {
GetImageLabels(ctx context.Context, name string) (map[string]string, error)
}
type ConfigLoader interface {
Load(ctx context.Context) (*domain.Config, error)
}
type LabelVerifier struct {
config *domain.Config
labelLoader ContainerRuntime
labelStore *labelStore
}
// Run start the verification process based on the passed config.
func (lv *LabelVerifier) Run(ctx context.Context) error {
if err := lv.fillLabelStore(ctx); err != nil {
return err
}
wg := new(sync.WaitGroup)
wg.Add(len(lv.config.Images))
for image := range lv.config.Images {
go func(image string) {
defer func() { wg.Done() }()
lv.runLabelConstraints(image)
}(image)
}
wg.Wait()
return nil
}
func (lv *LabelVerifier) runLabelConstraints(image string) {
for labelKey, labelConstraint := range lv.config.Images[image].LabelConstraints {
// fetch existing labels from store
existingLabels := lv.labelStore.GetLabelsForImage(image)
switch {
case strings.HasPrefix(labelKey, "%") && strings.HasSuffix(labelKey, "%"):
m := strings.TrimPrefix(strings.TrimSuffix(labelKey, "%"), "%")
re, err := regexp.Compile(fmt.Sprintf("^.*%v.*$", m))
if err != nil {
labelConstraint.CountResultMessage = err.Error()
} else {
state := labelCount(re, labelConstraint.Count, existingLabels)
labelConstraint.CountResult = &state
}
case strings.HasPrefix(labelKey, "%"):
m := strings.TrimPrefix(labelKey, "%")
re, err := regexp.Compile(fmt.Sprintf("^.*%v$", m))
if err != nil {
labelConstraint.CountResultMessage = err.Error()
} else {
state := labelCount(re, labelConstraint.Count, existingLabels)
labelConstraint.CountResult = &state
}
case strings.HasSuffix(labelKey, "%"):
m := strings.TrimSuffix(labelKey, "%")
re, err := regexp.Compile(fmt.Sprintf("^%v.*$", m))
if err != nil {
labelConstraint.CountResultMessage = err.Error()
} else {
state := labelCount(re, labelConstraint.Count, existingLabels)
labelConstraint.CountResult = &state
}
default:
// labelExists
if labelConstraint.Exists != nil {
state := lv.labelExists(labelKey, existingLabels)
labelConstraint.ExistsResult = state
if state {
labelConstraint.ExistsResultMessage = "Label found"
} else {
labelConstraint.ExistsResultMessage = "Label not found"
}
}
// labelCompareSemver
if labelConstraint.CompareSemver != nil {
labelExistState := lv.labelExists(labelKey, existingLabels)
if labelExistState {
parsedSemVer, err := semver.NewVersion(existingLabels[labelKey])
if err != nil {
b := false
labelConstraint.CompareSemverResult = &b
labelConstraint.CompareSemverResultMessage = err.Error()
}
state := labelCompareSemver(labelConstraint.CompareSemver, parsedSemVer)
labelConstraint.CompareSemverResult = &state
} else {
labelConstraint.CompareSemverResult = &labelExistState
labelConstraint.CompareSemverResultMessage = "Label found"
}
}
// labelCompareString
if labelConstraint.CompareString != nil {
state := labelCompareString(labelConstraint.CompareString, existingLabels[labelKey])
labelConstraint.CompareStringResult = &state
}
}
}
}
func labelCompareSemver(compareSemver *domain.LabelConstraintCompareSemver, parsedSemVer *semver.Version) bool {
var majorState bool
// Equal
if compareSemver.Equal != "" {
compareSemverEqualVersion, err := semver.NewVersion(compareSemver.Equal)
if err != nil {
compareSemver.EqualResultMessage = err.Error()
} else {
state := parsedSemVer.Equal(compareSemverEqualVersion)
compareSemver.EqualResult = &state
if state {
compareSemver.EqualResultMessage = fmt.Sprintf("Version %s is equal to %s", parsedSemVer.String(), compareSemverEqualVersion.String())
} else {
compareSemver.EqualResultMessage = fmt.Sprintf("Version %s is not equal to %s", parsedSemVer.String(), compareSemverEqualVersion.String())
}
}
}
// GreaterThan
if compareSemver.GreaterThan != "" {
compareSemverGreaterThanVersion, err := semver.NewVersion(compareSemver.GreaterThan)
if err != nil {
compareSemver.GreaterThanResultMessage = err.Error()
} else {
state := parsedSemVer.GreaterThan(compareSemverGreaterThanVersion)
compareSemver.GreaterThanResult = &state
if state {
compareSemver.GreaterThanResultMessage = fmt.Sprintf("Version %s is greater than %s", parsedSemVer.String(), compareSemverGreaterThanVersion.String())
} else {
compareSemver.GreaterThanResultMessage = fmt.Sprintf("Version %s is not greater than %s", parsedSemVer.String(), compareSemverGreaterThanVersion.String())
}
}
}
// LessThan
if compareSemver.LessThan != "" {
compareSemverLessThanVersion, err := semver.NewVersion(compareSemver.LessThan)
if err != nil {
compareSemver.LessThanResultMessage = err.Error()
} else {
state := parsedSemVer.LessThan(compareSemverLessThanVersion)
compareSemver.LessThanResult = &state
if state {
compareSemver.LessThanResultMessage = fmt.Sprintf("Version %s is lower than %s", parsedSemVer.String(), compareSemverLessThanVersion.String())
} else {
compareSemver.LessThanResultMessage = fmt.Sprintf("Version %s is not lower than %s", parsedSemVer.String(), compareSemverLessThanVersion.String())
}
}
}
return majorState
}
func labelCompareString(compareString *domain.LabelConstraintCompareString, labelValue string) bool {
var majorState bool = true
// Equal
if compareString.Equal != "" {
state := compareString.Equal == labelValue
if compareString.Equal == labelValue {
compareString.EqualResult = &state
compareString.EqualResultMessage = fmt.Sprintf("%s and %s are equal", labelValue, compareString.Equal)
} else {
compareString.EqualResult = &state
compareString.EqualResultMessage = fmt.Sprintf("%s and %s are not equal", labelValue, compareString.Equal)
}
}
// hasPrefix
if compareString.HasPrefix != "" {
state := strings.HasPrefix(labelValue, compareString.HasPrefix)
if state {
compareString.HasPrefixResult = &state
compareString.HasPrefixResultMessage = fmt.Sprintf("%s has prefix %s", labelValue, compareString.HasPrefix)
} else {
compareString.HasPrefixResult = &state
compareString.HasPrefixResultMessage = fmt.Sprintf("%s has not prefix %s", labelValue, compareString.HasPrefix)
}
}
// hasSuffix
if compareString.HasSuffix != "" {
state := strings.HasSuffix(labelValue, compareString.HasSuffix)
if state {
compareString.HasSuffixResult = &state
compareString.HasSuffixResultMessage = fmt.Sprintf("%s has suffix %s", labelValue, compareString.HasSuffix)
} else {
compareString.HasSuffixResult = &state
compareString.HasSuffixResultMessage = fmt.Sprintf("%s has not suffix %s", labelValue, compareString.HasSuffix)
}
}
return majorState
}
func labelCount(re *regexp.Regexp, labelConstraintCounter *domain.LabelConstraintCounter, labels map[string]string) bool {
var majorState bool = true
var i uint = 0
for key := range labels {
if re.MatchString(key) {
i++
}
}
switch {
case labelConstraintCounter.Equal != nil:
switch {
case i == *labelConstraintCounter.Equal:
state := true
labelConstraintCounter.EqualResult = &state
case i > *labelConstraintCounter.Equal:
fallthrough
case i < *labelConstraintCounter.Equal:
state := false
labelConstraintCounter.EqualResult = &state
labelConstraintCounter.EqualResultMessage = fmt.Sprintf("%v is not equal %v", i, *labelConstraintCounter.Equal)
majorState = false
}
case labelConstraintCounter.LessThan != nil:
switch {
case i < *labelConstraintCounter.LessThan:
state := true
labelConstraintCounter.LessThanResult = &state
case i >= *labelConstraintCounter.LessThan:
state := false
labelConstraintCounter.LessThanResult = &state
labelConstraintCounter.LessThanResultMessage = fmt.Sprintf("%v is not less than %v", i, *labelConstraintCounter.Equal)
majorState = false
}
case labelConstraintCounter.GreaterThan != nil:
switch {
case i < *labelConstraintCounter.GreaterThan:
state := true
labelConstraintCounter.GreaterThanResult = &state
case i >= *labelConstraintCounter.GreaterThan:
state := false
labelConstraintCounter.GreaterThanResult = &state
labelConstraintCounter.GreaterThanResultMessage = fmt.Sprintf("%v is not greater than %v", i, *labelConstraintCounter.Equal)
majorState = false
}
}
return majorState
}
func (lv *LabelVerifier) labelExists(requiresLabelKey string, existingLabels map[string]string) bool {
for existingLabelKey := range existingLabels {
if existingLabelKey == requiresLabelKey {
return true
}
}
return false
}
// fillLabelStore fills the label store with the labels of the defined images
// from config.
func (lv *LabelVerifier) fillLabelStore(ctx context.Context) error {
wg := new(sync.WaitGroup)
wg.Add(len(lv.config.Images))
errorChannel := make(chan error, len(lv.config.Images))
for image := range lv.config.Images {
go func(image string) {
defer wg.Done()
labels, err := lv.labelLoader.GetImageLabels(ctx, image)
if err != nil {
errorChannel <- err
return
}
lv.labelStore.AddLabelsForImage(image, labels)
}(image)
}
wg.Wait()
close(errorChannel)
for {
err, open := <-errorChannel
if err != nil {
return err
}
if !open {
break
}
}
return nil
}
func NewLabelVerifier(config *domain.Config, labelLoader ContainerRuntime) (*LabelVerifier, error) {
return &LabelVerifier{
config: config,
labelLoader: labelLoader,
labelStore: newLabelStore(),
}, nil
}
type labelStore struct {
labels map[string]map[string]string
mutex *sync.RWMutex
}
func (ls *labelStore) AddLabelsForImage(image string, labels map[string]string) {
ls.mutex.Lock()
defer func() { ls.mutex.Unlock() }()
ls.labels[image] = labels
}
func (ls *labelStore) GetLabelsForImage(image string) map[string]string {
ls.mutex.RLock()
defer func() { ls.mutex.RUnlock() }()
return ls.labels[image]
}
func newLabelStore() *labelStore {
return &labelStore{
labels: make(map[string]map[string]string),
mutex: new(sync.RWMutex),
}
}