Skip to content

Commit

Permalink
add --authfile to OCI login/logout, pull, push, and action-cmds
Browse files Browse the repository at this point in the history
Signed-off-by: jason yang <jasonyangshadow@gmail.com>
  • Loading branch information
JasonYangShadow committed Aug 30, 2024
1 parent b954073 commit 05b9309
Show file tree
Hide file tree
Showing 39 changed files with 797 additions and 595 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ For older changes see the [archived Singularity change log](https://github.com/a
for example: `Apptainer runtime parent: example.sif`.
- Fix the `mconfig -s` option to build the apptainer and starter
binaries statically as documented.
- The `registry login` and `registry logout` commands now support a `--authfile
<path>` flag, which causes the OCI credentials to be written to / removed from
a custom file located at `<path>` instead of the default location
(`$HOME/.singularity/docker-config.json`). The commands `pull`, `push`, `run`,
`exec`, `shell` and `instance start` can now also be passed a `--authfile
<path>` option, to read OCI registry credentials from this custom file.

## Changes for v1.3.x

Expand Down
1 change: 1 addition & 0 deletions cmd/internal/cli/action_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,7 @@ func init() {
cmdManager.RegisterFlagForCmd(&actionIgnoreUsernsFlag, actionsInstanceCmd...)
cmdManager.RegisterFlagForCmd(&actionUnderlayFlag, actionsInstanceCmd...)
cmdManager.RegisterFlagForCmd(&actionShareNSFlag, actionsCmd...)
cmdManager.RegisterFlagForCmd(&commonAuthFileFlag, actionsInstanceCmd...)
cmdManager.RegisterFlagForCmd(&actionRunscriptTimeoutFlag, actionsRunscriptCmd...)
})
}
11 changes: 6 additions & 5 deletions cmd/internal/cli/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@ func handleOCI(ctx context.Context, imgCache *cache.Handle, cmd *cobra.Command,
}

pullOpts := oci.PullOptions{
TmpDir: tmpDir,
OciAuth: ociAuth,
DockerHost: dockerHost,
NoHTTPS: noHTTPS,
TmpDir: tmpDir,
OciAuth: ociAuth,
DockerHost: dockerHost,
NoHTTPS: noHTTPS,
ReqAuthFile: reqAuthFile,
}

return oci.Pull(ctx, imgCache, pullFrom, pullOpts)
Expand All @@ -99,7 +100,7 @@ func handleOras(ctx context.Context, imgCache *cache.Handle, cmd *cobra.Command,
if err != nil {
return "", fmt.Errorf("while creating docker credentials: %v", err)
}
return oras.Pull(ctx, imgCache, pullFrom, tmpDir, ociAuth, noHTTPS)
return oras.Pull(ctx, imgCache, pullFrom, tmpDir, ociAuth, noHTTPS, reqAuthFile)
}

