Initial Commit
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2022-01-26 11:15:28 +01:00
commit b82578fbf0
19 changed files with 2558 additions and 0 deletions

142
pkg/config/config.go Normal file
View File

@@ -0,0 +1,142 @@
package config
import (
"encoding/json"
"io"
"os"
"path/filepath"
"git.cryptic.systems/volker.raschek/civ/pkg/domain"
"gopkg.in/yaml.v2"
)
type FileReader interface {
ReadFile() (*domain.Config, error)
}
func NewFileReader(name string) FileReader {
switch filepath.Ext(name) {
case ".json":
return &JSONReader{
name: name,
}
default:
return &YAMLReader{
name: name,
}
}
}
type FileWriter interface {
WriteFile(config *domain.Config) error
}
func NewFileWriter(name string) FileWriter {
switch filepath.Ext(name) {
case ".json":
return &JSONWriter{
name: name,
}
default:
return &YAMLWriter{
name: name,
}
}
}
type JSONReader struct {
name string
}
func (jr *JSONReader) read(r io.Reader) (*domain.Config, error) {
config := new(domain.Config)
jsonDecoder := json.NewDecoder(r)
err := jsonDecoder.Decode(config)
if err != nil {
return nil, err
}
return config, nil
}
func (jr *JSONReader) ReadFile() (*domain.Config, error) {
f, err := os.Open(jr.name)
if err != nil {
return nil, err
}
defer f.Close()
return jr.read(f)
}
type JSONWriter struct {
name string
}
func (jw *JSONWriter) WriteFile(config *domain.Config) error {
f, err := os.Create(jw.name)
if err != nil {
return err
}
defer func() { f.Close() }()
return jw.write(f, config)
}
func (jw *JSONWriter) write(w io.Writer, config *domain.Config) error {
jsonEncoder := json.NewEncoder(w)
jsonEncoder.SetIndent("", " ")
err := jsonEncoder.Encode(config)
if err != nil {
return err
}
return nil
}
type YAMLReader struct {
name string
}
func (yr *YAMLReader) ReadFile() (*domain.Config, error) {
f, err := os.Open(yr.name)
if err != nil {
return nil, err
}
defer f.Close()
return yr.read(f)
}
func (yr *YAMLReader) read(r io.Reader) (*domain.Config, error) {
config := new(domain.Config)
yamlDecoder := yaml.NewDecoder(r)
err := yamlDecoder.Decode(config)
if err != nil {
return nil, err
}
return config, nil
}
type YAMLWriter struct {
name string
}
func (yw *YAMLWriter) WriteFile(config *domain.Config) error {
f, err := os.Create(yw.name)
if err != nil {
return err
}
defer func() { f.Close() }()
return yw.write(f, config)
}
func (yw *YAMLWriter) write(w io.Writer, config *domain.Config) error {
err := yaml.NewEncoder(w).Encode(config)
if err != nil {
return err
}
return nil
}

34
pkg/docker/docker.go Normal file
View File

@@ -0,0 +1,34 @@
package docker
import (
"context"
"log"
"git.cryptic.systems/volker.raschek/dockerutils"
)
type Runtime struct {
dockerClient *dockerutils.Client
}
func (r *Runtime) GetImageLabels(ctx context.Context, image string) (map[string]string, error) {
log.Printf("Pull image: %v", image)
if err := r.dockerClient.PullQuiet(ctx, image); err != nil {
return nil, err
}
log.Printf("Image successfully pulled: %v", image)
imageSpec, _, err := r.dockerClient.ImageInspectWithRaw(ctx, image)
if err != nil {
return nil, err
}
return imageSpec.Config.Labels, nil
}
func NewRuntime(dockerClient *dockerutils.Client) (*Runtime, error) {
return &Runtime{
dockerClient: dockerClient,
}, nil
}

69
pkg/domain/constraint.go Normal file
View File

@@ -0,0 +1,69 @@
package domain
type Config struct {
Images map[string]Image `yaml:"images"`
}
type Image struct {
LabelConstraints map[string]*LabelConstraint `yaml:"labelConstraints,omitempty"`
}
type LabelConstraint struct {
CompareSemver *LabelConstraintCompareSemver `yaml:"compareSemver,omitempty"`
CompareSemverResult *bool `yaml:"compareSemverResult,omitempty"`
CompareSemverResultMessage string `yaml:"compareSemverResultMessage,omitempty"`
CompareString *LabelConstraintCompareString `yaml:"compareString,omitempty"`
CompareStringResult *bool `yaml:"compareStringResult,omitempty"`
CompareStringResultMessage bool `yaml:"compareStringResultMessage,omitempty"`
Count *LabelConstraintCounter `yaml:"count,omitempty"`
CountResult *bool `yaml:"countResult,omitempty"`
CountResultMessage string `yaml:"countMessage,omitempty"`
Exists *bool `yaml:"exists,omitempty"`
ExistsResult bool `yaml:"existsResult,omitempty"`
ExistsResultMessage string `yaml:"existsResultMessage,omitempty"`
}
type LabelConstraintCompareSemver struct {
Equal string `yaml:"equal,omitempty"`
EqualResult *bool `yaml:"equalResult,omitempty"`
EqualResultMessage string `yaml:"equalResultMessage,omitempty"`
GreaterThan string `yaml:"greaterThan,omitempty"`
GreaterThanResult *bool `yaml:"greaterThanResult,omitempty"`
GreaterThanResultMessage string `yaml:"greaterThanResultMessage,omitempty"`
LessThan string `yaml:"lessThan,omitempty"`
LessThanResult *bool `yaml:"lessThanResult,omitempty"`
LessThanResultMessage string `yaml:"lessThanResultMessage,omitempty"`
}
type LabelConstraintCompareString struct {
Equal string `yaml:"equal,omitempty"`
EqualResult *bool `yaml:"equalResult,omitempty"`
EqualResultMessage string `yaml:"equalResultMessage,omitempty"`
HasPrefix string `yaml:"hasPrefix,omitempty"`
HasPrefixResult *bool `yaml:"hasPrefixResult,omitempty"`
HasPrefixResultMessage string `yaml:"hasPrefixResultMessage,omitempty"`
HasSuffix string `yaml:"hasSuffix,omitempty"`
HasSuffixResult *bool `yaml:"hasSuffixResult,omitempty"`
HasSuffixResultMessage string `yaml:"hasSuffixResultMessage,omitempty"`
}
type LabelConstraintCounter struct {
Equal *uint `yaml:"equal,omitempty"`
EqualResult *bool `yaml:"equalResult,omitempty"`
EqualResultMessage string `yaml:"equalResultMessage,omitempty"`
GreaterThan *uint `yaml:"greaterThan,omitempty"`
GreaterThanResult *bool `yaml:"greaterThanResult,omitempty"`
GreaterThanResultMessage string `yaml:"greaterThanResultMessage,omitempty"`
LessThan *uint `yaml:"lessThan,omitempty"`
LessThanResult *bool `yaml:"lessThanResult,omitempty"`
LessThanResultMessage string `yaml:"lessThanResultMessage,omitempty"`
}

View File

@@ -0,0 +1,352 @@
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),
}
}