11// Copyright (c) 2020, Control Command Inc. All rights reserved.
2- // Copyright (c) 2018-2023 , Sylabs Inc. All rights reserved.
2+ // Copyright (c) 2018-2026 , Sylabs Inc. All rights reserved.
33// This software is licensed under a 3-clause BSD license. Please consult the
44// LICENSE.md file distributed with the sources of this project regarding your
55// rights to use or distribute this software.
@@ -11,6 +11,7 @@ import (
1111 "errors"
1212 "fmt"
1313 "os"
14+ "path/filepath"
1415 "strings"
1516
1617 "github.com/spf13/cobra"
@@ -53,14 +54,15 @@ type contextKey string
5354
5455const (
5556 keyOrigImageURI contextKey = "origImageURI"
57+ keyPullTempDir contextKey = "pullTempDir"
5658)
5759
5860// actionPreRun will:
5961// - do the proper path unsetting;
6062// - and implement flag inferences for:
6163// --compat
6264// --hostname
63- // - run replaceURIWithImage;
65+ // - retrieve remote images to the cache or a temporary directory for execution
6466func actionPreRun (cmd * cobra.Command , args []string ) {
6567 // For compatibility - we still set USER_PATH so it will be visible in the
6668 // container, and can be used there if needed. USER_PATH is not used by
@@ -87,39 +89,76 @@ func actionPreRun(cmd *cobra.Command, args []string) {
8789 utsNamespace = true
8890 }
8991
90- origImageURI := replaceURIWithImage (cmd .Context (), cmd , args )
92+ // Store the original image URI in the command context, so it can be used by
93+ // any fallback logic.
94+ origImageURI := args [0 ]
9195 cmd .SetContext (context .WithValue (cmd .Context (), keyOrigImageURI , & origImageURI ))
92- }
9396
94- func handleOCI (ctx context.Context , imgCache * cache.Handle , cmd * cobra.Command , pullFrom string ) (string , error ) {
95- ociAuth , err := makeOCICredentials (cmd )
96- if err != nil {
97- sylog .Fatalf ("While creating Docker credentials: %v" , err )
98- }
97+ // Replace remote URI with a local image path, pulling to cache or a
98+ // temporary directory as needed.
99+ localImage , pullTempDir := uriToImage (cmd .Context (), cmd , origImageURI )
100+ args [0 ] = localImage
99101
100- pullOpts := oci.PullOptions {
101- TmpDir : tmpDir ,
102- OciAuth : ociAuth ,
103- DockerHost : dockerHost ,
104- NoHTTPS : noHTTPS ,
105- OciSif : isOCI ,
106- KeepLayers : keepLayers ,
107- Platform : getOCIPlatform (),
108- ReqAuthFile : reqAuthFile ,
109- }
102+ // Track the pullTempDir (if set) in the context, so it can be cleaned up on container exit.
103+ cmd .SetContext (context .WithValue (cmd .Context (), keyPullTempDir , & pullTempDir ))
104+ }
110105
111- return oci .Pull (ctx , imgCache , pullFrom , pullOpts )
106+ func uriToCacheImage (ctx context.Context , refType string , cmd * cobra.Command , imgCache * cache.Handle , pullFrom string ) (string , error ) {
107+ switch refType {
108+ case uri .Library :
109+ return handleLibrary (ctx , imgCache , "" , pullFrom )
110+ case uri .Oras :
111+ ociAuth , err := makeOCICredentials (cmd )
112+ if err != nil {
113+ return "" , fmt .Errorf ("while creating docker credentials: %v" , err )
114+ }
115+ return oras .PullToCache (ctx , imgCache , pullFrom , ociAuth , reqAuthFile )
116+ case uri .Shub :
117+ return shub .PullToCache (ctx , imgCache , pullFrom , noHTTPS )
118+ case ociimage .SupportedTransport (refType ):
119+ return handleOCI (ctx , cmd , imgCache , "" , pullFrom )
120+ case uri .HTTP :
121+ return net .PullToCache (ctx , imgCache , pullFrom )
122+ case uri .HTTPS :
123+ return net .PullToCache (ctx , imgCache , pullFrom )
124+ default :
125+ return "" , fmt .Errorf ("unsupported transport type: %s" , refType )
126+ }
112127}
113128
114- func handleOras (ctx context.Context , imgCache * cache. Handle , cmd * cobra.Command , pullFrom string ) (string , error ) {
115- ociAuth , err := makeOCICredentials ( cmd )
129+ func uriToTempImage (ctx context.Context , refType string , cmd * cobra.Command , imgCache * cache. Handle , pullFrom string ) (string , string , error ) {
130+ pullTempDir , err := os . MkdirTemp ( tmpDir , "singularity-action-pull-" )
116131 if err != nil {
117- return "" , fmt .Errorf ("while creating docker credentials : %v " , err )
132+ return "" , "" , fmt .Errorf ("unable to create temporary directory : %w " , err )
118133 }
119- return oras .Pull (ctx , imgCache , pullFrom , tmpDir , ociAuth , reqAuthFile )
134+ tmpImage := filepath .Join (pullTempDir , "image" )
135+ sylog .Debugf ("Cache disabled, pulling image to temporary file: %s" , tmpImage )
136+
137+ switch refType {
138+ case uri .Library :
139+ _ , err = handleLibrary (ctx , imgCache , tmpImage , pullFrom )
140+ case uri .Oras :
141+ ociAuth , authErr := makeOCICredentials (cmd )
142+ if authErr != nil {
143+ return "" , "" , fmt .Errorf ("while creating docker credentials: %v" , authErr )
144+ }
145+ _ , err = oras .PullToFile (ctx , imgCache , tmpImage , pullFrom , ociAuth , reqAuthFile )
146+ case uri .Shub :
147+ _ , err = shub .PullToFile (ctx , imgCache , tmpImage , pullFrom , noHTTPS )
148+ case ociimage .SupportedTransport (refType ):
149+ _ , err = handleOCI (ctx , cmd , imgCache , tmpImage , pullFrom )
150+ case uri .HTTP :
151+ _ , err = net .PullToFile (ctx , imgCache , tmpImage , pullFrom )
152+ case uri .HTTPS :
153+ _ , err = net .PullToFile (ctx , imgCache , tmpImage , pullFrom )
154+ default :
155+ return "" , "" , fmt .Errorf ("unsupported transport type: %s" , refType )
156+ }
157+
158+ return tmpImage , pullTempDir , err
120159}
121160
122- func handleLibrary (ctx context.Context , imgCache * cache.Handle , pullFrom string ) (string , error ) {
161+ func handleLibrary (ctx context.Context , imgCache * cache.Handle , tmpImage , pullFrom string ) (string , error ) {
123162 r , err := library .NormalizeLibraryRef (pullFrom )
124163 if err != nil {
125164 return "" , err
@@ -149,50 +188,71 @@ func handleLibrary(ctx context.Context, imgCache *cache.Handle, pullFrom string)
149188 TmpDir : tmpDir ,
150189 Platform : getOCIPlatform (),
151190 }
152- return library .Pull (ctx , imgCache , r , pullOpts )
153- }
154191
155- func handleShub (ctx context.Context , imgCache * cache.Handle , pullFrom string ) (string , error ) {
156- return shub .Pull (ctx , imgCache , pullFrom , tmpDir , noHTTPS )
192+ var imagePath string
193+ if tmpImage == "" {
194+ imagePath , err = library .PullToCache (ctx , imgCache , r , pullOpts )
195+ } else {
196+ imagePath , err = library .PullToFile (ctx , imgCache , tmpImage , r , pullOpts )
197+ }
198+
199+ if err != nil && err != library .ErrLibraryPullUnsigned {
200+ return "" , err
201+ }
202+ if err == library .ErrLibraryPullUnsigned {
203+ sylog .Warningf ("Skipping container verification" )
204+ }
205+ return imagePath , nil
157206}
158207
159- func handleNet (ctx context.Context , imgCache * cache.Handle , pullFrom string ) (string , error ) {
160- return net .Pull (ctx , imgCache , pullFrom , tmpDir )
208+ func handleOCI (ctx context.Context , cmd * cobra.Command , imgCache * cache.Handle , tmpImage , pullFrom string ) (string , error ) {
209+ ociAuth , err := makeOCICredentials (cmd )
210+ if err != nil {
211+ sylog .Fatalf ("While creating Docker credentials: %v" , err )
212+ }
213+
214+ pullOpts := oci.PullOptions {
215+ TmpDir : tmpDir ,
216+ OciAuth : ociAuth ,
217+ DockerHost : dockerHost ,
218+ NoHTTPS : noHTTPS ,
219+ OciSif : isOCI ,
220+ KeepLayers : keepLayers ,
221+ Platform : getOCIPlatform (),
222+ ReqAuthFile : reqAuthFile ,
223+ }
224+
225+ if tmpImage == "" {
226+ return oci .PullToCache (ctx , imgCache , pullFrom , pullOpts )
227+ }
228+ return oci .PullToFile (ctx , imgCache , tmpImage , pullFrom , pullOpts )
161229}
162230
163- func replaceURIWithImage (ctx context.Context , cmd * cobra.Command , args []string ) string {
164- origImageURI := args [0 ]
165- t , _ := uri .Split (origImageURI )
231+ // uriToImage will pull a remote image to the cache, or a temporary directory if
232+ // the cache is disabled. It returns a path to the pulled image, and the
233+ // temporary directory that should be removed when the container exits, where
234+ // applicable.
235+ func uriToImage (ctx context.Context , cmd * cobra.Command , origImageURI string ) (imagePath , tempDir string ) {
236+ refType , _ := uri .Split (origImageURI )
166237 // If joining an instance (instance://xxx), or we have a bare filename then
167238 // no retrieval / conversion is required.
168- if t == "instance" || t == "" {
169- return origImageURI
239+ if refType == "instance" || refType == "" {
240+ return origImageURI , ""
170241 }
171242
172- var image string
173- var err error
174-
175- // Create a cache handle only when we know we are using a URI
176243 imgCache := getCacheHandle (cache.Config {Disable : disableCache })
177244 if imgCache == nil {
178245 sylog .Fatalf ("failed to create a new image cache handle" )
179246 }
180247
181- switch t {
182- case uri .Library :
183- image , err = handleLibrary (ctx , imgCache , origImageURI )
184- case uri .Oras :
185- image , err = handleOras (ctx , imgCache , cmd , origImageURI )
186- case uri .Shub :
187- image , err = handleShub (ctx , imgCache , origImageURI )
188- case ociimage .SupportedTransport (t ):
189- image , err = handleOCI (ctx , imgCache , cmd , origImageURI )
190- case uri .HTTP :
191- image , err = handleNet (ctx , imgCache , origImageURI )
192- case uri .HTTPS :
193- image , err = handleNet (ctx , imgCache , origImageURI )
194- default :
195- sylog .Fatalf ("Unsupported transport type: %s" , t )
248+ // If the cache is disabled, then we pull to a temporary location, which
249+ // will need to be removed on container exit. Otherwise, we pull to the
250+ // cache and run directly from there.
251+ var err error
252+ if disableCache {
253+ imagePath , tempDir , err = uriToTempImage (ctx , refType , cmd , imgCache , origImageURI )
254+ } else {
255+ imagePath , err = uriToCacheImage (ctx , refType , cmd , imgCache , origImageURI )
196256 }
197257
198258 // If we are in OCI mode, then we can still attempt to run from a directory
@@ -206,16 +266,26 @@ func replaceURIWithImage(ctx context.Context, cmd *cobra.Command, args []string)
206266 }
207267 sylog .Warningf ("%v" , err )
208268 sylog .Warningf ("OCI-SIF could not be created, falling back to unpacking OCI bundle in temporary sandbox dir" )
209- return origImageURI
269+ return origImageURI , ""
210270 }
211271
212272 if err != nil {
213273 sylog .Fatalf ("Unable to handle %s uri: %v" , origImageURI , err )
214274 }
215275
216- args [0 ] = image
276+ return imagePath , tempDir
277+ }
217278
218- return origImageURI
279+ func pullTempDirFromContext (ctx context.Context ) string {
280+ pullTempDirPtr := ctx .Value (keyPullTempDir )
281+ if pullTempDirPtr != nil {
282+ pullTempDir , ok := pullTempDirPtr .(* string )
283+ if ! ok {
284+ sylog .Fatalf ("unable to recover pull temp dir (expected string, found: %T) from context" , pullTempDirPtr )
285+ }
286+ return * pullTempDir
287+ }
288+ return ""
219289}
220290
221291// ExecCmd represents the exec command
@@ -227,10 +297,11 @@ var ExecCmd = &cobra.Command{
227297 Run : func (cmd * cobra.Command , args []string ) {
228298 // singularity exec <image> <command> [args...]
229299 ep := launcher.ExecParams {
230- Image : args [0 ],
231- Action : "exec" ,
232- Process : args [1 ],
233- Args : args [2 :],
300+ Image : args [0 ],
301+ PullTempDir : pullTempDirFromContext (cmd .Context ()),
302+ Action : "exec" ,
303+ Process : args [1 ],
304+ Args : args [2 :],
234305 }
235306 if err := launchContainer (cmd , ep ); err != nil {
236307 sylog .Fatalf ("%s" , err )
@@ -252,8 +323,9 @@ var ShellCmd = &cobra.Command{
252323 Run : func (cmd * cobra.Command , args []string ) {
253324 // singularity shell <image>
254325 ep := launcher.ExecParams {
255- Image : args [0 ],
256- Action : "shell" ,
326+ Image : args [0 ],
327+ PullTempDir : pullTempDirFromContext (cmd .Context ()),
328+ Action : "shell" ,
257329 }
258330 if err := launchContainer (cmd , ep ); err != nil {
259331 sylog .Fatalf ("%s" , err )
@@ -275,9 +347,10 @@ var RunCmd = &cobra.Command{
275347 Run : func (cmd * cobra.Command , args []string ) {
276348 // singularity run <image> [args...]
277349 ep := launcher.ExecParams {
278- Image : args [0 ],
279- Action : "run" ,
280- Args : args [1 :],
350+ Image : args [0 ],
351+ PullTempDir : pullTempDirFromContext (cmd .Context ()),
352+ Action : "run" ,
353+ Args : args [1 :],
281354 }
282355 if err := launchContainer (cmd , ep ); err != nil {
283356 sylog .Fatalf ("%s" , err )
@@ -299,9 +372,10 @@ var TestCmd = &cobra.Command{
299372 Run : func (cmd * cobra.Command , args []string ) {
300373 // singularity test <image> [args...]
301374 ep := launcher.ExecParams {
302- Image : args [0 ],
303- Action : "test" ,
304- Args : args [1 :],
375+ Image : args [0 ],
376+ PullTempDir : pullTempDirFromContext (cmd .Context ()),
377+ Action : "test" ,
378+ Args : args [1 :],
305379 }
306380 if err := launchContainer (cmd , ep ); err != nil {
307381 sylog .Fatalf ("%s" , err )
@@ -396,6 +470,7 @@ func launchContainer(cmd *cobra.Command, ep launcher.ExecParams) error {
396470 launcher .OptNoCompat (noCompat ),
397471 launcher .OptTmpSandbox (tmpSandbox ),
398472 launcher .OptNoTmpSandbox (noTmpSandbox ),
473+ launcher .OptPullTempDir (ep .PullTempDir ),
399474 }
400475
401476 // Explicitly use the interface type here, as we will add alternative launchers later...
0 commit comments