blob: c19841ebddc7ffeb2c502c0e006177052370fac7 [file] [log] [blame]
package main
import (
"context"
"fmt"
"os"
"strings"
"sync"
"github.com/containers/image/v5/docker"
"github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/types"
"github.com/goccy/go-yaml"
"github.com/goccy/go-yaml/ast"
"github.com/goccy/go-yaml/parser"
"github.com/opencontainers/go-digest"
log "github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight"
)
var digestGroup singleflight.Group
// GetImageDigest fetches the digest for a given image reference.
func GetImageDigest(ctx context.Context, reference string) (digest.Digest, error) {
ref, err := docker.ParseReference(reference)
if err != nil {
return "", fmt.Errorf("error parsing reference '%s': %w", reference, err)
}
sysCtx := &types.SystemContext{}
imageSource, err := ref.NewImageSource(ctx, sysCtx)
if err != nil {
return "", fmt.Errorf("error creating image source for '%s': %w", reference, err)
}
defer imageSource.Close()
rawManifest, _, err := imageSource.GetManifest(ctx, nil)
if err != nil {
return "", fmt.Errorf("error getting manifest for '%s': %w", reference, err)
}
dgst, err := manifest.Digest(rawManifest)
if err != nil {
return "", fmt.Errorf("error getting digest for '%s': %w", reference, err)
}
return dgst, nil
}
// GetImageNameToPull normalizes the image name by replacing variables and adding necessary prefixes.
func GetImageNameToPull(image string, release string) string {
// Replace Jinja2 variables with actual values
image = strings.ReplaceAll(image, "{{ atmosphere_image_prefix }}", "")
image = strings.ReplaceAll(image, "{{ atmosphere_release }}", release)
// Add mirror if the image is not hosted with us
if !strings.HasPrefix(image, "registry.atmosphere.dev") {
image = fmt.Sprintf("harbor.atmosphere.dev/%s", image)
}
// Switch out of the CDN since we are in CI
if strings.HasPrefix(image, "registry.atmosphere.dev") {
image = strings.ReplaceAll(image, "registry.atmosphere.dev", "harbor.atmosphere.dev")
}
return image
}
// AppendDigestToImage appends the digest to the original image reference.
func AppendDigestToImage(image string, dgst digest.Digest) string {
if strings.Contains(image, "@") {
// Replace existing digest if present
parts := strings.Split(image, "@")
return parts[0] + "@" + dgst.String()
}
// Append digest
return image + "@" + dgst.String()
}
func main() {
varsFilePath := "roles/defaults/vars/main.yml"
file, err := parser.ParseFile(varsFilePath, parser.ParseComments)
if err != nil {
log.WithError(err).Fatal("error parsing yaml file")
}
if len(file.Docs) != 1 {
log.Fatal("expected exactly one yaml document")
}
doc := file.Docs[0]
body := doc.Body.(*ast.MappingNode)
var release string
var images *ast.MappingNode
for _, item := range body.Values {
switch item.Key.(*ast.StringNode).Value {
case "atmosphere_release":
release = item.Value.(*ast.StringNode).Value
case "_atmosphere_images":
images = item.Value.(*ast.MappingNode)
}
}
if release == "" {
log.Fatalf("atmosphere_release not found")
}
if images == nil {
log.Fatalf("_atmosphere_images not found")
}
type imageInfo struct {
Key string
Value string
Normalized string
Digest digest.Digest
}
var imageInfos []imageInfo
uniqueImages := make(map[string][]int)
for i, item := range images.Values {
normalized := GetImageNameToPull(item.Value.(*ast.StringNode).Value, release)
info := imageInfo{
Key: item.Key.(*ast.StringNode).Value,
Value: item.Value.(*ast.StringNode).Value,
Normalized: normalized,
}
imageInfos = append(imageInfos, info)
uniqueImages[normalized] = append(uniqueImages[normalized], i)
}
digestMap := make(map[string]digest.Digest)
var mapMutex sync.Mutex
var wg sync.WaitGroup
for normImg := range uniqueImages {
wg.Add(1)
go func(normImg string) {
defer wg.Done()
result, err, _ := digestGroup.Do(normImg, func() (interface{}, error) {
dgst, err := GetImageDigest(context.TODO(), "//"+normImg)
if err != nil {
return nil, err
}
return dgst, nil
})
if err != nil {
log.WithError(err).WithFields(log.Fields{
"image": normImg,
}).Error("Error fetching digest")
return
}
dgst := result.(digest.Digest)
mapMutex.Lock()
digestMap[normImg] = dgst
mapMutex.Unlock()
log.WithFields(log.Fields{
"image": normImg,
"digest": dgst,
}).Info("Fetched image digest")
}(normImg)
}
wg.Wait()
// Update the image references with digests
for normImg, indices := range uniqueImages {
dgst, exists := digestMap[normImg]
if !exists {
log.WithField("image", normImg).Error("Digest not found, skipping update")
continue
}
for _, idx := range indices {
updatedImage, err := yaml.ValueToNode(AppendDigestToImage(imageInfos[idx].Value, dgst))
if err != nil {
log.WithError(err).Fatal("error converting value to node")
}
images.Values[idx].Value = updatedImage
}
}
if err := os.WriteFile(varsFilePath, []byte(file.String()), 0644); err != nil {
log.WithError(err).Fatal("error writing updated yaml file")
}
log.Info("Successfully updated YAML file with image digests")
}