func handleLibrary(ctx context.Context, imgCache *cache.Handle, pullFrom string) (string, error) {
Expand Down
11 changes: 11 additions & 0 deletions cmd/internal/cli/apptainer.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ var (
noHTTPS bool
useBuildConfig bool
tmpDir string
// Optional user requested authentication file for writing/reading OCI registry credentials
reqAuthFile string
)

// apptainer command flags
Expand Down Expand Up @@ -258,6 +260,15 @@ var singBuildConfigFlag = cmdline.Flag{
Usage: "use configuration needed for building containers",
}

// --authfile
var commonAuthFileFlag = cmdline.Flag{
ID: "commonAuthFileFlag",
Value: &reqAuthFile,
DefaultValue: "",
Name: "authfile",
Usage: "Docker-style authentication file to use for writing/reading OCI registry credentials",
}

func getCurrentUser() *user.User {
usr, err := user.Current()
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/internal/cli/keyserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ var KeyserverLogoutCmd = &cobra.Command{
name = args[0]
}

if err := apptainer.KeyserverLogout(remoteConfig, name); err != nil {
if err := apptainer.KeyserverLogout(remoteConfig, name, reqAuthFile); err != nil {
sylog.Fatalf("%s", err)
}
sylog.Infof("Logout succeeded")
Expand Down
1 change: 1 addition & 0 deletions cmd/internal/cli/loginargs.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func ObtainLoginArgs(name string) *apptainer.LoginArgs {
loginArgs.Password = loginPassword
loginArgs.Tokenfile = loginTokenFile
loginArgs.Insecure = loginInsecure
loginArgs.ReqAuthFile = reqAuthFile

if loginPasswordStdin {
p, err := io.ReadAll(os.Stdin)
Expand Down
16 changes: 9 additions & 7 deletions cmd/internal/cli/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ func init() {
cmdManager.RegisterFlagForCmd(&pullAllowUnauthenticatedFlag, PullCmd)
cmdManager.RegisterFlagForCmd(&pullArchFlag, PullCmd)
cmdManager.RegisterFlagForCmd(&pullArchVariantFlag, PullCmd)
cmdManager.RegisterFlagForCmd(&commonAuthFileFlag, PullCmd)
})
}

Expand Down Expand Up @@ -267,7 +268,7 @@ func pullRun(cmd *cobra.Command, args []string) {
sylog.Fatalf("Unable to make docker oci credentials: %s", err)
}

_, err = oras.PullToFile(ctx, imgCache, pullTo, pullFrom, tmpDir, ociAuth, noHTTPS)
_, err = oras.PullToFile(ctx, imgCache, pullTo, pullFrom, tmpDir, ociAuth, noHTTPS, reqAuthFile)
if err != nil {
sylog.Fatalf("While pulling image from oci registry: %v", err)
}
Expand All @@ -288,12 +289,13 @@ func pullRun(cmd *cobra.Command, args []string) {
return
}
pullOpts := oci.PullOptions{
TmpDir: tmpDir,
OciAuth: ociAuth,
DockerHost: dockerHost,
NoHTTPS: noHTTPS,
NoCleanUp: buildArgs.noCleanUp,
Pullarch: arch,
TmpDir: tmpDir,
OciAuth: ociAuth,
DockerHost: dockerHost,
NoHTTPS: noHTTPS,
NoCleanUp: buildArgs.noCleanUp,
Pullarch: arch,
ReqAuthFile: reqAuthFile,
}

_, err = oci.PullToFile(ctx, imgCache, pullTo, pullFrom, pullOpts)
Expand Down
3 changes: 2 additions & 1 deletion cmd/internal/cli/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func init() {
cmdManager.RegisterFlagForCmd(&dockerHostFlag, PushCmd)
cmdManager.RegisterFlagForCmd(&dockerUsernameFlag, PushCmd)
cmdManager.RegisterFlagForCmd(&dockerPasswordFlag, PushCmd)
cmdManager.RegisterFlagForCmd(&commonAuthFileFlag, PushCmd)
})
}

Expand Down Expand Up @@ -167,7 +168,7 @@ var PushCmd = &cobra.Command{
sylog.Fatalf("Unable to make docker oci credentials: %s", err)
}

if err := oras.UploadImage(cmd.Context(), file, ref, ociAuth, noHTTPS); err != nil {
if err := oras.UploadImage(cmd.Context(), file, ref, ociAuth, noHTTPS, reqAuthFile); err != nil {
sylog.Fatalf("Unable to push image to oci registry: %v", err)
}
sylog.Infof("Upload complete")
Expand Down
4 changes: 3 additions & 1 deletion cmd/internal/cli/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ func init() {
cmdManager.RegisterFlagForCmd(&registryLoginUsernameFlag, RegistryLoginCmd)
cmdManager.RegisterFlagForCmd(&registryLoginPasswordFlag, RegistryLoginCmd)
cmdManager.RegisterFlagForCmd(&registryLoginPasswordStdinFlag, RegistryLoginCmd)
cmdManager.RegisterFlagForCmd(&commonAuthFileFlag, RegistryLoginCmd)
cmdManager.RegisterFlagForCmd(&commonAuthFileFlag, RegistryLogoutCmd)
})
}

Expand Down Expand Up @@ -114,7 +116,7 @@ var RegistryLogoutCmd = &cobra.Command{
name = args[0]
}

