client/secrets: Wrap results and options

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-10-21 21:33:26 +02:00
parent 2401bd1e12
commit 95fac07ccc
26 changed files with 360 additions and 295 deletions

View File

@@ -203,11 +203,11 @@ type VolumeAPIClient interface {
// SecretAPIClient defines API client methods for secrets
type SecretAPIClient interface {
SecretList(ctx context.Context, options SecretListOptions) ([]swarm.Secret, error)
SecretCreate(ctx context.Context, secret swarm.SecretSpec) (swarm.SecretCreateResponse, error)
SecretRemove(ctx context.Context, id string) error
SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error
SecretList(ctx context.Context, options SecretListOptions) (SecretListResult, error)
SecretCreate(ctx context.Context, options SecretCreateOptions) (SecretCreateResult, error)
SecretRemove(ctx context.Context, id string, options SecretRemoveOptions) (SecretRemoveResult, error)
SecretInspect(ctx context.Context, id string, options SecretInspectOptions) (SecretInspectResult, error)
SecretUpdate(ctx context.Context, id string, options SecretUpdateOptions) (SecretUpdateResult, error)
}
// ConfigAPIClient defines API client methods for configs

View File

@@ -7,15 +7,28 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// SecretCreate creates a new secret.
func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (swarm.SecretCreateResponse, error) {
resp, err := cli.post(ctx, "/secrets/create", nil, secret, nil)
defer ensureReaderClosed(resp)
if err != nil {
return swarm.SecretCreateResponse{}, err
// SecretCreateOptions holds options for creating a secret.
type SecretCreateOptions struct {
Spec swarm.SecretSpec
}
var response swarm.SecretCreateResponse
err = json.NewDecoder(resp.Body).Decode(&response)
return response, err
// SecretCreateResult holds the result from the [Client.SecretCreate] method.
type SecretCreateResult struct {
ID string
}
// SecretCreate creates a new secret.
func (cli *Client) SecretCreate(ctx context.Context, options SecretCreateOptions) (SecretCreateResult, error) {
resp, err := cli.post(ctx, "/secrets/create", nil, options.Spec, nil)
defer ensureReaderClosed(resp)
if err != nil {
return SecretCreateResult{}, err
}
var out swarm.ConfigCreateResponse
err = json.NewDecoder(resp.Body).Decode(&out)
if err != nil {
return SecretCreateResult{}, err
}
return SecretCreateResult{ID: out.ID}, nil
}

View File

@@ -17,7 +17,7 @@ import (
func TestSecretCreateError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
_, err = client.SecretCreate(context.Background(), swarm.SecretSpec{})
_, err = client.SecretCreate(context.Background(), SecretCreateOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}
@@ -40,7 +40,7 @@ func TestSecretCreate(t *testing.T) {
}))
assert.NilError(t, err)
r, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
r, err := client.SecretCreate(context.Background(), SecretCreateOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(r.ID, "test_secret"))
}

View File

@@ -1,34 +1,35 @@
package client
import (
"bytes"
"context"
"encoding/json"
"io"
"github.com/moby/moby/api/types/swarm"
)
// SecretInspectWithRaw returns the secret information with raw data
func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
// SecretInspectOptions holds options for inspecting a secret.
type SecretInspectOptions struct {
// Add future optional parameters here
}
// SecretInspectResult holds the result from the [Client.SecretInspect]. method.
type SecretInspectResult struct {
Secret swarm.Secret
Raw []byte
}
// SecretInspect returns the secret information with raw data.
func (cli *Client) SecretInspect(ctx context.Context, id string, options SecretInspectOptions) (SecretInspectResult, error) {
id, err := trimID("secret", id)
if err != nil {
return swarm.Secret{}, nil, err
return SecretInspectResult{}, err
}
resp, err := cli.get(ctx, "/secrets/"+id, nil, nil)
defer ensureReaderClosed(resp)
if err != nil {
return swarm.Secret{}, nil, err
return SecretInspectResult{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return swarm.Secret{}, nil, err
}
var secret swarm.Secret
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&secret)
return secret, body, err
var out SecretInspectResult
out.Raw, err = decodeWithRaw(resp, &out.Secret)
return out, err
}

View File

@@ -19,7 +19,7 @@ func TestSecretInspectError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
_, _, err = client.SecretInspectWithRaw(context.Background(), "nothing")
_, err = client.SecretInspect(context.Background(), "nothing", SecretInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}
@@ -27,7 +27,7 @@ func TestSecretInspectSecretNotFound(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusNotFound, "Server error")))
assert.NilError(t, err)
_, _, err = client.SecretInspectWithRaw(context.Background(), "unknown")
_, err = client.SecretInspect(context.Background(), "unknown", SecretInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
}
@@ -36,11 +36,11 @@ func TestSecretInspectWithEmptyID(t *testing.T) {
return nil, errors.New("should not make request")
}))
assert.NilError(t, err)
_, _, err = client.SecretInspectWithRaw(context.Background(), "")
_, err = client.SecretInspect(context.Background(), "", SecretInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, _, err = client.SecretInspectWithRaw(context.Background(), " ")
_, err = client.SecretInspect(context.Background(), " ", SecretInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -64,7 +64,7 @@ func TestSecretInspect(t *testing.T) {
}))
assert.NilError(t, err)
secretInspect, _, err := client.SecretInspectWithRaw(context.Background(), "secret_id")
res, err := client.SecretInspect(context.Background(), "secret_id", SecretInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(secretInspect.ID, "secret_id"))
assert.Check(t, is.Equal(res.Secret.ID, "secret_id"))
}

