| 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") |
| } |