Repository: compose-spec/compose-go
Pull request #747 (2025-02-25 09:31:17)

introduce watch.include


Patch:
diff --git a/go.mod b/go.mod
index 082bb2f..d56ff2d 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
 	github.com/sirupsen/logrus v1.9.0
 	github.com/stretchr/testify v1.8.4
 	github.com/xeipuuv/gojsonschema v1.2.0
+	github.com/xhit/go-str2duration/v2 v2.1.0
 	golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3
 	golang.org/x/sync v0.3.0
 	gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
index 6eb3cfe..6f28674 100644
--- a/go.sum
+++ b/go.sum
@@ -31,6 +31,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
 github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
+github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
diff --git a/schema/compose-spec.json b/schema/compose-spec.json
index 1da7f22..2a42c0e 100644
--- a/schema/compose-spec.json
+++ b/schema/compose-spec.json
@@ -370,9 +370,10 @@
         "pre_stop": {"type": "array", "items": {"$ref": "#/definitions/service_hook"}},
         "privileged": {"type": ["boolean", "string"]},
         "profiles": {"$ref": "#/definitions/list_of_strings"},
-        "pull_policy": {"type": "string", "enum": [
-          "always", "never", "if_not_present", "build", "missing"
-        ]},
+        "pull_policy": {"type": "string",
+          "pattern": "always|never|build|if_not_present|missing|refresh|daily|weekly|every_([0-9]+[wdhms])+"
+        },
+        "pull_refresh_after": {"type": "string"},
         "read_only": {"type": ["boolean", "string"]},
         "restart": {"type": "string"},
         "runtime": {
@@ -490,7 +491,8 @@
             "type": "object",
             "required": ["path", "action"],
             "properties": {
-              "ignore": {"type": "array", "items": {"type": "string"}},
+              "ignore": {"$ref": "#/definitions/string_or_list"},
+              "include": {"$ref": "#/definitions/string_or_list"},
               "path": {"type": "string"},
               "action": {"type": "string", "enum": ["rebuild", "sync", "restart", "sync+restart", "sync+exec"]},
               "target": {"type": "string"},
diff --git a/transform/canonical.go b/transform/canonical.go
index d37eb1e..3f65d11 100644
--- a/transform/canonical.go
+++ b/transform/canonical.go
@@ -44,6 +44,8 @@ func init() {
 	transformers["services.*.build.ssh"] = transformSSH
 	transformers["services.*.ulimits.*"] = transformUlimits
 	transformers["services.*.build.ulimits.*"] = transformUlimits
+	transformers["services.*.develop.watch.*.ignore"] = transformStringOrList
+	transformers["services.*.develop.watch.*.include"] = transformStringOrList
 	transformers["volumes.*"] = transformMaybeExternal
 	transformers["networks.*"] = transformMaybeExternal
 	transformers["secrets.*"] = transformMaybeExternal
diff --git a/types/derived.gen.go b/types/derived.gen.go
index d4b8257..17b22e6 100644
--- a/types/derived.gen.go
+++ b/types/derived.gen.go
@@ -2025,6 +2025,24 @@ func deriveDeepCopy_46(dst, src *Trigger) {
 		deriveDeepCopy_44(field, &src.Exec)
 		dst.Exec = *field
 	}()
+	if src.Include == nil {
+		dst.Include = nil
+	} else {
+		if dst.Include != nil {
+			if len(src.Include) > len(dst.Include) {
+				if cap(dst.Include) >= len(src.Include) {
+					dst.Include = (dst.Include)[:len(src.Include)]
+				} else {
+					dst.Include = make([]string, len(src.Include))
+				}
+			} else if len(src.Include) < len(dst.Include) {
+				dst.Include = (dst.Include)[:len(src.Include)]
+			}
+		} else {
+			dst.Include = make([]string, len(src.Include))
+		}
+		copy(dst.Include, src.Include)
+	}
 	if src.Ignore == nil {
 		dst.Ignore = nil
 	} else {
diff --git a/types/develop.go b/types/develop.go
index 8f7c8fa..fa30615 100644
--- a/types/develop.go
+++ b/types/develop.go
@@ -37,6 +37,7 @@ type Trigger struct {
 	Action     WatchAction `yaml:"action" json:"action"`
 	Target     string      `yaml:"target,omitempty" json:"target,omitempty"`
 	Exec       ServiceHook `yaml:"exec,omitempty" json:"exec,omitempty"`
+	Include    []string    `yaml:"include,omitempty" json:"include,omitempty"`
 	Ignore     []string    `yaml:"ignore,omitempty" json:"ignore,omitempty"`
 	Extensions Extensions  `yaml:"#extensions,inline,omitempty" json:"-"`
 }
diff --git a/types/duration.go b/types/duration.go
index 95f562a..c1c3973 100644
--- a/types/duration.go
+++ b/types/duration.go
@@ -21,6 +21,8 @@ import (
 	"fmt"
 	"strings"
 	"time"
+
+	"github.com/xhit/go-str2duration/v2"
 )
 
 // Duration is a thin wrapper around time.Duration with improved JSON marshalling
@@ -31,7 +33,7 @@ func (d Duration) String() string {
 }
 
 func (d *Duration) DecodeMapstructure(value interface{}) error {
-	v, err := time.ParseDuration(fmt.Sprint(value))
+	v, err := str2duration.ParseDuration(fmt.Sprint(value))
 	if err != nil {
 		return err
 	}
diff --git a/types/types.go b/types/types.go
index 4d8b376..1773f69 100644
--- a/types/types.go
+++ b/types/types.go
@@ -21,8 +21,10 @@ import (
 	"fmt"
 	"sort"
 	"strings"
+	"time"
 
 	"github.com/docker/go-connections/nat"
+	"github.com/xhit/go-str2duration/v2"
 )
 
 // ServiceConfig is the configuration of one service
@@ -215,6 +217,8 @@ const (
 	PullPolicyMissing = "missing"
 	// PullPolicyBuild force building images
 	PullPolicyBuild = "build"
+	// PullPolicyRefresh checks if image needs to be updated
+	PullPolicyRefresh = "refresh"
 )
 
 const (
@@ -268,6 +272,27 @@ func (s ServiceConfig) GetDependents(p *Project) []string {
 	return dependent
 }
 
+func (s ServiceConfig) GetPullPolicy() (string, time.Duration, error) {
+	switch s.PullPolicy {
+	case PullPolicyAlways, PullPolicyNever, PullPolicyIfNotPresent, PullPolicyMissing, PullPolicyBuild:
+		return s.PullPolicy, 0, nil
+	case "daily":
+		return PullPolicyRefresh, 24 * time.Hour, nil
+	case "weekly":
+		return PullPolicyRefresh, 7 * 24 * time.Hour, nil
+	default:
+		if strings.HasPrefix(s.PullPolicy, "every_") {
+			delay := s.PullPolicy[6:]
+			duration, err := str2duration.ParseDuration(delay)
+			if err != nil {
+				return "", 0, err
+			}
+			return PullPolicyRefresh, duration, nil
+		}
+		return PullPolicyMissing, 0, nil
+	}
+}
+
 // BuildConfig is a type for build
 type BuildConfig struct {
 	Context            string                    `yaml:"context,omitempty" json:"context,omitempty"`


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index a1afd74..c0c7565 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -3719,3 +3719,17 @@ services:
 	assert.NilError(t, err)
 	assert.Equal(t, p.Services["test"].Networks["test"].GatewayPriority, 42)
 }
+
+func TestPullRefresh(t *testing.T) {
+	p, err := loadYAML(`
+name: load-all-gpus
+services:
+  test:
+    pull_policy: every_2d
+`)
+	assert.NilError(t, err)
+	policy, duration, err := p.Services["test"].GetPullPolicy()
+	assert.NilError(t, err)
+	assert.Equal(t, policy, types.PullPolicyRefresh)
+	assert.Equal(t, duration, 2*24*time.Hour)
+}