Repository: crazy-max/diun (go)

Issue:
[Feature] Do not launch update checks at startup
### Support guidelines

- [X] I've read the [support guidelines](https://github.com/crazy-max/diun/blob/master/.github/SUPPORT.md)

### I've found a bug and checked that ...

- [X] ... the documentation does not mention anything about my problem
- [X] ... there are no open or closed issues that are related to my problem

### Description

At container startup, Diun lauches the update check, as it is explained in the comment https://github.com/crazy-max/diun/issues/833#issuecomment-1586332791.
In my use case, my container is stopped every night because installed on Synolog NAS that shutdown every night, and every morning Diun check updates, despite i set it up to check only on sundays.

### Expected behaviour

Add a new config parameter to activate or not the run at startup in addition with the cron trigger.

### Actual behaviour

Run is launched at startup and when triggered by cron.

### Steps to reproduce

1. Start Diun container
2. Check is done immediatly even if the moment does not satisfy the cron

### Diun version

4.24.0

### Docker info

```text
Client:
 Context:    default
 Debug Mode: false

Server:
 Containers: 17
  Running: 15
  Paused: 0
  Stopped: 2
 Images: 33
 Server Version: 20.10.3
 Storage Driver: btrfs
  Build Version: Btrfs v4.0
  Library Version: 101
 Logging Driver: db
 Cgroup Driver: cgroupfs
 Cgroup Version: 1
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs db fluentd gcplogs gelf journald json-file local logentries splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 io.containerd.runtime.v1.linux runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 3fa00912415f3e9c6f82dd72119179d599efd13b
 runc version: 31cc25f16f5eba4d0f53e35374532873744f4b31
 init version: ed96d00 (expected: de40ad0)
 Security Options:
  apparmor
 Kernel Version: 4.4.180+
 OSType: linux
 Architecture: x86_64
 CPUs: 2
 Total Memory: 5.641GiB
 Name: SkuairStation
 ID: DGMR:ZSA2:HDUM:REYZ:EILQ:YXMF:IW7K:XNCW:LDI7:CT6U:NSZS:DKGV
 Docker Root Dir: /volume1/@docker
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Labels:
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Live Restore Enabled: false
```


### Docker Compose config

_No response_

### Logs

```text
2023-06-08T05:33:43.375570918Z | stdout | �[90mThu, 08 Jun 2023 07:33:43 CEST�[0m �[32mINF�[0m Next run in 2 days 16 hours (2023-06-11 00:30:09.977184317 +0200 CEST)
-- | -- | --
2023-06-08T05:33:43.375482822Z | stdout | �[90mThu, 08 Jun 2023 07:33:43 CEST�[0m �[32mINF�[0m Cron initialized with schedule 30 0 * * 0
2023-06-08T05:33:43.375026847Z | stdout | �[90mThu, 08 Jun 2023 07:33:43 CEST�[0m �[32mINF�[0m Jobs completed �[36madded=�[0m0 �[36mfailed=�[0m0 �[36mskipped=�[0m0 �[36munchanged=�[0m13 �[36mupdated=�[0m2
2023-06-08T05:33:21.636264725Z | stdout | �[90mThu, 08 Jun 2023 07:33:21 CEST�[0m �[32mINF�[0m Image update found �[36mimage=�[0mdocker.io/linuxserver/pyload-ng:latest �[36mprovider=�[0mdocker
2023-06-08T05:33:21.143026694Z | stdout | �[90mThu, 08 Jun 2023 07:33:21 CEST�[0m �[32mINF�[0m Image update found �[36mimage=�[0mdocker.io/gitea/gitea:latest �[36mprovider=�[0mdocker
2023-06-08T05:33:17.557081854Z | stdout | �[90mThu, 08 Jun 2023 07:33:17 CEST�[0m �[32mINF�[0m Found 15 image(s) to analyze �[36mprovider=�[0mdocker
2023-06-08T05:33:02.548833910Z | stdout | �[90mThu, 08 Jun 2023 07:33:02 CEST�[0m �[32mINF�[0m Cron triggered
2023-06-08T05:32:59.547710032Z | stdout | �[90mThu, 08 Jun 2023 07:32:59 CEST�[0m �[32mINF�[0m Configuration loaded from 1 environment variable(s)
2023-06-08T05:32:59.547617557Z | stdout | �[90mThu, 08 Jun 2023 07:32:59 CEST�[0m �[32mINF�[0m Configuration loaded from file: /diun.yml
2023-06-08T05:32:59.106430652Z | stdout | �[90mThu, 08 Jun 2023 07:32:59 CEST�[0m �[32mINF�[0m Starting Diun �[36mversion=�[0mv4.24.0
2023-06-07T23:00:22.571237010Z | stdout | �[90mThu, 08 Jun 2023 01:00:22 CEST�[0m �[31mWRN�[0m Caught signal terminated
```


