Skip to content

Implement Standalone SOCI Image Convert mode for SOCI index creation without containerd#1870

Closed
prafgup wants to merge 1 commit into
awslabs:mainfrom
prafgup:main
Closed

Implement Standalone SOCI Image Convert mode for SOCI index creation without containerd#1870
prafgup wants to merge 1 commit into
awslabs:mainfrom
prafgup:main

Conversation

@prafgup
Copy link
Copy Markdown
Contributor

@prafgup prafgup commented Feb 20, 2026

NEW PR - #1881

Issue #, if available:
Closes #1057

Description of changes:

This PR adds standalone mode to the soci convert command, enabling SOCI index creation without requiring a running containerd daemon. This is particularly useful for CI/CD pipelines and environments where running containerd is impractical without privileged mode.

Key Features

  • Standalone Mode: Download images directly from registries using ORAS, create SOCI indexes, and push converted images back to registries
  • Authentication: Full support for registry authentication via --user flag, Docker config, and environment variables
  • TLS Options: Support for --skip-verify and --plain-http flags
  • Platform Selection: Compatible with --platform and --all-platforms flags
  • Verbose Output: Optional --verbose flag for detailed progress information

Usage Example

soci convert --standalone \
  --user myuser:mypass \
  --verbose \
  source-registry.com/myimage:latest \
  dest-registry.com/myimage:latest-soci  

Testing performed:

  • Added new Integration test and all integration tests pass (TestStandaloneConvert*)
    - Basic conversion with validation
    - Specific platform selection (--platform)
    - Authentication with --user flag
    - Error handling (nonexistent images, missing arguments)
    - Idempotency verification
  • Tested on ARM64 architecture (linux/arm64/v8) (lima on macOs)
    • lima sudo GO_TEST_FLAGS="-run TestStandaloneConvert" make integration
    • lima sudo make test
  • Manual verification of image download, conversion, and push workflow
sudo ./out/soci convert xxx.xxx.com/xxx:full xxx.xxx.com/xxx:standalone-test-99 --all-platforms --standalone --user $REPOSITORY:$TOKEN  --verbose

Standalone mode: downloading image xxx.xxx.com/xxx:full
Downloading image xxx.xxx.com/xxx:full from registry...
  Downloading fd398e9fa269 (application/vnd.oci.image.layer.v1.tar+gzip, 5616874 bytes)
  Downloading 12bb71ceae34 (application/vnd.oci.image.layer.v1.tar+gzip, 221568 bytes)
  Downloading d28cb90eb7da (application/vnd.oci.image.config.v1+json, 17958 bytes)
  Downloading ef6465a1674b (application/vnd.oci.image.layer.v1.tar+gzip, 78 bytes)
  Downloading 3e9c02b40ec1 (application/vnd.oci.image.layer.v1.tar+gzip, 14489277 bytes)
  Downloading 7511aa994dfe (application/vnd.oci.image.layer.v1.tar+gzip, 3222 bytes)
  Downloading 5b91cf883cde (application/vnd.oci.image.layer.v1.tar+gzip, 10392231 bytes)
  Downloading f6a26d16afc7 (application/vnd.oci.image.layer.v1.tar+gzip, 10395600 bytes)
  Downloading 4f4fb700ef54 (application/vnd.oci.image.layer.v1.tar+gzip, 32 bytes)
  Downloading d8c6b361b623 (application/vnd.oci.image.layer.v1.tar+gzip, 271302478 bytes)
  Downloading faf5416d559a (application/vnd.oci.image.manifest.v1+json, 2383 bytes)
