Repository: digitalocean/doctl
Pull request #1379 (2023-05-18 17:09:26)

allow read-only creds for container registry login
Addresses #1375. Adds a new flag to login to the container registry generating a read-only API token.


Patch:
diff --git a/args.go b/args.go
index a2462851..9a0afc81 100644
--- a/args.go
+++ b/args.go
@@ -379,6 +379,8 @@ const (
 	ArgRegistry = "registry"
 	// ArgRegistryExpirySeconds indicates the length of time the token will be valid in seconds.
 	ArgRegistryExpirySeconds = "expiry-seconds"
+	// ArgRegistryReadOnly indicates that a generated registry API token should be read-only.
+	ArgRegistryReadOnly = "read-only"
 	// ArgSubscriptionTier is a subscription tier slug.
 	ArgSubscriptionTier = "subscription-tier"
 	// ArgGCIncludeUntaggedManifests indicates that a garbage collection should delete
diff --git a/commands/registry.go b/commands/registry.go
index a38819f5..61dace31 100644
--- a/commands/registry.go
+++ b/commands/registry.go
@@ -84,6 +84,8 @@ func Registry() *Command {
 		loginRegDesc, Writer)
 	AddIntFlag(cmdRegistryLogin, doctl.ArgRegistryExpirySeconds, "", 0,
 		"The length of time the registry credentials will be valid for in seconds. By default, the credentials do not expire.")
+	AddBoolFlag(cmdRegistryLogin, doctl.ArgRegistryReadOnly, "", false,
+		"If true, the DigitalOcean API token generated by the login command will be read-only, causing any push operations to fail. By default, the API token is read-write.")
 
 	logoutRegDesc := "This command logs Docker out of the private container registry, revoking access to it."
 	cmdRunRegistryLogout := CmdBuilder(cmd, RunRegistryLogout, "logout", "Log out Docker from a container registry",
@@ -379,8 +381,13 @@ func RunRegistryLogin(c *CmdConfig) error {
 	if err != nil {
 		return err
 	}
+	readOnly, err := c.Doit.GetBool(c.NS, doctl.ArgRegistryReadOnly)
+	if err != nil {
+		return err
+	}
+
 	regCredReq := godo.RegistryDockerCredentialsRequest{
-		ReadWrite: true,
+		ReadWrite: !readOnly,
 	}
 	if expirySeconds != 0 {
 		regCredReq.ExpirySeconds = godo.Int(expirySeconds)


Test patch:
diff --git a/commands/registry_test.go b/commands/registry_test.go
index cd1cd3d8..8279a012 100644
--- a/commands/registry_test.go
+++ b/commands/registry_test.go
@@ -639,6 +639,7 @@ func TestRegistryLogin(t *testing.T) {
 	tests := []struct {
 		name          string
 		expirySeconds int
+		readOnly      bool
 		expect        func(m *mocks.MockRegistryService)
 	}{
 		{
@@ -662,6 +663,17 @@ func TestRegistryLogin(t *testing.T) {
 				}).Return(testDockerCredentials, nil)
 			},
 		},
+		{
+			name:          "with-read-only",
+			expirySeconds: 0,
+			readOnly:      true,
+			expect: func(m *mocks.MockRegistryService) {
+				m.EXPECT().Endpoint().Return(do.RegistryHostname)
+				m.EXPECT().DockerCredentials(&godo.RegistryDockerCredentialsRequest{
+					ReadWrite: false,
+				}).Return(testDockerCredentials, nil)
+			},
+		},
 	}
 
 	for _, test := range tests {
@@ -672,6 +684,7 @@ func TestRegistryLogin(t *testing.T) {
 				}
 
 				config.Doit.Set(config.NS, doctl.ArgRegistryExpirySeconds, test.expirySeconds)
+				config.Doit.Set(config.NS, doctl.ArgRegistryReadOnly, test.readOnly)
 
 				config.Out = os.Stderr
 				err := RunRegistryLogin(config)
diff --git a/integration/registry_login_test.go b/integration/registry_login_test.go
index 500dab80..6312b096 100644
--- a/integration/registry_login_test.go
+++ b/integration/registry_login_test.go
@@ -47,11 +47,16 @@ var _ = suite("registry/login", func(t *testing.T, when spec.G, it spec.S) {
 					return
 				}
 
+				readWriteParam := req.URL.Query().Get("read_write")
 				expiryParam := req.URL.Query().Get("expiry_seconds")
 				if expiryParam == "3600" {
 					w.Write([]byte(registryDockerCredentialsExpiryResponse))
 				} else if expiryParam == "" {
-					w.Write([]byte(registryDockerCredentialsResponse))
+					if readWriteParam == "false" {
+						w.Write([]byte(registryDockerCredentialsReadOnlyRegistryResponse))
+					} else {
+						w.Write([]byte(registryDockerCredentialsResponse))
+					}
 				} else {
 					t.Fatalf("received unknown value: %s", expiryParam)
 				}
@@ -131,8 +136,43 @@ var _ = suite("registry/login", func(t *testing.T, when spec.G, it spec.S) {
 			}
 		})
 	})
+
+	when("read-only flag is passed", func() {
+		it("add the correct query parameter", func() {
+			tmpDir := t.TempDir()
+
+			config := filepath.Join(tmpDir, "config.json")
+
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"registry",
+				"login",
+				"--read-only",
+				"true",
+			)
+			cmd.Env = os.Environ()
+			cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONFIG=%s", tmpDir))
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err)
+
+			fileBytes, err := ioutil.ReadFile(config)
+			expect.NoError(err)
+
+			var dc dockerConfig
+			err = json.Unmarshal(fileBytes, &dc)
+			expect.NoError(err)
+
+			expect.Equal("Logging Docker in to registry.digitalocean.com\n", string(output))
+			for host := range dc.Auths {
+				expect.Equal("readonlyregistry.registry.com", host)
+			}
+		})
+	})
 })
 
 const (
-	registryDockerCredentialsExpiryResponse = `{"auths":{"expiring.registry.com":{"auth":"Y3JlZGVudGlhbHM6dGhhdGV4cGlyZQ=="}}}`
+	registryDockerCredentialsExpiryResponse           = `{"auths":{"expiring.registry.com":{"auth":"Y3JlZGVudGlhbHM6dGhhdGV4cGlyZQ=="}}}`
+	registryDockerCredentialsReadOnlyRegistryResponse = `{"auths":{"readonlyregistry.registry.com":{"auth":"Y3JlZGVudGlhbHM6dGhhdGV4cGlyZQ=="}}}`
 )