You've already forked civ
This commit is contained in:
142
pkg/config/config.go
Normal file
142
pkg/config/config.go
Normal 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
34
pkg/docker/docker.go
Normal 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
69
pkg/domain/constraint.go
Normal 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"`
|
||||
}
|
||||
352
pkg/usecases/verifyLabels.go
Normal file
352
pkg/usecases/verifyLabels.go
Normal 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),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user