View File

@@ -8,18 +8,31 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// SecretList returns the list of secrets.
func (cli *Client) SecretList(ctx context.Context, options SecretListOptions) ([]swarm.Secret, error) {
query := url.Values{}
// SecretListOptions holds parameters to list secrets
type SecretListOptions struct {
Filters Filters
}
// SecretListResult holds the result from the [client.SecretList] method.
type SecretListResult struct {
Items []swarm.Secret
}
// SecretList returns the list of secrets.
func (cli *Client) SecretList(ctx context.Context, options SecretListOptions) (SecretListResult, error) {
query := url.Values{}
options.Filters.updateURLValues(query)
resp, err := cli.get(ctx, "/secrets", query, nil)
defer ensureReaderClosed(resp)
if err != nil {
return nil, err
return SecretListResult{}, err
}
var secrets []swarm.Secret
err = json.NewDecoder(resp.Body).Decode(&secrets)
return secrets, err
var out SecretListResult
err = json.NewDecoder(resp.Body).Decode(&out.Items)
if err != nil {
return SecretListResult{}, err
}
return out, nil
}

View File

@@ -1,6 +0,0 @@
package client
// SecretListOptions holds parameters to list secrets
type SecretListOptions struct {
Filters Filters
}

View File

@@ -75,8 +75,8 @@ func TestSecretList(t *testing.T) {
}))
assert.NilError(t, err)
secrets, err := client.SecretList(context.Background(), listCase.options)
res, err := client.SecretList(context.Background(), listCase.options)
assert.NilError(t, err)
assert.Check(t, is.Len(secrets, 2))
assert.Check(t, is.Len(res.Items, 2))
}
}

View File

@@ -2,13 +2,24 @@ package client
import "context"
type SecretRemoveOptions struct {
// Add future optional parameters here
}
type SecretRemoveResult struct {
// Add future fields here
}
// SecretRemove removes a secret.
func (cli *Client) SecretRemove(ctx context.Context, id string) error {
func (cli *Client) SecretRemove(ctx context.Context, id string, options SecretRemoveOptions) (SecretRemoveResult, error) {
id, err := trimID("secret", id)
if err != nil {
return err
return SecretRemoveResult{}, err
}
resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil)
defer ensureReaderClosed(resp)
return err
if err != nil {
return SecretRemoveResult{}, err
}
return SecretRemoveResult{}, nil
}

View File

@@ -16,14 +16,14 @@ func TestSecretRemoveError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
err = client.SecretRemove(context.Background(), "secret_id")
_, err = client.SecretRemove(context.Background(), "secret_id", SecretRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.SecretRemove(context.Background(), "")
_, err = client.SecretRemove(context.Background(), "", SecretRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.SecretRemove(context.Background(), " ")
_, err = client.SecretRemove(context.Background(), " ", SecretRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -42,6 +42,6 @@ func TestSecretRemove(t *testing.T) {
}))
assert.NilError(t, err)
err = client.SecretRemove(context.Background(), "secret_id")
_, err = client.SecretRemove(context.Background(), "secret_id", SecretRemoveOptions{})
assert.NilError(t, err)
}

View File

