@@ -2,12 +2,16 @@ package build
22
33import (
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