if err := apptainer.RegistryLogout(remoteConfig, name); err != nil {
if err := apptainer.RegistryLogout(remoteConfig, name, reqAuthFile); err != nil {
sylog.Fatalf("%s", err)
}
sylog.Infof("Logout succeeded")
Expand Down
6 changes: 4 additions & 2 deletions cmd/internal/cli/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,9 @@ var RemoteAddCmd = &cobra.Command{
sylog.Infof("Global option detected. Will not automatically log into remote.")
} else if !remoteNoLogin {
loginArgs := &apptainer.LoginArgs{
Name: name,
Tokenfile: loginTokenFile,
Name: name,
Tokenfile: loginTokenFile,
ReqAuthFile: reqAuthFile,
}
if err := apptainer.RemoteLogin(remoteConfig, loginArgs); err != nil {
sylog.Fatalf("%s", err)
Expand Down Expand Up @@ -392,6 +393,7 @@ var RemoteLoginCmd = &cobra.Command{
loginArgs.Password = loginPassword
loginArgs.Tokenfile = loginTokenFile
loginArgs.Insecure = loginInsecure
loginArgs.ReqAuthFile = reqAuthFile

if loginPasswordStdin {
p, err := io.ReadAll(os.Stdin)
Expand Down
124 changes: 124 additions & 0 deletions e2e/actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3012,6 +3012,129 @@ func (c actionTests) relWorkdirScratch(t *testing.T) {
}
}

// actionAuth tests run/exec/shell flows that involve authenticated pulls from
// OCI registries.
func (c actionTests) actionAuth(t *testing.T) {
profiles := []e2e.Profile{
e2e.UserProfile,
e2e.RootProfile,
}

for _, p := range profiles {
t.Run(p.String(), func(t *testing.T) {
t.Run("default", func(t *testing.T) {
c.actionAuthTester(t, false, p)
})
t.Run("custom", func(t *testing.T) {
c.actionAuthTester(t, true, p)
})
})
}
}

func (c actionTests) actionAuthTester(t *testing.T, withCustomAuthFile bool, profile e2e.Profile) {
e2e.EnsureImage(t, c.env)

tmpdir, tmpdirCleanup := e2e.MakeTempDir(t, c.env.TestDir, "action-auth", "")
t.Cleanup(func() {
if !t.Failed() {
tmpdirCleanup(t)
}
})

prevCwd, err := os.Getwd()
if err != nil {
t.Fatalf("could not get current working directory: %s", err)
}
defer os.Chdir(prevCwd)
if err = os.Chdir(tmpdir); err != nil {
t.Fatalf("could not change cwd to %q: %s", tmpdir, err)
}

localAuthFileName := ""
if withCustomAuthFile {
localAuthFileName = "./my_local_authfile"
}

authFileArgs := []string{}
if withCustomAuthFile {
authFileArgs = []string{"--authfile", localAuthFileName}
}

t.Cleanup(func() {
e2e.PrivateRepoLogout(t, c.env, e2e.UserProfile, localAuthFileName)
})

orasCustomPushTarget := fmt.Sprintf("oras://%s/authfile-pushtest-oras-busybox:latest", c.env.TestRegistryPrivPath)

tests := []struct {
name string
cmd string
args []string
whileLoggedIn bool
expectExit int
}{
{
name: "docker before auth",
cmd: "exec",
args: []string{"--disable-cache", "--no-https", c.env.TestRegistryPrivImage, "true"},
whileLoggedIn: false,
expectExit: 255,
},
{
name: "docker",
cmd: "exec",
args: []string{"--disable-cache", "--no-https", c.env.TestRegistryPrivImage, "true"},
whileLoggedIn: true,
expectExit: 0,
},
{
name: "noauth docker",
cmd: "exec",
args: []string{"--disable-cache", "--no-https", c.env.TestRegistryPrivImage, "true"},
whileLoggedIn: false,
expectExit: 255,
},
{
name: "oras push",
cmd: "push",
args: []string{c.env.ImagePath, orasCustomPushTarget},
whileLoggedIn: true,
expectExit: 0,
},
{
name: "noauth oras",
cmd: "exec",
args: []string{"--disable-cache", "--no-https", orasCustomPushTarget, "true"},
whileLoggedIn: false,
expectExit: 255,
},
{
name: "oras",
cmd: "exec",
args: []string{"--disable-cache", "--no-https", orasCustomPushTarget, "true"},
whileLoggedIn: true,
expectExit: 0,
},
}

for _, tt := range tests {
if tt.whileLoggedIn {
e2e.PrivateRepoLogin(t, c.env, profile, localAuthFileName)
} else {
e2e.PrivateRepoLogout(t, c.env, profile, localAuthFileName)
}
c.env.RunApptainer(
t,
e2e.AsSubtest(tt.name),
e2e.WithProfile(profile),
e2e.WithCommand(tt.cmd),
e2e.WithArgs(append(authFileArgs, tt.args...)...),
e2e.ExpectExit(tt.expectExit),
)
}
}

// E2ETests is the main func to trigger the test suite
func E2ETests(env e2e.TestEnv) testhelper.Tests {
c := actionTests{
Expand Down Expand Up @@ -3064,5 +3187,6 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests {
"fakeroot home": c.actionFakerootHome, // test home dir in fakeroot
"relWorkdirScratch": np(c.relWorkdirScratch), // test relative --workdir with --scratch
"issue 1868": c.issue1868, // https://github.com/apptainer/apptainer/issues/1868
"auth": np(c.actionAuth), // tests action cmds w/authenticated pulls from OCI registries
}
}
Loading

0 comments on commit 05b9309

Please sign in to comment.