You've already forked civ
							
							
		
			All checks were successful
		
		
	
	Golang Tests / unittest (stable, ubuntu-latest-amd64) (push) Successful in 8s
				
			Lint Markdown files / markdown-lint (push) Successful in 4s
				
			Golang CI lint / golangci (stable, ubuntu-latest-amd64) (push) Successful in 2m31s
				
			Golang Tests / unittest (stable, ubuntu-latest-arm64) (push) Successful in 45s
				
			Golang CI lint / golangci (stable, ubuntu-latest-arm64) (push) Successful in 1m50s
				
			
		
			
				
	
	
		
			353 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			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 = 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 = 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),
 | |
| 	}
 | |
| }
 |