feat: Implement docker cluster volume (#793)

* feat: Implement docker cluster volume

* fix: linter findings

* fix: Make cluster attribute updateable
This commit is contained in:
Martin 2025-09-25 23:32:53 +02:00 committed by GitHub
parent b78f540ded
commit e2c8d0b73a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 879 additions and 5 deletions

View file

@ -23,6 +23,7 @@ resource "docker_volume" "shared_volume" {
### Optional
- `cluster` (Block List, Max: 1) Cluster-specific options for volume creation. Only works if the Docker daemon is running in swarm mode and is the swarm manager. (see [below for nested schema](#nestedblock--cluster))
- `driver` (String) Driver type for the volume. Defaults to `local`.
- `driver_opts` (Map of String) Options specific to the driver.
- `labels` (Block Set) User-defined key/value metadata (see [below for nested schema](#nestedblock--labels))
@ -33,6 +34,26 @@ resource "docker_volume" "shared_volume" {
- `id` (String) The ID of this resource.
- `mountpoint` (String) The mountpoint of the volume.
<a id="nestedblock--cluster"></a>
### Nested Schema for `cluster`
Optional:
- `availability` (String) Availability of the volume. Can be `active` (default), `pause`, or `drain`.
- `group` (String) Cluster Volume group
- `limit_bytes` (String) Minimum size of the Cluster Volume in human readable memory bytes (like 128MiB, 2GiB, etc). Must be in format of KiB, MiB, Gib, Tib or PiB.
- `required_bytes` (String) Maximum size of the Cluster Volume in human readable memory bytes (like 128MiB, 2GiB, etc). Must be in format of KiB, MiB, Gib, Tib or PiB.
- `scope` (String) The scope of the volume. Can be `single` (default) or `multi`.
- `sharing` (String) The sharing mode. Can be `none` (default), `readonly`, `onewriter` or `all`.
- `topology_preferred` (String) A topology that the Cluster Volume would be preferred in
- `topology_required` (String) A topology that the Cluster Volume must be accessible from
- `type` (String) Cluster Volume access type. Can be `mount` or `block` (default).
Read-Only:
- `id` (String) The ID of the cluster volume.
<a id="nestedblock--labels"></a>
### Nested Schema for `labels`

37
go.mod
View file

@ -33,6 +33,14 @@ require (
require (
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // indirect
cel.dev/expr v0.20.0 // indirect
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.15.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.2.2 // indirect
cloud.google.com/go/monitoring v1.21.2 // indirect
cloud.google.com/go/storage v1.49.0 // indirect
github.com/4meepo/tagalign v1.4.2 // indirect
github.com/Abirdcfly/dupword v0.1.3 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
@ -44,9 +52,14 @@ require (
github.com/Crocmagnon/fatcontext v0.7.1 // indirect
github.com/Djarvur/go-err113 v0.1.0 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
@ -57,10 +70,12 @@ require (
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/alingse/nilnesserr v0.1.2 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/ashanbrown/forbidigo v1.6.0 // indirect
github.com/ashanbrown/makezero v1.2.0 // indirect
github.com/aws/aws-sdk-go v1.37.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
@ -75,6 +90,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/bkielbasa/cyclop v1.2.3 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
@ -92,6 +108,7 @@ require (
github.com/chavacava/garif v0.1.0 // indirect
github.com/ckaznocha/intrange v0.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
github.com/containerd/containerd/api v1.8.0 // indirect
github.com/containerd/containerd/v2 v2.0.5 // indirect
github.com/containerd/continuity v0.4.5 // indirect
@ -108,6 +125,8 @@ require (
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
@ -119,6 +138,7 @@ require (
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.9 // indirect
github.com/go-critic/go-critic v0.12.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
@ -136,6 +156,7 @@ require (
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
github.com/golangci/go-printf-func-name v0.1.0 // indirect
@ -147,8 +168,11 @@ require (
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
@ -161,11 +185,13 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-getter v1.5.3 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.3 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
@ -176,6 +202,7 @@ require (
github.com/hashicorp/terraform-exec v0.23.0 // indirect
github.com/hashicorp/terraform-json v0.25.0 // indirect
github.com/hashicorp/terraform-plugin-go v0.27.0 // indirect
github.com/hashicorp/terraform-plugin-sdk v1.17.2 // indirect
github.com/hashicorp/terraform-registry-address v0.2.5 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
@ -187,6 +214,7 @@ require (
github.com/jgautheron/goconst v1.7.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jjti/go-spancheck v0.6.4 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/julz/importas v0.2.0 // indirect
@ -215,6 +243,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgechev/revive v1.7.0 // indirect
github.com/mitchellh/cli v1.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
@ -284,6 +313,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/viper v1.13.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
@ -300,6 +330,7 @@ require (
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/ultraware/funlen v0.2.0 // indirect
github.com/ultraware/whitespace v0.2.0 // indirect
github.com/uudashr/gocognit v1.2.0 // indirect
@ -316,11 +347,15 @@ require (
github.com/yuin/goldmark v1.7.7 // indirect
github.com/yuin/goldmark-meta v1.1.0 // indirect
github.com/zclconf/go-cty v1.16.3 // indirect
github.com/zclconf/go-cty-yaml v1.0.2 // indirect
github.com/zeebo/errs v1.4.0 // indirect
gitlab.com/bosi/decorder v0.4.2 // indirect
go-simpler.org/musttag v0.13.0 // indirect
go-simpler.org/sloglint v0.9.0 // indirect
go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
@ -348,7 +383,9 @@ require (
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.33.0 // indirect
google.golang.org/api v0.223.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
google.golang.org/grpc v1.72.1 // indirect

509
go.sum

File diff suppressed because it is too large Load diff

View file

@ -3,14 +3,19 @@ package provider
import (
"context"
"encoding/json"
"fmt"
"log"
"sort"
"strings"
"time"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/errdefs"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
const (
@ -26,6 +31,7 @@ func resourceDockerVolume() *schema.Resource {
CreateContext: resourceDockerVolumeCreate,
ReadContext: resourceDockerVolumeRead,
DeleteContext: resourceDockerVolumeDelete,
UpdateContext: resourceDockerVolumeUpdate,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
@ -63,6 +69,85 @@ func resourceDockerVolume() *schema.Resource {
Description: "The mountpoint of the volume.",
Computed: true,
},
"cluster": {
Type: schema.TypeList,
Description: "Cluster-specific options for volume creation. Only works if the Docker daemon is running in swarm mode and is the swarm manager.",
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Description: "The ID of the cluster volume.",
Computed: true,
},
"scope": {
Type: schema.TypeString,
Description: "The scope of the volume. Can be `single` (default) or `multi`.",
Optional: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{"single", "multi"}, false),
Default: "single",
},
"sharing": {
Type: schema.TypeString,
Description: "The sharing mode. Can be `none` (default), `readonly`, `onewriter` or `all`.",
Optional: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{"none", "readonly", "onewriter", "all"}, false),
Default: "none",
},
"type": {
Type: schema.TypeString,
Description: "Cluster Volume access type. Can be `mount` or `block` (default).",
Optional: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{"mount", "block"}, false),
Default: "block",
},
"topology_preferred": {
Type: schema.TypeString,
Description: "A topology that the Cluster Volume would be preferred in",
Optional: true,
ForceNew: true,
Default: "",
},
"topology_required": {
Type: schema.TypeString,
Description: "A topology that the Cluster Volume must be accessible from",
Optional: true,
ForceNew: true,
Default: "",
},
"required_bytes": {
Type: schema.TypeString,
Description: "Maximum size of the Cluster Volume in human readable memory bytes (like 128MiB, 2GiB, etc). Must be in format of KiB, MiB, Gib, Tib or PiB.",
Optional: true,
ForceNew: true,
},
"limit_bytes": {
Type: schema.TypeString,
Description: "Minimum size of the Cluster Volume in human readable memory bytes (like 128MiB, 2GiB, etc). Must be in format of KiB, MiB, Gib, Tib or PiB.",
Optional: true,
ForceNew: true,
},
"group": {
Type: schema.TypeString,
Description: "Cluster Volume group",
Optional: true,
ForceNew: true,
Default: "",
},
"availability": {
Type: schema.TypeString,
Description: "Availability of the volume. Can be `active` (default), `pause`, or `drain`.",
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"active", "pause", "drain"}, false),
Default: "active",
},
},
},
},
},
SchemaVersion: 1,
StateUpgraders: []schema.StateUpgrader{
@ -95,6 +180,96 @@ func resourceDockerVolumeCreate(ctx context.Context, d *schema.ResourceData, met
createOpts.DriverOpts = mapTypeMapValsToString(v.(map[string]interface{}))
}
// Handle cluster volume options
if v, ok := d.GetOk("cluster"); ok {
if client.ClientVersion() < "1.42" {
return diag.Errorf("You have supplied a 'cluster' block for your docker_volume resource. This is supported starting with client version '1.42', but you have %s", client.ClientVersion())
}
clusterList := v.([]interface{})
if len(clusterList) > 0 {
clusterConfig := clusterList[0].(map[string]interface{})
createOpts.ClusterVolumeSpec = &volume.ClusterVolumeSpec{
Group: clusterConfig["group"].(string),
AccessMode: &volume.AccessMode{
Scope: volume.Scope(clusterConfig["scope"].(string)),
Sharing: volume.SharingMode(clusterConfig["sharing"].(string)),
},
Availability: volume.Availability(clusterConfig["availability"].(string)),
}
switch clusterConfig["type"].(string) {
case "mount":
createOpts.ClusterVolumeSpec.AccessMode.MountVolume = &volume.TypeMount{}
case "block":
createOpts.ClusterVolumeSpec.AccessMode.BlockVolume = &volume.TypeBlock{}
}
vcr := &volume.CapacityRange{}
var memBytes opts.MemBytes
if r := clusterConfig["required_bytes"].(string); len(r) > 0 {
if err := memBytes.Set(r); err != nil {
return diag.Errorf("Invalid value for required_bytes: %s", err)
}
vcr.RequiredBytes = memBytes.Value()
fmt.Printf("[DEBUG] Required bytes set to %d\n", vcr.RequiredBytes)
}
if l := clusterConfig["limit_bytes"].(string); len(l) > 0 {
if err := memBytes.Set(l); err != nil {
return diag.Errorf("Invalid value for limit_bytes: %s", err)
}
vcr.LimitBytes = memBytes.Value()
fmt.Printf("[DEBUG] Limit bytes set to %d\n", vcr.LimitBytes)
}
createOpts.ClusterVolumeSpec.CapacityRange = vcr
if secretsList, ok := clusterConfig["secrets"].([]interface{}); ok && len(secretsList) > 0 {
secrets := make([]volume.Secret, len(secretsList))
for i, secretItem := range secretsList {
secret := secretItem.(map[string]interface{})
secrets[i] = volume.Secret{
Key: secret["key"].(string),
Secret: secret["secret"].(string),
}
}
createOpts.ClusterVolumeSpec.Secrets = secrets
sort.SliceStable(createOpts.ClusterVolumeSpec.Secrets, func(i, j int) bool {
return createOpts.ClusterVolumeSpec.Secrets[i].Key < createOpts.ClusterVolumeSpec.Secrets[j].Key
})
}
topology := &volume.TopologyRequirement{}
if topology_required, ok := clusterConfig["topology_required"].(string); ok && len(topology_required) > 0 {
segments := map[string]string{}
for _, segment := range strings.Split(topology_required, ",") {
k, v, _ := strings.Cut(segment, "=")
segments[k] = v
}
topology.Requisite = append(
topology.Requisite,
volume.Topology{Segments: segments},
)
}
if topology_preferred, ok := clusterConfig["topology_preferred"].(string); ok && len(topology_preferred) > 0 {
segments := map[string]string{}
for _, segment := range strings.Split(topology_preferred, ",") {
k, v, _ := strings.Cut(segment, "=")
segments[k] = v
}
topology.Preferred = append(
topology.Preferred,
volume.Topology{Segments: segments},
)
}
createOpts.ClusterVolumeSpec.AccessibilityRequirements = topology
}
}
var err error
var retVolume volume.Volume
retVolume, err = client.VolumeCreate(ctx, createOpts)
@ -114,7 +289,7 @@ func resourceDockerVolumeRead(ctx context.Context, d *schema.ResourceData, meta
if err != nil {
if errdefs.IsNotFound(err) {
log.Printf("[WARN] Volume with id '%s' not found, removing from state", d.Id())
log.Printf("[WARN] Volume with id `%s` not found, removing from state", d.Id())
d.SetId("")
return nil
}
@ -130,11 +305,56 @@ func resourceDockerVolumeRead(ctx context.Context, d *schema.ResourceData, meta
d.Set("driver_opts", volume.Options)
d.Set("mountpoint", volume.Mountpoint)
// check if volume.ClusterVolume is set
if volume.ClusterVolume != nil {
typeValue := "block"
if volume.ClusterVolume.Spec.AccessMode.MountVolume != nil {
typeValue = "mount"
}
// loop over volume.ClusterVolume.Spec.AccessibilityRequirements.Preferred
topologyPreferred := []string{}
for _, segment := range volume.ClusterVolume.Spec.AccessibilityRequirements.Preferred {
for key, value := range segment.Segments {
topologyPreferred = append(topologyPreferred, fmt.Sprintf("%s=%s", key, value))
}
}
topologyRequired := []string{}
for _, segment := range volume.ClusterVolume.Spec.AccessibilityRequirements.Requisite {
for key, value := range segment.Segments {
topologyRequired = append(topologyRequired, fmt.Sprintf("%s=%s", key, value))
}
}
d.Set("cluster", []interface{}{
map[string]interface{}{
"id": volume.ClusterVolume.ID,
"scope": volume.ClusterVolume.Spec.AccessMode.Scope,
"sharing": volume.ClusterVolume.Spec.AccessMode.Sharing,
"group": volume.ClusterVolume.Spec.Group,
"availability": volume.ClusterVolume.Spec.Availability,
"type": typeValue,
"required_bytes": func() string {
mb := opts.MemBytes(0)
mb = opts.MemBytes(volume.ClusterVolume.Spec.CapacityRange.RequiredBytes)
return mb.String()
}(),
"limit_bytes": func() string {
mb := opts.MemBytes(0)
mb = opts.MemBytes(volume.ClusterVolume.Spec.CapacityRange.LimitBytes)
return mb.String()
}(),
"topology_preferred": strings.Join(topologyPreferred, ","),
"topology_required": strings.Join(topologyRequired, ","),
},
})
}
return nil
}
func resourceDockerVolumeDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
log.Printf("[INFO] Waiting for volume: '%s' to get removed: max '%v seconds'", d.Id(), volumeReadRefreshTimeout)
log.Printf("[INFO] Waiting for volume: `%s` to get removed: max `%v seconds`", d.Id(), volumeReadRefreshTimeout)
stateConf := &retry.StateChangeConf{
Pending: []string{"in_use"},
@ -155,6 +375,44 @@ func resourceDockerVolumeDelete(ctx context.Context, d *schema.ResourceData, met
return nil
}
func resourceDockerVolumeUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
attrs := []string{
"cluster.availability",
}
for _, attr := range attrs {
if d.HasChange(attr) {
clusterList := d.Get("cluster").([]interface{})
if len(clusterList) > 0 {
clusterConfig := clusterList[0].(map[string]interface{})
client := meta.(*ProviderConfig).DockerClient
vol, _, err := client.VolumeInspectWithRaw(ctx, clusterConfig["id"].(string))
if err != nil {
return diag.FromErr(err)
}
if vol.ClusterVolume == nil || d.Get("cluster") == nil {
return diag.Errorf("Can only update cluster volumes")
}
vol.ClusterVolume.Spec.Availability = volume.Availability(clusterConfig["availability"].(string))
err = client.VolumeUpdate(
ctx, vol.ClusterVolume.ID, vol.ClusterVolume.Version,
volume.UpdateOptions{
Spec: &vol.ClusterVolume.Spec,
},
)
if err != nil {
return diag.Errorf("Unable to update the cluster volume: %v", err)
}
break
}
}
}
return nil
}
func resourceDockerVolumeRemoveRefreshFunc(
volumeID string, meta interface{}) retry.StateRefreshFunc {
return func() (interface{}, string, error) {
@ -163,13 +421,13 @@ func resourceDockerVolumeRemoveRefreshFunc(
if err := client.VolumeRemove(context.Background(), volumeID, forceDelete); err != nil {
if containsIgnorableErrorMessage(err.Error(), "volume is in use") {
log.Printf("[INFO] Volume with id '%v' is still in use", volumeID)
log.Printf("[INFO] Volume with id `%v` is still in use", volumeID)
return volumeID, "in_use", nil
}
log.Printf("[INFO] Removing volume with id '%v' caused an error: %v", volumeID, err)
log.Printf("[INFO] Removing volume with id `%v` caused an error: %v", volumeID, err)
return nil, "", err
}
log.Printf("[INFO] Removing volume with id '%v' got removed", volumeID)
log.Printf("[INFO] Removing volume with id `%v` got removed", volumeID)
return volumeID, "removed", nil
}
}

View file

@ -193,3 +193,33 @@ func checkDockerVolumeCreated(n string, volumeToCheck *volume.Volume) resource.T
return nil
}
}
func TestAccDockerVolume_cluster(t *testing.T) {
var v volume.Volume
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: loadTestConfiguration(t, RESOURCE, "docker_volume", "testAccDockerVolumeCluster"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("docker_volume.foo", "id", "testAccDockerVolume_cluster"),
resource.TestCheckResourceAttr("docker_volume.foo", "name", "testAccDockerVolume_cluster"),
resource.TestCheckResourceAttr("docker_volume.foo", "cluster.0.scope", "multi"),
resource.TestCheckResourceAttr("docker_volume.foo", "cluster.0.required_bytes", "1MiB"),
resource.TestCheckResourceAttr("docker_volume.foo", "cluster.0.limit_bytes", "2MiB"),
resource.TestCheckResourceAttr("docker_volume.foo", "cluster.0.sharing", "all"),
resource.TestCheckResourceAttr("docker_volume.foo", "cluster.0.group", "testgroup"),
checkDockerVolumeCreated("docker_volume.foo", &v),
// testCheckVolumeInspect,
),
},
{
ResourceName: "docker_volume.foo",
ImportState: true,
ImportStateVerify: true,
},
},
})
}

View file

@ -0,0 +1,19 @@
resource "docker_volume" "foo" {
name = "testAccDockerVolume_cluster"
driver = "local"
driver_opts = {
type = "btrfs"
device = "/dev/sda2"
}
cluster {
scope = "multi"
sharing = "all"
group = "testgroup"
required_bytes = "1MiB"
limit_bytes = "2MiB"
}
}