Successfully downloaded image
Converting image to SOCI-enabled format with 1 platform(s)
ztoc skipped - layer sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 (application/vnd.oci.image.layer.v1.tar+gzip) size 32 is less than min-layer-size 10485760
ztoc skipped - layer sha256:5b91cf883cde9e49909af81cdce1cc89948745e38796395b0a90d859d7db97d2 (application/vnd.oci.image.layer.v1.tar+gzip) size 10392231 is less than min-layer-size 10485760
ztoc skipped - layer sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 (application/vnd.oci.image.layer.v1.tar+gzip) size 32 is less than min-layer-size 10485760
ztoc skipped - layer sha256:ef6465a1674b258144bd7eac86347823926436a5d53f100cadfe098c82e4c4d4 (application/vnd.oci.image.layer.v1.tar+gzip) size 78 is less than min-layer-size 10485760
ztoc skipped - layer sha256:f6a26d16afc76b03f36290b56770ae0db2ab11aab759d2c7bd49bdfc865b3b87 (application/vnd.oci.image.layer.v1.tar+gzip) size 10395600 is less than min-layer-size 10485760
ztoc skipped - layer sha256:fd398e9fa269e6877ec10f14d61837fc23f05d757530fed2946073018dc07705 (application/vnd.oci.image.layer.v1.tar+gzip) size 5616874 is less than min-layer-size 10485760
ztoc skipped - layer sha256:12bb71ceae3401038ce93aa7ce3a115f4a5cb3e9a85277ef4a97718c0fc9298e (application/vnd.oci.image.layer.v1.tar+gzip) size 221568 is less than min-layer-size 10485760
ztoc skipped - layer sha256:ef6465a1674b258144bd7eac86347823926436a5d53f100cadfe098c82e4c4d4 (application/vnd.oci.image.layer.v1.tar+gzip) size 78 is less than min-layer-size 10485760
ztoc skipped - layer sha256:7511aa994dfed5f54c697fcec6db7176449e7d87317b3f6ad093185182ce01c7 (application/vnd.oci.image.layer.v1.tar+gzip) size 3222 is less than min-layer-size 10485760
layer sha256:3e9c02b40ec11c1a3cc84f775102e2a3b100700fc7753098adc6d62e3fc6563b -> ztoc sha256:0aa198e2f2f6ff02675b7f7dc27e008a1b81bc15a87a5e4d5f868259a3cdc5f0
layer sha256:d8c6b361b623cabb0a89fad4a7d491d7d543f0834ca0c581e0176e1b76a68a15 -> ztoc sha256:ea8f91d2a57139b053186c8a1ae1901684bc06fda5b7d00aef0bd3d6d4e9e4c1
Successfully created SOCI-enabled image: sha256:f265a1fddffc035eb949ac3ff03a597ce4c823b8bde2ae6cb63a0c85f69d0de9
Pushing SOCI-enabled image to xxx.xxx.com/xxx:standalone-test-99...
Successfully pushed SOCI-enabled image to xxx.xxx.com/xxx:standalone-test-99 (digest: sha256:f265a1fddffc035eb949ac3ff03a597ce4c823b8bde2ae6cb63a0c85f69d0de9)

Integration tests -

sudo GO_TEST_FLAGS="-run TestStandalone -count=1" make integration
cd cmd/ ; GO111MODULE=auto go build -o /Volumes/git/prafulg/soci-snapshotter/out/soci-snapshotter-grpc  -ldflags '-X github.com/awslabs/soci-snapshotter/version.Version=be11c954.m -X github.com/awslabs/soci-snapshotter/version.Revision=be11c954b73ee597b504985deea72eebdefce2f2.m  -s -w '  ./soci-snapshotter-grpc
cd cmd/ ; GO111MODULE=auto go build -o /Volumes/git/prafulg/soci-snapshotter/out/soci  -ldflags '-X github.com/awslabs/soci-snapshotter/version.Version=be11c954.m -X github.com/awslabs/soci-snapshotter/version.Revision=be11c954b73ee597b504985deea72eebdefce2f2.m  -s -w '  ./soci
integration
SOCI_SNAPSHOTTER_PROJECT_ROOT=/Volumes/git/prafulg/soci-snapshotter
=== RUN   TestStandaloneConvertBasic
=== PAUSE TestStandaloneConvertBasic
=== RUN   TestStandaloneConvertSpecificPlatform
=== PAUSE TestStandaloneConvertSpecificPlatform
=== RUN   TestStandaloneConvertWithUserAuth
=== PAUSE TestStandaloneConvertWithUserAuth
=== RUN   TestStandaloneInvalidConversion
=== PAUSE TestStandaloneInvalidConversion
=== RUN   TestStandaloneConvertIdempotent
=== PAUSE TestStandaloneConvertIdempotent
=== CONT  TestStandaloneConvertBasic
=== CONT  TestStandaloneInvalidConversion
=== CONT  TestStandaloneConvertSpecificPlatform
=== CONT  TestStandaloneConvertWithUserAuth
=== CONT  TestStandaloneConvertIdempotent
=== RUN   TestStandaloneInvalidConversion/nonexistent_image
=== RUN   TestStandaloneInvalidConversion/missing_destination_ref
--- PASS: TestStandaloneInvalidConversion (1.38s)
    --- PASS: TestStandaloneInvalidConversion/nonexistent_image (0.11s)
    --- PASS: TestStandaloneInvalidConversion/missing_destination_ref (0.03s)
