Skip to content

Commit 750214c

Browse files
authored
Merge pull request #488 from oasisprotocol/andrej/feature/rofl-build-check-img-sizes
cmd/rofl/build: Validate image platform and size
2 parents 4039850 + 8870d9a commit 750214c

1 file changed

Lines changed: 86 additions & 2 deletions

File tree

cmd/rofl/build/container.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package build
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"strings"
78

89
"golang.org/x/net/idna"
910

10-
"github.com/compose-spec/compose-go/v2/cli"
11+
compose "github.com/compose-spec/compose-go/v2/cli"
12+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
13+
"oras.land/oras-go/v2"
14+
"oras.land/oras-go/v2/registry/remote"
1115

1216
"github.com/oasisprotocol/oasis-core/go/runtime/bundle"
1317

@@ -32,7 +36,7 @@ func tdxBuildContainer(
3236

3337
// Validate compose file.
3438
fmt.Println("Validating compose file...")
35-
options, err := cli.NewProjectOptions([]string{artifacts[artifactContainerCompose]})
39+
options, err := compose.NewProjectOptions([]string{artifacts[artifactContainerCompose]})
3640
if err != nil {
3741
return fmt.Errorf("failed to set up compose options: %w", err)
3842
}
@@ -42,10 +46,15 @@ func tdxBuildContainer(
4246
return fmt.Errorf("compose file validation failed")
4347
}
4448

49+
// Keep track of all images encountered, as we will need them in later steps.
50+
images := []string{}
51+
4552
// Make sure that the image fields for all services contain a FQDN or it will cause
4653
// Podman errors when trying to run it.
4754
for serviceName, service := range proj.Services {
4855
image := service.Image
56+
images = append(images, image)
57+
4958
validationFailedErr := fmt.Errorf("compose file validation failed: image '%s' of service '%s' is not a fully-qualified domain name", image, serviceName)
5059

5160
if !strings.Contains(image, "/") {
@@ -67,6 +76,81 @@ func tdxBuildContainer(
6776
}
6877
}
6978

79+
// Make sure that we have images.
80+
if len(images) == 0 {
81+
return fmt.Errorf("compose file validation failed: no images defined")
82+
}
83+
84+
// Make sure that the total size of images fits into the storage size
85+
// and perform other minor validations.
86+
//
87+
// Note: This checks the compressed image size only, because that is the only thing
88+
// available in the OCI manifest. To get the uncompressed size, we would need to
89+
// download all the layers and decompress them locally.
90+
maxSize := manifest.Resources.Storage.Size * 1024 * 1024
91+
var totalSize uint64
92+
for _, imageFull := range images {
93+
image := imageFull
94+
tag := "latest"
95+
96+
// Extract tag if present.
97+
digestBits := strings.Split(imageFull, "@")
98+
if len(digestBits) == 2 {
99+
// image@sha256:...
100+
image = digestBits[0]
101+
tag = digestBits[1]
102+
} else {
103+
// image:tag
104+
bits := strings.Split(imageFull, ":")
105+
if len(bits) > 1 {
106+
image = strings.Join(bits[:len(bits)-1], ":")
107+
tag = bits[len(bits)-1]
108+
}
109+
}
110+
111+
// Fetch manifest from OCI repository.
112+
repo, err := remote.NewRepository(image)
113+
if err != nil {
114+
return fmt.Errorf("compose file validation failed: %w", err)
115+
}
116+
mainDescriptor, mfRaw, err := oras.FetchBytes(context.Background(), repo, tag, oras.DefaultFetchBytesOptions)
117+
if err != nil {
118+
return fmt.Errorf("compose file validation failed: unable to fetch manifest for image '%s': %w", imageFull, err)
119+
}
120+
var mf ocispec.Manifest
121+
if err = json.Unmarshal(mfRaw, &mf); err != nil {
122+
return fmt.Errorf("compose file validation failed: unable to parse manifest for image '%s': %w", imageFull, err)
123+
}
124+
125+
// Validate platform if given.
126+
for _, platform := range []*ocispec.Platform{mainDescriptor.Platform, mf.Config.Platform} {
127+
if platform != nil {
128+
if platform.Architecture != "amd64" || platform.OS != "linux" {
129+
return fmt.Errorf("compose file validation failed: image '%s' has incorrect platform (expected linux/amd64, got %s/%s)", imageFull, platform.OS, platform.Architecture)
130+
}
131+
}
132+
}
133+
134+
// Add sizes for all layers.
135+
var imageSize uint64
136+
for _, layer := range mf.Layers {
137+
if layer.Size > 0 {
138+
imageSize += uint64(layer.Size)
139+
}
140+
}
141+
totalSize += imageSize
142+
}
143+
144+
// Since we calculate the compressed size only, multiply by a fudge factor to bring
145+
// it closer to the actual size.
146+
totalSize *= 2
147+
148+
// We could terminate early above, but it's more useful to give the user an estimate
149+
// of how big the storage should be.
150+
if totalSize > maxSize {
151+
return fmt.Errorf("compose file validation failed: estimated total size of images (%d MB) exceeds storage size set in ROFL manifest (%d MB)", totalSize/1024/1024, manifest.Resources.Storage.Size)
152+
}
153+
70154
// Use the pre-built container runtime.
71155
initPath := artifacts[artifactContainerRuntime]
72156

0 commit comments

Comments
 (0)