### Additional info

_No response_

Pull request description:
runOnStartup watch option
fixes #894

cc @Skuair

Patch:
diff --git a/docs/config/index.md b/docs/config/index.md
index 4cd65c7a..cb5e7989 100644
--- a/docs/config/index.md
+++ b/docs/config/index.md
@@ -34,6 +34,7 @@ You can override this using the [`--config` flag or `CONFIG` env var with `serve
       schedule: "0 */6 * * *"
       jitter: 30s
       firstCheckNotif: false
+      runOnStartup: true
 
     notif:
       amqp:
@@ -129,6 +130,7 @@ All configuration from file can be transposed into environment variables. As an
       schedule: "0 */6 * * *"
       jitter: 30s
       firstCheckNotif: false
+      runOnStartup: true
 
     notif:
       gotify:
@@ -183,6 +185,7 @@ Can be transposed to:
     DIUN_WATCH_SCHEDULE=0 */6 * * *
     DIUN_WATCH_JITTER=30s
     DIUN_WATCH_FIRSTCHECKNOTIF=false
+    DIUN_WATCH_RUNONSTARTUP=true
 
     DIUN_NOTIF_GOTIFY_ENDPOINT=http://gotify.foo.com
     DIUN_NOTIF_GOTIFY_TOKEN=Token123456
diff --git a/docs/config/watch.md b/docs/config/watch.md
index fd2eed9b..b30fafa6 100644
--- a/docs/config/watch.md
+++ b/docs/config/watch.md
@@ -8,6 +8,7 @@ watch:
   schedule: "0 */6 * * *"
   jitter: 30s
   firstCheckNotif: false
+  runOnStartup: true
   compareDigest: true
   healthchecks:
     baseURL: https://hc-ping.com/
@@ -76,6 +77,19 @@ Send notification at the very first analysis of an image. (default `false`)
 !!! abstract "Environment variables"
     * `DIUN_WATCH_FIRSTCHECKNOTIF`
 
+### `runOnStartup`
+
+Check for updates on startup. (default `true`)
+
+!!! example "Config file"
+    ```yaml
+    watch:
+      runOnStartup: true
+    ```
+
+!!! abstract "Environment variables"
+    * `DIUN_WATCH_RUNONSTARTUP`
+
 ### `compareDigest`
 
 Compare the digest of an image with the registry before downloading the image manifest. It is strongly
diff --git a/internal/app/diun.go b/internal/app/diun.go
index 850787d1..0ea50f91 100644
--- a/internal/app/diun.go
+++ b/internal/app/diun.go
@@ -103,8 +103,9 @@ func (di *Diun) Start() error {
 		}
 	}()
 
-	// Run on startup
-	di.Run()
+	if *di.cfg.Watch.RunOnStartup {
+		di.Run()
+	}
 
 	// Init scheduler if defined
 	if len(di.cfg.Watch.Schedule) == 0 {
diff --git a/internal/model/watch.go b/internal/model/watch.go
index 05c1557e..f9239519 100644
--- a/internal/model/watch.go
+++ b/internal/model/watch.go
@@ -12,6 +12,7 @@ type Watch struct {
 	Schedule        string         `yaml:"schedule,omitempty" json:"schedule,omitempty"`
 	Jitter          *time.Duration `yaml:"jitter,omitempty" json:"jitter,omitempty" validate:"required"`
 	FirstCheckNotif *bool          `yaml:"firstCheckNotif,omitempty" json:"firstCheckNotif,omitempty" validate:"required"`
+	RunOnStartup    *bool          `yaml:"runOnStartup,omitempty" json:"runOnStartup,omitempty" validate:"required"`
 	CompareDigest   *bool          `yaml:"compareDigest,omitempty" json:"compareDigest,omitempty" validate:"required"`
 	Healthchecks    *Healthchecks  `yaml:"healthchecks,omitempty" json:"healthchecks,omitempty"`
 }
@@ -28,5 +29,6 @@ func (s *Watch) SetDefaults() {
 	s.Workers = 10
 	s.Jitter = utl.NewDuration(30 * time.Second)
 	s.FirstCheckNotif = utl.NewFalse()
+	s.RunOnStartup = utl.NewTrue()
 	s.CompareDigest = utl.NewTrue()
 }


Test patch:
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 8f1e1af5..9a44470a 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -43,6 +43,8 @@ jobs:
             loglevel: info
           - folder: docker3
             loglevel: info
+          - folder: docker4
+            loglevel: info
           - folder: dockerfile1
             loglevel: debug
     steps:
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 1788a08e..1a092f49 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -1,8 +1,6 @@
 package config_test
 
 import (
-	"fmt"
-	"os"
 	"strings"
 	"testing"
 	"time"
@@ -54,6 +52,7 @@ func TestLoadFile(t *testing.T) {
 					Schedule:        "*/30 * * * *",
 					Jitter:          utl.NewDuration(30 * time.Second),
 					FirstCheckNotif: utl.NewTrue(),
+					RunOnStartup:    utl.NewFalse(),
 					CompareDigest:   utl.NewTrue(),
 					Healthchecks: &model.Healthchecks{
 						BaseURL: "https://hc-ping.com/",
@@ -247,8 +246,6 @@ for <code>{{ .Entry.Manifest.Platform }}</code> platform.
 }
 
 func TestLoadEnv(t *testing.T) {
-	defer UnsetEnv("DIUN_")
-
 	testCases := []struct {
 		desc     string
 		cfg      string
@@ -372,12 +369,10 @@ func TestLoadEnv(t *testing.T) {
 	for _, tt := range testCases {
 		tt := tt
 		t.Run(tt.desc, func(t *testing.T) {
-			UnsetEnv("DIUN_")
-
 			if tt.environ != nil {
 				for _, environ := range tt.environ {
 					n := strings.SplitN(environ, "=", 2)
-					os.Setenv(n[0], n[1])
+					t.Setenv(n[0], n[1])
 				}
 			}
 
@@ -394,8 +389,6 @@ func TestLoadEnv(t *testing.T) {
 }
 
 func TestLoadMixed(t *testing.T) {
-	defer UnsetEnv("DIUN_")
-
 	testCases := []struct {
 		desc     string
 		cfg      string
@@ -497,12 +490,10 @@ for <code>{{ .Entry.Manifest.Platform }}</code> platform.
 	for _, tt := range testCases {
 		tt := tt
 		t.Run(tt.desc, func(t *testing.T) {
-			UnsetEnv("DIUN_")
-
 			if tt.environ != nil {
 				for _, environ := range tt.environ {
 					n := strings.SplitN(environ, "=", 2)
-					os.Setenv(n[0], n[1])
+					t.Setenv(n[0], n[1])
 				}
 			}
 
@@ -532,61 +523,8 @@ func TestValidation(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			cfg, err := config.Load(tt.cfg)
 			require.NoError(t, err)
-
-			dec, err := env.Encode("DIUN_", cfg)
+			_, err = env.Encode("DIUN_", cfg)
 			require.NoError(t, err)
-			for _, value := range dec {
-				fmt.Printf(`%s=%s\n`, value.Name, value.Default)
-			}
 		})
 	}
 }
-
-func UnsetEnv(prefix string) (restore func()) {
-	before := map[string]string{}
-
-	for _, e := range os.Environ() {
-		if !strings.HasPrefix(e, prefix) {
-			continue
-		}
-
-		parts := strings.SplitN(e, "=", 2)
-		before[parts[0]] = parts[1]
-
-		os.Unsetenv(parts[0])
-	}
-
-	return func() {
-		after := map[string]string{}
-
-		for _, e := range os.Environ() {
-			if !strings.HasPrefix(e, prefix) {
-				continue
-			}
-
-			parts := strings.SplitN(e, "=", 2)
-			after[parts[0]] = parts[1]
-
-			// Check if the envar previously existed
-			v, ok := before[parts[0]]
-			if !ok {
-				// This is a newly added envar with prefix, zap it
-				os.Unsetenv(parts[0])
-				continue
-			}
-
-			if parts[1] != v {
-				// If the envar value has changed, set it back
-				os.Setenv(parts[0], v)
-			}
-		}
-
-		// Still need to check if there have been any deleted envars
-		for k, v := range before {
-			if _, ok := after[k]; !ok {
-				// k is not present in after, so we set it.
-				os.Setenv(k, v)
-			}
-		}
-	}
-}
diff --git a/internal/config/fixtures/config.test.yml b/internal/config/fixtures/config.test.yml
index a9c31851..39055cdf 100644
--- a/internal/config/fixtures/config.test.yml
+++ b/internal/config/fixtures/config.test.yml
@@ -6,6 +6,7 @@ watch:
   schedule: "*/30 * * * *"
   jitter: 30s
   firstCheckNotif: true
+  runOnStartup: false
   compareDigest: true
   healthchecks:
     baseURL: https://hc-ping.com/
diff --git a/test/docker4/diun.yml b/test/docker4/diun.yml
new file mode 100644
index 00000000..f4abe65f
--- /dev/null
+++ b/test/docker4/diun.yml
@@ -0,0 +1,9 @@
+watch:
+  workers: 20
+  schedule: "* * * * *"
+  runOnStartup: false
+
+providers:
+  docker:
+    watchByDefault: true
+    watchStopped: true