--- PASS: TestStandaloneConvertSpecificPlatform (21.16s)
--- PASS: TestStandaloneConvertWithUserAuth (21.28s)
--- PASS: TestStandaloneConvertIdempotent (22.26s)
--- PASS: TestStandaloneConvertBasic (22.51s)
PASS
ok      github.com/awslabs/soci-snapshotter/integration 132.773s

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@prafgup prafgup requested a review from a team as a code owner February 20, 2026 20:01
@github-actions github-actions Bot added go Pull requests that update Go code testing Unit and/or integration tests labels Feb 20, 2026
@prafgup
Copy link
Copy Markdown
Contributor Author

prafgup commented Feb 20, 2026

Hey SOCI Devs 👋
Would appreciate a review here.
cc @Shubhranshu153 @sondavidb @Swapnanil-Gupta

Also happy to followup with any changes (in this or a followup PR) 😄


orasStore, err := oci.New(tmpDir)
if err != nil {
cleanupTempDir(tmpDir)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we defer this instead of doing it in every error branch? You probably want to cleanup on success as well?

Copy link
Copy Markdown
Contributor Author

@prafgup prafgup Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a look earlier into refactoring this and figured that it would require us having named returns and then checking error in the defer func.
Overall using named returns make things somewhat complex to read, thus I let it be like this where we would cleanup on specific errors.

We do always cleanup in success scenarios here upstream - https://github.com/prafgup/soci-snapshotter/blob/83db223b6161ecde510d4a437d432eebb81baf35/cmd/soci/commands/convert.go#L207 lemme check if we can remove this manual cleanup then in case of errors and just move imageInfo.Cleanup() to be before error.

Copy link
Copy Markdown
Contributor Author

@prafgup prafgup Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in case of error upstream runStandaloneConvert wont have imageInfo to trigger a cleanup, thus I think its fine to keep it as is. Upstream always makes sure to cleanup in case DownloadImageFromRegistry is successful, and in case of failure in DownloadImageFromRegistry we would manually cleanup before returning a error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really a fan of this pattern since we have to be sure every single error out handles this error which is not very Go-like.

I've seen a couple of patterns that I think would flow a little nicer:

  1. Do initial setup for the tempdir, then have a separate function (e.g. downloadImageFromRegistry) that returns an error. Call that func from DownloadImageFromRegistry and on error call the cleanup func. This is probably the least error-prone way to do this.
  2. Declare error var after setting up the dir, then defer a func that calls the cleanup func if err != nil. I generally prefer this option less as if you declare an error within a nested function and forget to refer to the global err var you can inadvertently not call cleanup.

Comment thread cmd/soci/commands/internal/standalone.go
Comment thread cmd/soci/commands/internal/standalone.go
Comment thread cmd/soci/commands/internal/standalone.go Outdated
Signed-off-by: Praful Gupta <prafulgupta6@gmail.com>
Comment thread cmd/soci/commands/convert.go
fmt.Printf("Standalone mode: downloading image %s\n", srcRef)
}

imageInfo, err := internal.DownloadImageFromRegistry(ctx, cmd, srcRef)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems we are adding a operation which is not strictly done by convert. Soci doesnt have a pull command probably because it assumes ctr/ other cli are providing the image is there.

Overall seems like architecturally different to the purpose of convert. Any reason we cannot pull image to soci content store and do the convert and then push. Probably adds complexity vs being architecturally consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, if we were to think of this feature as of multiple steps (pull + convert + push) it would be a bit overly complex where -

  • We would have to maintain a persistent local content store across command invocations, essentially creating a replacement for containerd store.
  • Let a lot of abstractions from StandaloneImageInfo be handled by user across commands.
  • This feature is usually going to be used by CI/CD jobs, where having a single command is much simpler for automation.

I believe having a bit of tradeoff of consistency for simplicity would be the better choice here, as we don't see much extension of standalone convert in future atleast architecturally.

On a side note we do currently have deprecated push and create command which does not support V2 Manifests - https://github.com/awslabs/soci-snapshotter/blob/main/docs/cli-usage.md#soci-create and seems like we focused more on the convert command for V2 Manifests, thus I believe its the right place for standalone convert too.