@@ -7,15 +7,26 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// SecretUpdateOptions holds options for updating a secret.
type SecretUpdateOptions struct {
Version swarm.Version
Spec swarm.SecretSpec
}
type SecretUpdateResult struct{}
// SecretUpdate attempts to update a secret.
func (cli *Client) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error {
func (cli *Client) SecretUpdate(ctx context.Context, id string, options SecretUpdateOptions) (SecretUpdateResult, error) {
id, err := trimID("secret", id)
if err != nil {
return err
return SecretUpdateResult{}, err
}
query := url.Values{}
query.Set("version", version.String())
resp, err := cli.post(ctx, "/secrets/"+id+"/update", query, secret, nil)
query.Set("version", options.Version.String())
resp, err := cli.post(ctx, "/secrets/"+id+"/update", query, options.Spec, nil)
defer ensureReaderClosed(resp)
return err
if err != nil {
return SecretUpdateResult{}, err
}
return SecretUpdateResult{}, nil
}

View File

@@ -8,7 +8,6 @@ import (
"testing"
cerrdefs "github.com/containerd/errdefs"
"github.com/moby/moby/api/types/swarm"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@@ -17,14 +16,14 @@ func TestSecretUpdateError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
err = client.SecretUpdate(context.Background(), "secret_id", swarm.Version{}, swarm.SecretSpec{})
_, err = client.SecretUpdate(context.Background(), "secret_id", SecretUpdateOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.SecretUpdate(context.Background(), "", swarm.Version{}, swarm.SecretSpec{})
_, err = client.SecretUpdate(context.Background(), "", SecretUpdateOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.SecretUpdate(context.Background(), " ", swarm.Version{}, swarm.SecretSpec{})
_, err = client.SecretUpdate(context.Background(), " ", SecretUpdateOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -43,6 +42,6 @@ func TestSecretUpdate(t *testing.T) {
}))
assert.NilError(t, err)
err = client.SecretUpdate(context.Background(), "secret_id", swarm.Version{}, swarm.SecretSpec{})
_, err = client.SecretUpdate(context.Background(), "secret_id", SecretUpdateOptions{})
assert.NilError(t, err)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/moby/moby/v2/integration-cli/checker"
"github.com/moby/moby/v2/internal/testutil"
"gotest.tools/v3/assert"
@@ -71,16 +72,20 @@ func (s *DockerSwarmSuite) TestServiceCreateMountVolume(c *testing.T) {
func (s *DockerSwarmSuite) TestServiceCreateWithSecretSimple(c *testing.T) {
ctx := testutil.GetContext(c)
d := s.AddDaemon(ctx, c, true, true)
apiClient := d.NewClientT(c)
serviceName := "test-service-secret"
testName := "test_secret"
id := d.CreateSecret(c, swarm.SecretSpec{
scr, err := apiClient.SecretCreate(ctx, client.SecretCreateOptions{
Spec: swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: testName,
},
Data: []byte("TESTINGDATA"),
},
})
assert.Assert(c, id != "", "secrets: %s", id)
assert.NilError(c, err)
assert.Assert(c, scr.ID != "", "secrets: %s", scr.ID)
out, err := d.Cmd("service", "create", "--detach", "--no-resolve-image", "--name", serviceName, "--secret", testName, "busybox", "top")
assert.NilError(c, err, out)
@@ -100,7 +105,8 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecretSimple(c *testing.T) {
out, err = d.Cmd("service", "rm", serviceName)
assert.NilError(c, err, out)
d.DeleteSecret(c, testName)
_, err = apiClient.SecretRemove(c.Context(), testName, client.SecretRemoveOptions{})
assert.NilError(c, err)
}
func (s *DockerSwarmSuite) TestServiceCreateWithSecretSourceTargetPaths(c *testing.T) {
@@ -116,15 +122,18 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecretSourceTargetPaths(c *testi
var secretFlags []string
apiClient := d.NewClientT(c)
for testName, testTarget := range testPaths {
id := d.CreateSecret(c, swarm.SecretSpec{
scr, err := apiClient.SecretCreate(ctx, client.SecretCreateOptions{
Spec: swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: testName,
},
Data: []byte("TESTINGDATA " + testName + " " + testTarget),
},
})
assert.Assert(c, id != "", "secrets: %s", id)
assert.NilError(c, err)
assert.Assert(c, scr.ID != "", "secrets: %s", scr.ID)
secretFlags = append(secretFlags, "--secret", fmt.Sprintf("source=%s,target=%s", testName, testTarget))
}
@@ -174,13 +183,17 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecretReferencedTwice(c *testing
ctx := testutil.GetContext(c)
d := s.AddDaemon(ctx, c, true, true)
id := d.CreateSecret(c, swarm.SecretSpec{
apiClient := d.NewClientT(c)
scr, err := apiClient.SecretCreate(ctx, client.SecretCreateOptions{
Spec: swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: "mysecret",
},
Data: []byte("TESTINGDATA"),
},
})
assert.Assert(c, id != "", "secrets: %s", id)
assert.NilError(c, err)
assert.Assert(c, scr.ID != "", "secrets: %s", scr.ID)
serviceName := "svc"
out, err := d.Cmd("service", "create", "--detach", "--no-resolve-image", "--name", serviceName, "--secret", "source=mysecret,target=target1", "--secret", "source=mysecret,target=target2", "busybox", "top")

View File

@@ -21,6 +21,7 @@ import (
"github.com/cloudflare/cfssl/helpers"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/client"
"github.com/moby/moby/v2/daemon/libnetwork/driverapi"
"github.com/moby/moby/v2/daemon/libnetwork/ipamapi"
remoteipam "github.com/moby/moby/v2/daemon/libnetwork/ipams/remote/api"
@@ -1985,20 +1986,27 @@ func (s *DockerSwarmSuite) TestSwarmClusterEventsNetwork(c *testing.T) {
func (s *DockerSwarmSuite) TestSwarmClusterEventsSecret(c *testing.T) {
ctx := testutil.GetContext(c)
d := s.AddDaemon(ctx, c, true, true)
apiClient := d.NewClientT(c)
testName := "test_secret"
id := d.CreateSecret(c, swarm.SecretSpec{
scr, err := apiClient.SecretCreate(ctx, client.SecretCreateOptions{
Spec: swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: testName,
},
Data: []byte("TESTINGDATA"),
},
})
assert.Assert(c, id != "", "secrets: %s", id)
assert.NilError(c, err)
assert.Assert(c, scr.ID != "", "secrets: %s", scr.ID)
id := scr.ID
waitForEvent(c, d, "0", "-f scope=swarm", "secret create "+id, defaultRetryCount)
t1 := daemonUnixTime(c)
d.DeleteSecret(c, id)
_, err = apiClient.SecretRemove(c.Context(), id, client.SecretRemoveOptions{})
assert.NilError(c, err)
// filtered by secret
waitForEvent(c, d, t1, "-f type=secret", "secret remove "+id, defaultRetryCount)
}

View File

@@ -236,7 +236,9 @@ func TestTemplatedConfig(t *testing.T) {
},
Data: []byte("this is a secret"),
}
referencedSecret, err := c.SecretCreate(ctx, referencedSecretSpec)
referencedSecret, err := c.SecretCreate(ctx, client.SecretCreateOptions{
Spec: referencedSecretSpec,
})
assert.Check(t, err)
referencedConfigName := "referencedconfig-" + t.Name()

View File

@@ -32,14 +32,14 @@ func TestSecretInspect(t *testing.T) {
testName := t.Name()
secretID := createSecret(ctx, t, c, testName, []byte("TESTINGDATA"), nil)
insp, body, err := c.SecretInspectWithRaw(ctx, secretID)
result, err := c.SecretInspect(ctx, secretID, client.SecretInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(insp.Spec.Name, testName))
assert.Check(t, is.Equal(result.Secret.Spec.Name, testName))
var secret swarmtypes.Secret
err = json.Unmarshal(body, &secret)
err = json.Unmarshal(result.Raw, &secret)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(secret, insp))
assert.Check(t, is.DeepEqual(secret, result.Secret))
}
func TestSecretList(t *testing.T) {
@@ -51,12 +51,12 @@ func TestSecretList(t *testing.T) {
c := d.NewClientT(t)
defer c.Close()
configs, err := c.SecretList(ctx, client.SecretListOptions{})
result, err := c.SecretList(ctx, client.SecretListOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(len(configs), 0))
assert.Check(t, is.Equal(len(result.Items), 0))
testName0 := "test0_" + t.Name()
testName1 := "test1_" + t.Name()
testName0 := "test0-" + t.Name()
testName1 := "test1-" + t.Name()
testNames := []string{testName0, testName1}
sort.Strings(testNames)
@@ -67,58 +67,66 @@ func TestSecretList(t *testing.T) {
secret1ID := createSecret(ctx, t, c, testName1, []byte("TESTINGDATA1"), map[string]string{"type": "production"})
// test by `secret ls`
entries, err := c.SecretList(ctx, client.SecretListOptions{})
res, err := c.SecretList(ctx, client.SecretListOptions{})
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(secretNamesFromList(entries), testNames))
assert.Check(t, is.DeepEqual(namesFromList(res.Items), testNames))
testCases := []struct {
desc string
filters client.Filters
expected []string
}{
// test filter by name `secret ls --filter name=xxx`
{
desc: "test filter by name",
filters: make(client.Filters).Add("name", testName0),
expected: []string{testName0},
},
// test filter by id `secret ls --filter id=xxx`
{
desc: "test filter by id",
filters: make(client.Filters).Add("id", secret1ID),
expected: []string{testName1},
},
// test filter by label `secret ls --filter label=xxx`
{
desc: "test filter by label key only",
filters: make(client.Filters).Add("label", "type"),
expected: testNames,
},
{
desc: "test filter by label key=value " + testName0,
filters: make(client.Filters).Add("label", "type=test"),
expected: []string{testName0},
},
{
desc: "test filter by label key=value " + testName1,
filters: make(client.Filters).Add("label", "type=production"),
expected: []string{testName1},
},
}
for _, tc := range testCases {
entries, err = c.SecretList(ctx, client.SecretListOptions{
t.Run(tc.desc, func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
res, err = c.SecretList(ctx, client.SecretListOptions{
Filters: tc.filters,
})
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(secretNamesFromList(entries), tc.expected))
assert.Check(t, is.DeepEqual(namesFromList(res.Items), tc.expected))
})
}
}
func createSecret(ctx context.Context, t *testing.T, client client.APIClient, name string, data []byte, labels map[string]string) string {
secret, err := client.SecretCreate(ctx, swarmtypes.SecretSpec{
func createSecret(ctx context.Context, t *testing.T, apiClient client.APIClient, name string, data []byte, labels map[string]string) string {
result, err := apiClient.SecretCreate(ctx, client.SecretCreateOptions{
Spec: swarmtypes.SecretSpec{
Annotations: swarmtypes.Annotations{
Name: name,
Labels: labels,
},
Data: data,
},
})
assert.NilError(t, err)
assert.Check(t, secret.ID != "")
return secret.ID
assert.Check(t, result.ID != "")
return result.ID
}
func TestSecretsCreateAndDelete(t *testing.T) {
@@ -134,23 +142,25 @@ func TestSecretsCreateAndDelete(t *testing.T) {
secretID := createSecret(ctx, t, c, testName, []byte("TESTINGDATA"), nil)
// create an already existing secret, daemon should return a status code of 409
_, err := c.SecretCreate(ctx, swarmtypes.SecretSpec{
_, err := c.SecretCreate(ctx, client.SecretCreateOptions{
Spec: swarmtypes.SecretSpec{
Annotations: swarmtypes.Annotations{
Name: testName,
},
Data: []byte("TESTINGDATA"),
},
})
assert.Check(t, cerrdefs.IsConflict(err))
assert.Check(t, is.ErrorContains(err, testName))
err = c.SecretRemove(ctx, secretID)
_, err = c.SecretRemove(ctx, secretID, client.SecretRemoveOptions{})
assert.NilError(t, err)
_, _, err = c.SecretInspectWithRaw(ctx, secretID)
_, err = c.SecretInspect(ctx, secretID, client.SecretInspectOptions{})
assert.Check(t, cerrdefs.IsNotFound(err))
assert.Check(t, is.ErrorContains(err, secretID))
err = c.SecretRemove(ctx, "non-existing")
_, err = c.SecretRemove(ctx, "non-existing", client.SecretRemoveOptions{})
assert.Check(t, cerrdefs.IsNotFound(err))
assert.Check(t, is.ErrorContains(err, "non-existing"))
@@ -160,12 +170,12 @@ func TestSecretsCreateAndDelete(t *testing.T) {
"key2": "value2",
})
insp, _, err := c.SecretInspectWithRaw(ctx, secretID)
result, err := c.SecretInspect(ctx, secretID, client.SecretInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(insp.Spec.Name, testName))
assert.Check(t, is.Equal(len(insp.Spec.Labels), 2))
assert.Check(t, is.Equal(insp.Spec.Labels["key1"], "value1"))
assert.Check(t, is.Equal(insp.Spec.Labels["key2"], "value2"))
assert.Check(t, is.Equal(result.Secret.Spec.Name, testName))
assert.Check(t, is.Equal(len(result.Secret.Spec.Labels), 2))
assert.Check(t, is.Equal(result.Secret.Spec.Labels["key1"], "value1"))
assert.Check(t, is.Equal(result.Secret.Spec.Labels["key2"], "value2"))
}
func TestSecretsUpdate(t *testing.T) {
@@ -180,41 +190,53 @@ func TestSecretsUpdate(t *testing.T) {
testName := "test_secret_" + t.Name()
secretID := createSecret(ctx, t, c, testName, []byte("TESTINGDATA"), nil)
insp, _, err := c.SecretInspectWithRaw(ctx, secretID)
insp, err := c.SecretInspect(ctx, secretID, client.SecretInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(insp.ID, secretID))
assert.Check(t, is.Equal(insp.Secret.ID, secretID))
// test UpdateSecret with full ID
insp.Spec.Labels = map[string]string{"test": "test1"}
err = c.SecretUpdate(ctx, secretID, insp.Version, insp.Spec)
insp.Secret.Spec.Labels = map[string]string{"test": "test1"}
_, err = c.SecretUpdate(ctx, secretID, client.SecretUpdateOptions{
Version: insp.Secret.Version,
Spec: insp.Secret.Spec,
})
assert.NilError(t, err)
insp, _, err = c.SecretInspectWithRaw(ctx, secretID)
insp, err = c.SecretInspect(ctx, secretID, client.SecretInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(insp.Spec.Labels["test"], "test1"))
assert.Check(t, is.Equal(insp.Secret.Spec.Labels["test"], "test1"))
// test UpdateSecret with full name
insp.Spec.Labels = map[string]string{"test": "test2"}
err = c.SecretUpdate(ctx, testName, insp.Version, insp.Spec)
insp.Secret.Spec.Labels = map[string]string{"test": "test2"}
_, err = c.SecretUpdate(ctx, testName, client.SecretUpdateOptions{
Version: insp.Secret.Version,
Spec: insp.Secret.Spec,
})
assert.NilError(t, err)
insp, _, err = c.SecretInspectWithRaw(ctx, secretID)
insp, err = c.SecretInspect(ctx, secretID, client.SecretInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(insp.Spec.Labels["test"], "test2"))
assert.Check(t, is.Equal(insp.Secret.Spec.Labels["test"], "test2"))
// test UpdateSecret with prefix ID
insp.Spec.Labels = map[string]string{"test": "test3"}
err = c.SecretUpdate(ctx, secretID[:1], insp.Version, insp.Spec)
insp.Secret.Spec.Labels = map[string]string{"test": "test3"}
_, err = c.SecretUpdate(ctx, secretID[:1], client.SecretUpdateOptions{
Version: insp.Secret.Version,
Spec: insp.Secret.Spec,
})
assert.NilError(t, err)
insp, _, err = c.SecretInspectWithRaw(ctx, secretID)
insp, err = c.SecretInspect(ctx, secretID, client.SecretInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(insp.Spec.Labels["test"], "test3"))
assert.Check(t, is.Equal(insp.Secret.Spec.Labels["test"], "test3"))
// test UpdateSecret in updating Data which is not supported in daemon
// this test will produce an error in func UpdateSecret
insp.Spec.Data = []byte("TESTINGDATA2")
err = c.SecretUpdate(ctx, secretID, insp.Version, insp.Spec)
insp.Secret.Spec.Data = []byte("TESTINGDATA2")
_, err = c.SecretUpdate(ctx, secretID, client.SecretUpdateOptions{
Version: insp.Secret.Version,
Spec: insp.Secret.Spec,
})
assert.Check(t, cerrdefs.IsInvalidArgument(err))
assert.Check(t, is.ErrorContains(err, "only updates to Labels are allowed"))
}
@@ -236,7 +258,9 @@ func TestTemplatedSecret(t *testing.T) {
},
Data: []byte("this is a secret"),
}
referencedSecret, err := c.SecretCreate(ctx, referencedSecretSpec)
referencedSecret, err := c.SecretCreate(ctx, client.SecretCreateOptions{
Spec: referencedSecretSpec,
})
assert.Check(t, err)
referencedConfigName := "referencedconfig_" + t.Name()
@@ -259,12 +283,15 @@ func TestTemplatedSecret(t *testing.T) {
Templating: &swarmtypes.Driver{
Name: "golang",
},
Data: []byte("SERVICE_NAME={{.Service.Name}}\n" +
"{{secret \"referencedsecrettarget\"}}\n" +
"{{config \"referencedconfigtarget\"}}\n"),
Data: []byte(`SERVICE_NAME={{.Service.Name}}
{{secret "referencedsecrettarget"}}
{{config "referencedconfigtarget"}}
`),
}
templatedSecret, err := c.SecretCreate(ctx, secretSpec)
templatedSecret, err := c.SecretCreate(ctx, client.SecretCreateOptions{
Spec: secretSpec,
})
assert.Check(t, err)
const serviceName = "svc_templated_secret"
@@ -356,39 +383,39 @@ func TestSecretCreateResolve(t *testing.T) {
fakeName := secretID
fakeID := createSecret(ctx, t, c, fakeName, []byte("fake foo"), nil)
entries, err := c.SecretList(ctx, client.SecretListOptions{})
res, err := c.SecretList(ctx, client.SecretListOptions{})
assert.NilError(t, err)
assert.Check(t, is.Contains(secretNamesFromList(entries), testName))
assert.Check(t, is.Contains(secretNamesFromList(entries), fakeName))
assert.Check(t, is.Contains(namesFromList(res.Items), testName))
assert.Check(t, is.Contains(namesFromList(res.Items), fakeName))
err = c.SecretRemove(ctx, secretID)
_, err = c.SecretRemove(ctx, secretID, client.SecretRemoveOptions{})
assert.NilError(t, err)
// Fake one will remain
entries, err = c.SecretList(ctx, client.SecretListOptions{})
res, err = c.SecretList(ctx, client.SecretListOptions{})
assert.NilError(t, err)
assert.Assert(t, is.DeepEqual(secretNamesFromList(entries), []string{fakeName}))
assert.Assert(t, is.DeepEqual(namesFromList(res.Items), []string{fakeName}))
// Remove based on name prefix of the fake one should not work
// as search is only done based on:
// - Full ID
// - Full Name
// - Partial ID (prefix)
err = c.SecretRemove(ctx, fakeName[:5])
_, err = c.SecretRemove(ctx, fakeName[:5], client.SecretRemoveOptions{})
assert.Assert(t, err != nil)
entries, err = c.SecretList(ctx, client.SecretListOptions{})
res, err = c.SecretList(ctx, client.SecretListOptions{})
assert.NilError(t, err)
assert.Assert(t, is.DeepEqual(secretNamesFromList(entries), []string{fakeName}))
assert.Assert(t, is.DeepEqual(namesFromList(res.Items), []string{fakeName}))
// Remove based on ID prefix of the fake one should succeed
err = c.SecretRemove(ctx, fakeID[:5])
_, err = c.SecretRemove(ctx, fakeID[:5], client.SecretRemoveOptions{})
assert.NilError(t, err)
entries, err = c.SecretList(ctx, client.SecretListOptions{})
res, err = c.SecretList(ctx, client.SecretListOptions{})
assert.NilError(t, err)
assert.Assert(t, is.Equal(0, len(entries)))
assert.Assert(t, is.Equal(0, len(res.Items)))
}
func secretNamesFromList(entries []swarmtypes.Secret) []string {
func namesFromList(entries []swarmtypes.Secret) []string {
var values []string
for _, entry := range entries {
values = append(values, entry.Spec.Name)

View File

@@ -199,11 +199,13 @@ func TestCreateServiceSecretFileMode(t *testing.T) {
defer apiClient.Close()
secretName := "TestSecret_" + t.Name()
secretResp, err := apiClient.SecretCreate(ctx, swarmtypes.SecretSpec{
secretResp, err := apiClient.SecretCreate(ctx, client.SecretCreateOptions{
Spec: swarmtypes.SecretSpec{
Annotations: swarmtypes.Annotations{
Name: secretName,
},
Data: []byte("TESTSECRET"),
},
})
assert.NilError(t, err)
@@ -242,7 +244,7 @@ func TestCreateServiceSecretFileMode(t *testing.T) {
assert.NilError(t, err)
poll.WaitOn(t, swarm.NoTasksForService(ctx, apiClient, serviceID), swarm.ServicePoll)
err = apiClient.SecretRemove(ctx, secretName)
_, err = apiClient.SecretRemove(ctx, secretName, client.SecretRemoveOptions{})
assert.NilError(t, err)
}

View File

@@ -83,11 +83,13 @@ func TestServiceUpdateSecrets(t *testing.T) {
secretName := "TestSecret_" + t.Name()
secretTarget := "targetName"
secretResp, err := apiClient.SecretCreate(ctx, swarmtypes.SecretSpec{
secretResp, err := apiClient.SecretCreate(ctx, client.SecretCreateOptions{
Spec: swarmtypes.SecretSpec{
Annotations: swarmtypes.Annotations{
Name: secretName,
},
Data: []byte("TESTINGDATA"),
},
})
assert.NilError(t, err)
assert.Check(t, secretResp.ID != "")

View File

@@ -1,74 +0,0 @@
package daemon
import (
"context"
"testing"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
)
// SecretConstructor defines a swarm secret constructor
type SecretConstructor func(*swarm.Secret)
// CreateSecret creates a secret given the specified spec
func (d *Daemon) CreateSecret(t testing.TB, secretSpec swarm.SecretSpec) string {
t.Helper()
cli := d.NewClientT(t)
defer cli.Close()
scr, err := cli.SecretCreate(context.Background(), secretSpec)
assert.NilError(t, err)
return scr.ID
}
// ListSecrets returns the list of the current swarm secrets
func (d *Daemon) ListSecrets(t testing.TB) []swarm.Secret {
t.Helper()
cli := d.NewClientT(t)
defer cli.Close()
secrets, err := cli.SecretList(context.Background(), client.SecretListOptions{})
assert.NilError(t, err)
return secrets
}
// GetSecret returns a swarm secret identified by the specified id
func (d *Daemon) GetSecret(t testing.TB, id string) *swarm.Secret {
t.Helper()
cli := d.NewClientT(t)
defer cli.Close()
secret, _, err := cli.SecretInspectWithRaw(context.Background(), id)
assert.NilError(t, err)
return &secret
}
// DeleteSecret removes the swarm secret identified by the specified id
func (d *Daemon) DeleteSecret(t testing.TB, id string) {
t.Helper()
cli := d.NewClientT(t)
defer cli.Close()
err := cli.SecretRemove(context.Background(), id)
assert.NilError(t, err)
}
// UpdateSecret updates the swarm secret identified by the specified id
// Currently, only label update is supported.
func (d *Daemon) UpdateSecret(t testing.TB, id string, f ...SecretConstructor) {
t.Helper()
cli := d.NewClientT(t)
defer cli.Close()
secret := d.GetSecret(t, id)
for _, fn := range f {
fn(secret)
}
err := cli.SecretUpdate(context.Background(), secret.ID, secret.Version, secret.Spec)
assert.NilError(t, err)
}

View File

@@ -203,11 +203,11 @@ type VolumeAPIClient interface {
// SecretAPIClient defines API client methods for secrets
type SecretAPIClient interface {
SecretList(ctx context.Context, options SecretListOptions) ([]swarm.Secret, error)
SecretCreate(ctx context.Context, secret swarm.SecretSpec) (swarm.SecretCreateResponse, error)
SecretRemove(ctx context.Context, id string) error
SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error
SecretList(ctx context.Context, options SecretListOptions) (SecretListResult, error)
SecretCreate(ctx context.Context, options SecretCreateOptions) (SecretCreateResult, error)
SecretRemove(ctx context.Context, id string, options SecretRemoveOptions) (SecretRemoveResult, error)
SecretInspect(ctx context.Context, id string, options SecretInspectOptions) (SecretInspectResult, error)
SecretUpdate(ctx context.Context, id string, options SecretUpdateOptions) (SecretUpdateResult, error)
}
// ConfigAPIClient defines API client methods for configs

View File

@@ -7,15 +7,28 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// SecretCreate creates a new secret.
func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (swarm.SecretCreateResponse, error) {
resp, err := cli.post(ctx, "/secrets/create", nil, secret, nil)
defer ensureReaderClosed(resp)
if err != nil {
return swarm.SecretCreateResponse{}, err
// SecretCreateOptions holds options for creating a secret.
type SecretCreateOptions struct {
Spec swarm.SecretSpec
}
var response swarm.SecretCreateResponse
err = json.NewDecoder(resp.Body).Decode(&response)
return response, err
// SecretCreateResult holds the result from the [Client.SecretCreate] method.
type SecretCreateResult struct {
ID string
}
// SecretCreate creates a new secret.
func (cli *Client) SecretCreate(ctx context.Context, options SecretCreateOptions) (SecretCreateResult, error) {
resp, err := cli.post(ctx, "/secrets/create", nil, options.Spec, nil)
defer ensureReaderClosed(resp)
if err != nil {
return SecretCreateResult{}, err
}
var out swarm.ConfigCreateResponse
err = json.NewDecoder(resp.Body).Decode(&out)
if err != nil {
return SecretCreateResult{}, err
}
return SecretCreateResult{ID: out.ID}, nil
}

View File

@@ -1,34 +1,35 @@
package client
import (
"bytes"
"context"
"encoding/json"
"io"
"github.com/moby/moby/api/types/swarm"
)
// SecretInspectWithRaw returns the secret information with raw data
func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
// SecretInspectOptions holds options for inspecting a secret.
type SecretInspectOptions struct {
// Add future optional parameters here
}
// SecretInspectResult holds the result from the [Client.SecretInspect]. method.
type SecretInspectResult struct {
Secret swarm.Secret
Raw []byte
}
// SecretInspect returns the secret information with raw data.
func (cli *Client) SecretInspect(ctx context.Context, id string, options SecretInspectOptions) (SecretInspectResult, error) {
id, err := trimID("secret", id)
if err != nil {
return swarm.Secret{}, nil, err
return SecretInspectResult{}, err
}
resp, err := cli.get(ctx, "/secrets/"+id, nil, nil)
defer ensureReaderClosed(resp)
if err != nil {
return swarm.Secret{}, nil, err
return SecretInspectResult{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return swarm.Secret{}, nil, err
}
var secret swarm.Secret
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&secret)
return secret, body, err
var out SecretInspectResult
out.Raw, err = decodeWithRaw(resp, &out.Secret)
return out, err
}

View File

@@ -8,18 +8,31 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// SecretList returns the list of secrets.
func (cli *Client) SecretList(ctx context.Context, options SecretListOptions) ([]swarm.Secret, error) {
query := url.Values{}
// SecretListOptions holds parameters to list secrets
type SecretListOptions struct {
Filters Filters
}
// SecretListResult holds the result from the [client.SecretList] method.
type SecretListResult struct {
Items []swarm.Secret
}
// SecretList returns the list of secrets.
func (cli *Client) SecretList(ctx context.Context, options SecretListOptions) (SecretListResult, error) {
query := url.Values{}
options.Filters.updateURLValues(query)
resp, err := cli.get(ctx, "/secrets", query, nil)
defer ensureReaderClosed(resp)
if err != nil {
return nil, err
return SecretListResult{}, err
}
var secrets []swarm.Secret
err = json.NewDecoder(resp.Body).Decode(&secrets)
return secrets, err
var out SecretListResult
err = json.NewDecoder(resp.Body).Decode(&out.Items)
if err != nil {
return SecretListResult{}, err
}
return out, nil
}

View File

@@ -1,6 +0,0 @@
package client
// SecretListOptions holds parameters to list secrets
type SecretListOptions struct {
Filters Filters
}

View File

@@ -2,13 +2,24 @@ package client
import "context"
type SecretRemoveOptions struct {
// Add future optional parameters here
}
type SecretRemoveResult struct {
// Add future fields here
}
// SecretRemove removes a secret.
func (cli *Client) SecretRemove(ctx context.Context, id string) error {
func (cli *Client) SecretRemove(ctx context.Context, id string, options SecretRemoveOptions) (SecretRemoveResult, error) {
id, err := trimID("secret", id)
if err != nil {
return err
return SecretRemoveResult{}, err
}
resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil)
defer ensureReaderClosed(resp)
return err
if err != nil {
return SecretRemoveResult{}, err
}
return SecretRemoveResult{}, nil
}

View File

@@ -7,15 +7,26 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// SecretUpdateOptions holds options for updating a secret.
type SecretUpdateOptions struct {
Version swarm.Version
Spec swarm.SecretSpec
}
type SecretUpdateResult struct{}
// SecretUpdate attempts to update a secret.
func (cli *Client) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error {
func (cli *Client) SecretUpdate(ctx context.Context, id string, options SecretUpdateOptions) (SecretUpdateResult, error) {
id, err := trimID("secret", id)
if err != nil {
return err
return SecretUpdateResult{}, err
}
query := url.Values{}
query.Set("version", version.String())
resp, err := cli.post(ctx, "/secrets/"+id+"/update", query, secret, nil)
query.Set("version", options.Version.String())
resp, err := cli.post(ctx, "/secrets/"+id+"/update", query, options.Spec, nil)
defer ensureReaderClosed(resp)
return err
if err != nil {
return SecretUpdateResult{}, err
}
return SecretUpdateResult{}, nil
}