crazy-max_diun/internal/app/job.go

244 lines
6.2 KiB
Go

package app
import (
"fmt"
"regexp"
"github.com/containers/image/v5/pkg/docker/config"
"github.com/containers/image/v5/types"
"github.com/crazy-max/diun/v4/internal/model"
"github.com/crazy-max/diun/v4/pkg/registry"
"github.com/crazy-max/diun/v4/pkg/utl"
"github.com/imdario/mergo"
"github.com/rs/zerolog/log"
)
func (di *Diun) createJob(job model.Job) {
var err error
var prvImage registry.Image
sublog := log.With().
Str("provider", job.Provider).
Str("image", job.Image.Name).
Logger()
// Parse image
prvImage, err = registry.ParseImage(registry.ParseImageOptions{
Name: job.Image.Name,
HubTpl: job.Image.HubTpl,
})
if err != nil {
sublog.Error().Err(err).Msg("Cannot parse image")
return
}
job.RegImage = prvImage
job.HubLinkOverride = job.Image.HubLink
// First check?
job.FirstCheck, err = di.db.First(job.RegImage)
if err != nil {
sublog.Error().Err(err).Msg("Cannot check first")
return
}
// Get registry options
reg, err := di.cfg.RegOpts.Select(job.Image.RegOpt, job.RegImage)
if err != nil {
sublog.Warn().Err(err).Msg("Registry options")
} else if reg != nil {
sublog.Debug().Str("regopt", reg.Name).Msg("Registry options will be used")
} else {
reg = (&model.RegOpt{}).GetDefaults()
}
regUser, err := utl.GetSecret(reg.Username, reg.UsernameFile)
if err != nil {
log.Warn().Err(err).Msgf("Cannot retrieve username secret for regopts %s", reg.Name)
}
regPassword, err := utl.GetSecret(reg.Password, reg.PasswordFile)
if err != nil {
log.Warn().Err(err).Msgf("Cannot retrieve password secret for regopts %s", reg.Name)
}
// Set defaults
if err := mergo.Merge(&job.Image, model.Image{
Platform: model.ImagePlatform{},
WatchRepo: utl.NewFalse(),
MaxTags: 0,
}); err != nil {
sublog.Error().Err(err).Msg("Cannot set default values")
return
}
// Validate include/exclude tags
for _, includeTag := range job.Image.IncludeTags {
if _, err := regexp.Compile(includeTag); err != nil {
sublog.Error().Err(err).Msg("Include tag regex '%s' cannot compile")
return
}
}
for _, excludeTag := range job.Image.ExcludeTags {
if _, err := regexp.Compile(excludeTag); err != nil {
sublog.Error().Err(err).Msg("Exclude tag regex '%s' cannot compile")
return
}
}
var auth types.DockerAuthConfig
if len(regUser) > 0 {
auth = types.DockerAuthConfig{
Username: regUser,
Password: regPassword,
}
} else {
auth, err = config.GetCredentials(nil, job.RegImage.Domain)
if err != nil {
sublog.Warn().Err(err).Msg("Error seeking Docker credentials")
}
}
job.Registry, err = registry.New(registry.Options{
Auth: auth,
Timeout: *reg.Timeout,
InsecureTLS: *reg.InsecureTLS,
UserAgent: di.meta.UserAgent,
CompareDigest: *di.cfg.Watch.CompareDigest,
ImageOs: job.Image.Platform.OS,
ImageArch: job.Image.Platform.Arch,
ImageVariant: job.Image.Platform.Variant,
})
if err != nil {
sublog.Error().Err(err).Msg("Cannot create registry client")
return
}
di.wg.Add(1)
err = di.pool.Invoke(job)
if err != nil {
sublog.Error().Err(err).Msgf("Invoking job")
}
if !*job.Image.WatchRepo || len(job.RegImage.Domain) == 0 {
return
}
tags, err := job.Registry.Tags(registry.TagsOptions{
Image: job.RegImage,
Max: job.Image.MaxTags,
Sort: job.Image.SortTags,
Include: job.Image.IncludeTags,
Exclude: job.Image.ExcludeTags,
})
if err != nil {
sublog.Error().Err(err).Msg("Cannot list tags from registry")
return
}
log.Debug().Str("image", job.RegImage.String()).Msgf("%d tag(s) found in repository. %d will be analyzed (%d max, %d not included, %d excluded).",
tags.Total,
len(tags.List),
job.Image.MaxTags,
tags.NotIncluded,
tags.Excluded,
)
for _, tag := range tags.List {
if prvImage.Tag == tag {
continue
}
job.Image.Name = fmt.Sprintf("%s/%s:%s", job.RegImage.Domain, job.RegImage.Path, tag)
job.RegImage, err = registry.ParseImage(registry.ParseImageOptions{
Name: job.Image.Name,
HubTpl: job.Image.HubTpl,
})
if err != nil {
sublog.Error().Err(err).Msg("Cannot parse image (tag)")
continue
}
di.wg.Add(1)
err = di.pool.Invoke(job)
if err != nil {
sublog.Error().Err(err).Msgf("Invoking job (tag)")
}
}
}
func (di *Diun) runJob(job model.Job) (entry model.NotifEntry) {
var err error
entry = model.NotifEntry{
Status: model.ImageStatusError,
Provider: job.Provider,
Image: job.RegImage,
Metadata: job.Image.Metadata,
}
sublog := log.With().
Str("provider", job.Provider).
Str("image", job.RegImage.String()).
Logger()
if !utl.IsIncluded(job.RegImage.Tag, job.Image.IncludeTags) {
entry.Status = model.ImageStatusSkip
sublog.Debug().Msg("Tag not included")
return
} else if utl.IsExcluded(job.RegImage.Tag, job.Image.ExcludeTags) {
entry.Status = model.ImageStatusSkip
sublog.Debug().Msg("Tag excluded")
return
}
dbManifest, err := di.db.GetManifest(job.RegImage)
if err != nil {
sublog.Error().Err(err).Msg("Cannot get manifest from db")
return
}
var updated bool
entry.Manifest, updated, err = job.Registry.Manifest(job.RegImage, dbManifest)
if err != nil {
sublog.Warn().Err(err).Msg("Cannot get remote manifest")
return
}
if v, ok := entry.Manifest.Labels["org.opencontainers.image.url"]; ok {
entry.Image.HubLink = v
}
if job.HubLinkOverride != "" {
entry.Image.HubLink = job.HubLinkOverride
}
if len(dbManifest.Name) == 0 {
entry.Status = model.ImageStatusNew
sublog.Info().Msg("New image found")
} else if updated {
entry.Status = model.ImageStatusUpdate
sublog.Info().Msg("Image update found")
} else {
entry.Status = model.ImageStatusUnchange
sublog.Debug().Msg("No changes")
}
if err := di.db.PutManifest(job.RegImage, entry.Manifest); err != nil {
sublog.Error().Err(err).Msg("Cannot write manifest to db")
return
}
sublog.Debug().Msg("Manifest saved to database")
if entry.Status == model.ImageStatusUnchange {
return
}
if job.FirstCheck && !*di.cfg.Watch.FirstCheckNotif {
sublog.Debug().Msg("Skipping notification (first check)")
return
}
notifyOn := model.NotifyOn(entry.Status)
if !notifyOn.OneOf(job.Image.NotifyOn) {
sublog.Debug().Msgf("Skipping notification (%s not part of specified notify status)", entry.Status)
return
}
di.notif.Send(entry)
return
}