What do you think? 😄

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah i agree with the cost but doing a push and pull in convert seems a bit hacky. Having a single command is ok as long as it is fundamentally doing a specific operation it was designed for. Still think having a pull/convert and push command here makes better architectural sense. For daemon less services we can use the soci content store.

@sondavidb what do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Shubhranshu153 @sondavidb :)
Requesting an update here regarding this thread for the path forward ^ (this is blocking some development on our end 😓)

One of the other possible option that I can think of apart from keeping this as a flag in convert instead, is that we can have another independent command something like standalone-convert that would do this flow of convert --standalone.
But I do believe that with proper documentation convert --standalone does seem to be the right place for this logic (of converting a image upstream) instead of expanding it across multiple pull/convert/push commands (where push has already been deprecated for V2 use).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we really dont want to architect it entirely one way can be to introduce to introduce a --pull and use the soci store but push i think still needs to be a separate cli command. And push is not deprecated rather its not extended to v2 migration. Having pull/convert/push in a single cli is inherently changing the cmd purpose.

Copy link
Copy Markdown
Contributor Author

@prafgup prafgup Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sondavidb thanks for the review, I glanced over other commends and would address them later. But just want to confirm this main flow.

You want to pull with your CLI command of choice, convert it to a SOCI v2 image, then push the image, which will in turn push the SOCI index with it.

Just to confirm the wording, are you suggesting something like -

crane pull nginx:latest IMG_TAR_PATH

soci convert --standalone IMG_TAR_PATH IMG_OUTPUT_TAR_PATH

crane push IMG_OUTPUT_TAR_PATH my-reg/my-image:my-tag

I believe this makes sense as well. A user can pull the image as tar in some path and we would load that up and index it and store as output. Also in this case we won't have to do any registry authentications as we are neither pulling nor pushing.

Would you prefer this logic to be made explicit by user sending in --standalone flag in create command? Or should we auto detect that the path given (input and output) is from disk and if a tar file exist there proceed with conversion without containerd?

Copy link
Copy Markdown
Contributor

@Shubhranshu153 Shubhranshu153 Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@david for v2 don't we push the entire image with the index together. Here standalone means not using containerd rather doing everything using the soci cli

Also the delete option needs to be created but if soci store is used to pull convert and push images as we need to clean it up also. Or we create tmp store abstraction. (Still I think a delete is needed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the idea of using some other cli to pull and push images if that's an option @prafgup

Copy link
Copy Markdown
Contributor

@sondavidb sondavidb Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, what you said would be my ideal scenario @prafgup. It is honestly probably the least intrusive solution 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @sondavidb @Shubhranshu153 ,

I have created another PR here - #1881

Fundamentally its a bit different from this one thus thought of closing this PR and creating a new clean one.

Would appreciate a Review on the same 😄 (I'll update the docs in a followup once that is merged. Also happy to resolve any small nits and refactoring as followups there as well as Update the docs in followups)

return nil
}

func parseBuilderOptions(cmd *cli.Command) ([]soci.BuilderOption, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also used for create, we should probably do some deduping here. Happy to take that as a followup though.


orasStore, err := oci.New(tmpDir)
if err != nil {
cleanupTempDir(tmpDir)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really a fan of this pattern since we have to be sure every single error out handles this error which is not very Go-like.

I've seen a couple of patterns that I think would flow a little nicer:

  1. Do initial setup for the tempdir, then have a separate function (e.g. downloadImageFromRegistry) that returns an error. Call that func from DownloadImageFromRegistry and on error call the cleanup func. This is probably the least error-prone way to do this.
  2. Declare error var after setting up the dir, then defer a func that calls the cleanup func if err != nil. I generally prefer this option less as if you declare an error within a nested function and forget to refer to the global err var you can inadvertently not call cleanup.

return nil
}

func newRemoteRepo(cmd *cli.Command, refspec reference.Spec) (*remote.Repository, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to reuse anything from the fs package? (I haven't checked but I'm wary of re-duplicating logic, especially when it comes to registry authentication.)

},
}

if cmd.Bool("skip-verify") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems inverted?

Suggested change
if cmd.Bool("skip-verify") {
if !cmd.Bool("skip-verify") {

)

func TestStandaloneConvertBasic(t *testing.T) {
t.Parallel()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Little torn on this but OK with it. Our integration tests run a separate container for each test (newShellWithRegistry I believe composes a container) so parallelizing too much might be a lot for the runner.

@prafgup prafgup closed this Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go Pull requests that update Go code testing Unit and/or integration tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Create a SOCI Index without a Container Runtime

4 participants