Repository: compose-spec/compose-go
Pull request #248 (2022-04-12 13:29:27)

introduce ServiceVolumeConfig.String()
introduce ServiceVolumeConfig.String() as reverse operation for loader.ParseVolume
this can be used to create back a docker cli/engine `volume` string to be used on `bind` API

see https://github.com/docker/compose/issues/9380


Patch:
diff --git a/types/types.go b/types/types.go
index 1c1ed3a..58369d4 100644
--- a/types/types.go
+++ b/types/types.go
@@ -291,19 +291,19 @@ func (s set) toSlice() []string {
 
 // BuildConfig is a type for build
 type BuildConfig struct {
-	Context    string            `yaml:",omitempty" json:"context,omitempty"`
-	Dockerfile string            `yaml:",omitempty" json:"dockerfile,omitempty"`
-	Args       MappingWithEquals `yaml:",omitempty" json:"args,omitempty"`
-	SSH        SSHConfig         `yaml:"ssh,omitempty" json:"ssh,omitempty"`
-	Labels     Labels            `yaml:",omitempty" json:"labels,omitempty"`
-	CacheFrom  StringList        `mapstructure:"cache_from" yaml:"cache_from,omitempty" json:"cache_from,omitempty"`
-	CacheTo    StringList        `mapstructure:"cache_to" yaml:"cache_to,omitempty" json:"cache_to,omitempty"`
-	NoCache    bool              `mapstructure:"no_cache" yaml:"no_cache,omitempty" json:"no_cache,omitempty"`
-	Pull       bool              `mapstructure:"pull" yaml:"pull,omitempty" json:"pull,omitempty"`
-	ExtraHosts HostsList         `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
-	Isolation  string            `yaml:",omitempty" json:"isolation,omitempty"`
-	Network    string            `yaml:",omitempty" json:"network,omitempty"`
-	Target     string            `yaml:",omitempty" json:"target,omitempty"`
+	Context    string                `yaml:",omitempty" json:"context,omitempty"`
+	Dockerfile string                `yaml:",omitempty" json:"dockerfile,omitempty"`
+	Args       MappingWithEquals     `yaml:",omitempty" json:"args,omitempty"`
+	SSH        SSHConfig             `yaml:"ssh,omitempty" json:"ssh,omitempty"`
+	Labels     Labels                `yaml:",omitempty" json:"labels,omitempty"`
+	CacheFrom  StringList            `mapstructure:"cache_from" yaml:"cache_from,omitempty" json:"cache_from,omitempty"`
+	CacheTo    StringList            `mapstructure:"cache_to" yaml:"cache_to,omitempty" json:"cache_to,omitempty"`
+	NoCache    bool                  `mapstructure:"no_cache" yaml:"no_cache,omitempty" json:"no_cache,omitempty"`
+	Pull       bool                  `mapstructure:"pull" yaml:"pull,omitempty" json:"pull,omitempty"`
+	ExtraHosts HostsList             `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
+	Isolation  string                `yaml:",omitempty" json:"isolation,omitempty"`
+	Network    string                `yaml:",omitempty" json:"network,omitempty"`
+	Target     string                `yaml:",omitempty" json:"target,omitempty"`
 	Secrets    []ServiceSecretConfig `yaml:",omitempty" json:"secrets,omitempty"`
 
 	Extensions map[string]interface{} `yaml:",inline" json:"-"`
@@ -679,6 +679,25 @@ type ServiceVolumeConfig struct {
 	Extensions map[string]interface{} `yaml:",inline" json:"-"`
 }
 
+// String render ServiceVolumeConfig as a volume string, one can parse back using loader.ParseVolume
+func (s ServiceVolumeConfig) String() string {
+	access := "rw"
+	if s.ReadOnly {
+		access = "ro"
+	}
+	options := []string{access}
+	if s.Bind != nil && s.Bind.SELinux != "" {
+		options = append(options, s.Bind.SELinux)
+	}
+	if s.Bind != nil && s.Bind.Propagation != "" {
+		options = append(options, s.Bind.Propagation)
+	}
+	if s.Volume != nil && s.Volume.NoCopy {
+		options = append(options, "nocopy")
+	}
+	return fmt.Sprintf("%s:%s:%s", s.Source, s.Target, strings.Join(options, ","))
+}
+
 const (
 	// VolumeTypeBind is the type for mounting host dir
 	VolumeTypeBind = "bind"


Test patch:
diff --git a/loader/volume_test.go b/loader/volume_test.go
index 2562532..55be29e 100644
--- a/loader/volume_test.go
+++ b/loader/volume_test.go
@@ -280,3 +280,18 @@ func TestParseVolumeInvalidSections(t *testing.T) {
 	_, err := ParseVolume("/foo::rw")
 	assert.ErrorContains(t, err, "invalid spec")
 }
+
+func TestVolumeStringer(t *testing.T) {
+	v := types.ServiceVolumeConfig{
+		Type:     "bind",
+		Source:   "/src",
+		Target:   "/target",
+		ReadOnly: false,
+		Bind: &types.ServiceVolumeBind{
+			CreateHostPath: true,
+			Propagation:    types.PropagationShared,
+			SELinux:        types.SELinuxShared,
+		},
+	}
+	assert.Equal(t, v.String(), "/src:/target:rw,z,shared")
+}