Skip to content

Commit ddf4117

Browse files
committed
feat: add one-off synchronization command for Docker images
1 parent 7c16c6c commit ddf4117

File tree

5 files changed

+258
-33
lines changed

5 files changed

+258
-33
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ Build the project:
2525
```console
2626
make
2727
```
28+
To run a one-off synchronization, you can run `dist/dockersync sync`.
29+
30+
``` sh
31+
dist/dockersync --source docker.io/library/ubuntu --targets r2:blablabla:docker-sync-test:ubuntu --url1 docker.io --username1 foo --password1 bar --url2 r2:blablabla:docker-sync-test --username2 foo --password2 bar
32+
```
33+
34+
Run `dist/dockersync sync --help` for more configuration options.
35+
36+
## Configuration
2837

2938
Write the default config file:
3039

@@ -38,8 +47,6 @@ Edit the config file accordingly, then run:
3847
dist/dockersync
3948
```
4049

41-
## Configuration
42-
4350
The default configuration looks like this:
4451

4552
```yaml

cmd/dockersync/cmd/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,15 @@ var rootCmd = &cobra.Command{
6060
}
6161

6262
func Execute() {
63+
initConfig()
64+
6365
err := rootCmd.Execute()
6466
if err != nil {
6567
os.Exit(1)
6668
}
6769
}
6870

6971
func init() {
70-
cobra.OnInitialize(initConfig)
71-
7272
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is config.yaml)")
7373
}
7474

cmd/dockersync/cmd/sync.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"os"
6+
"os/signal"
7+
"path/filepath"
8+
"syscall"
9+
"time"
10+
11+
dockersync "github.com/Altinity/docker-sync"
12+
"github.com/Altinity/docker-sync/config"
13+
"github.com/Altinity/docker-sync/logging"
14+
"github.com/rs/zerolog/log"
15+
"github.com/spf13/cobra"
16+
"gopkg.in/yaml.v3"
17+
)
18+
19+
type syncImage struct {
20+
Source string `yaml:"source"`
21+
Targets []string `yaml:"targets"`
22+
MutableTags []string `yaml:"mutableTags"`
23+
}
24+
25+
type syncAuth struct {
26+
Username string `yaml:"username"`
27+
Password string `yaml:"password"`
28+
Token string `yaml:"token"`
29+
Helper string `yaml:"helper"`
30+
}
31+
32+
type syncRegistry struct {
33+
Auth syncAuth `yaml:"auth"`
34+
Name string `yaml:"name"`
35+
URL string `yaml:"url"`
36+
}
37+
38+
type syncConfig struct {
39+
Ecr struct {
40+
Region string `yaml:"region"`
41+
} `yaml:"ecr"`
42+
Sync struct {
43+
Images []syncImage `yaml:"images"`
44+
Registries []syncRegistry `yaml:"registries"`
45+
} `yaml:"sync"`
46+
}
47+
48+
var syncCmd = &cobra.Command{
49+
Use: "sync",
50+
Short: "Sync a single image",
51+
Run: func(cmd *cobra.Command, args []string) {
52+
ctx, cancel := context.WithCancel(context.Background())
53+
defer cancel()
54+
55+
c := make(chan os.Signal, 1)
56+
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
57+
go func() {
58+
<-c
59+
cancel()
60+
time.Sleep(1 * time.Second)
61+
os.Exit(0)
62+
}()
63+
64+
logging.ReloadGlobalLogger()
65+
66+
log.Info().Msg("Starting Docker Sync")
67+
68+
cnf := syncConfig{}
69+
cnf.Ecr.Region, _ = cmd.Flags().GetString("ecr-region")
70+
71+
source, _ := cmd.Flags().GetString("source")
72+
targets, _ := cmd.Flags().GetStringSlice("targets")
73+
74+
cnf.Sync.Images = append(cnf.Sync.Images, syncImage{
75+
Source: source,
76+
Targets: targets,
77+
})
78+
79+
var registries []syncRegistry
80+
81+
helper1, _ := cmd.Flags().GetString("helper1")
82+
password1, _ := cmd.Flags().GetString("password1")
83+
token1, _ := cmd.Flags().GetString("token1")
84+
username1, _ := cmd.Flags().GetString("username1")
85+
url1, _ := cmd.Flags().GetString("url1")
86+
87+
helper2, _ := cmd.Flags().GetString("helper2")
88+
password2, _ := cmd.Flags().GetString("password2")
89+
token2, _ := cmd.Flags().GetString("token2")
90+
username2, _ := cmd.Flags().GetString("username2")
91+
url2, _ := cmd.Flags().GetString("url2")
92+
93+
helper3, _ := cmd.Flags().GetString("helper3")
94+
password3, _ := cmd.Flags().GetString("password3")
95+
token3, _ := cmd.Flags().GetString("token3")
96+
username3, _ := cmd.Flags().GetString("username3")
97+
url3, _ := cmd.Flags().GetString("url3")
98+
99+
if url1 != "" && (username1 != "" || password1 != "" || token1 != "" || helper1 != "") {
100+
registries = append(registries, syncRegistry{
101+
Auth: syncAuth{
102+
Username: username1,
103+
Password: password1,
104+
Token: token1,
105+
Helper: helper1,
106+
},
107+
Name: "registry1",
108+
URL: url1,
109+
})
110+
}
111+
112+
if url2 != "" && (username2 != "" || password2 != "" || token2 != "" || helper2 != "") {
113+
registries = append(registries, syncRegistry{
114+
Auth: syncAuth{
115+
Username: username2,
116+
Password: password2,
117+
Token: token2,
118+
Helper: helper2,
119+
},
120+
Name: "registry2",
121+
URL: url2,
122+
})
123+
}
124+
125+
if url3 != "" && (username3 != "" || password3 != "" || token3 != "" || helper3 != "") {
126+
registries = append(registries, syncRegistry{
127+
Auth: syncAuth{
128+
Username: username3,
129+
Password: password3,
130+
Token: token3,
131+
Helper: helper3,
132+
},
133+
Name: "registry3",
134+
URL: url3,
135+
})
136+
}
137+
138+
cnf.Sync.Registries = registries
139+
140+
tmpDir, err := os.MkdirTemp(os.TempDir(), "docker-sync")
141+
if err != nil {
142+
log.Fatal().Err(err).Msg("Failed to create temporary directory")
143+
}
144+
defer os.RemoveAll(tmpDir)
145+
146+
b, err := yaml.Marshal(cnf)
147+
if err != nil {
148+
log.Fatal().Err(err).Msg("Failed to marshal configuration")
149+
}
150+
151+
if err := os.WriteFile(filepath.Join(tmpDir, "config.yaml"), b, 0644); err != nil {
152+
log.Fatal().Err(err).Msg("Failed to write configuration")
153+
}
154+
155+
if err := config.InitConfig(filepath.Join(tmpDir, "config.yaml")); err != nil {
156+
log.Fatal().Err(err).Msg("Failed to initialize config")
157+
}
158+
159+
dockersync.Reload()
160+
161+
images := config.SyncImages.Images()
162+
163+
if err := dockersync.RunOnce(ctx, images); err != nil {
164+
cmd.Annotations["error"] = err.Error()
165+
log.Error().
166+
Stack().
167+
Err(err).
168+
Msg("Error running Docker Sync")
169+
}
170+
171+
log.Info().Msg("Shutting down Docker Sync")
172+
},
173+
}
174+
175+
func init() {
176+
rootCmd.AddCommand(syncCmd)
177+
178+
syncCmd.Flags().StringP("source", "s", "", "Source image")
179+
syncCmd.Flags().StringSliceP("targets", "t", []string{}, "Target images")
180+
181+
syncCmd.MarkFlagRequired("source")
182+
syncCmd.MarkFlagRequired("targets")
183+
184+
syncCmd.Flags().StringP("mutableTags", "m", "", "Mutable tags")
185+
186+
syncCmd.Flags().StringP("ecr-region", "", os.Getenv("AWS_REGION"), "AWS region for ECR")
187+
188+
syncCmd.Flags().StringP("helper1", "", os.Getenv("DOCKER_SYNC_REGISTRY_1_HELPER"), "First registry helper for target image")
189+
syncCmd.Flags().StringP("password1", "", os.Getenv("DOCKER_SYNC_REGISTRY_1_PASSWORD"), "First registry password for target image")
190+
syncCmd.Flags().StringP("token1", "", os.Getenv("DOCKER_SYNC_REGISTRY_1_TOKEN"), "First registry token for target image")
191+
syncCmd.Flags().StringP("username1", os.Getenv("DOCKER_SYNC_TARGET_REGISTRY_1_USERNAME"), "", "First registry username for target image")
192+
syncCmd.Flags().StringP("url1", "", os.Getenv("DOCKER_SYNC_TARGET_REGISTRY_1_URL"), "First registry URL for target image")
193+
194+
syncCmd.Flags().StringP("helper2", "", os.Getenv("DOCKER_SYNC_REGISTRY_2_HELPER"), "Second registry helper for target image")
195+
syncCmd.Flags().StringP("password2", "", os.Getenv("DOCKER_SYNC_REGISTRY_2_PASSWORD"), "Second registry password for target image")
196+
syncCmd.Flags().StringP("token2", "", os.Getenv("DOCKER_SYNC_REGISTRY_2_TOKEN"), "Second registry token for target image")
197+
syncCmd.Flags().StringP("username2", os.Getenv("DOCKER_SYNC_TARGET_REGISTRY_2_USERNAME"), "", "Second registry username for target image")
198+
syncCmd.Flags().StringP("url2", "", os.Getenv("DOCKER_SYNC_TARGET_REGISTRY_2_URL"), "Second registry URL for target image")
199+
200+
syncCmd.Flags().StringP("helper3", "", os.Getenv("DOCKER_SYNC_REGISTRY_3_HELPER"), "Third registry helper for target image")
201+
syncCmd.Flags().StringP("password3", "", os.Getenv("DOCKER_SYNC_REGISTRY_3_PASSWORD"), "Third registry password for target image")
202+
syncCmd.Flags().StringP("token3", "", os.Getenv("DOCKER_SYNC_REGISTRY_3_TOKEN"), "Third registry token for target image")
203+
syncCmd.Flags().StringP("username3", os.Getenv("DOCKER_SYNC_TARGET_REGISTRY_3_USERNAME"), "", "Third registry username for target image")
204+
syncCmd.Flags().StringP("url3", "", os.Getenv("DOCKER_SYNC_TARGET_REGISTRY_3_URL"), "Third registry URL for target image")
205+
206+
// For more registries, please use a configuration file
207+
}

main.go

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/Altinity/docker-sync/config"
88
"github.com/Altinity/docker-sync/internal/sync"
99
"github.com/Altinity/docker-sync/internal/telemetry"
10+
"github.com/Altinity/docker-sync/structs"
1011
"github.com/rs/zerolog/log"
1112
"go.uber.org/multierr"
1213
)
@@ -24,35 +25,43 @@ func Run(ctx context.Context) error {
2425

2526
images := config.SyncImages.Images()
2627

27-
var merr error
28-
2928
for {
30-
for k := range images {
31-
select {
32-
case <-ctx.Done():
33-
return nil
34-
default:
35-
image := images[k]
36-
37-
if err := sync.SyncImage(ctx, image); err != nil {
38-
log.Error().
39-
Err(err).
40-
Str("source", image.Source).
41-
Msg("Failed to sync image")
42-
43-
merr = multierr.Append(merr, err)
44-
45-
if config.SyncMaxErrors.Int() > 0 {
46-
if len(multierr.Errors(merr)) >= config.SyncMaxErrors.Int() {
47-
return merr
48-
}
49-
}
50-
}
51-
}
29+
if err := RunOnce(ctx, images); err != nil {
30+
return err
5231
}
5332

5433
dur := config.SyncInterval.Duration()
5534
log.Info().Dur("interval", dur).Msg("Waiting for next sync")
5635
time.Sleep(dur)
5736
}
5837
}
38+
39+
func RunOnce(ctx context.Context, images []*structs.Image) error {
40+
var merr error
41+
42+
for k := range images {
43+
select {
44+
case <-ctx.Done():
45+
return nil
46+
default:
47+
image := images[k]
48+
49+
if err := sync.SyncImage(ctx, image); err != nil {
50+
log.Error().
51+
Err(err).
52+
Str("source", image.Source).
53+
Msg("Failed to sync image")
54+
55+
merr = multierr.Append(merr, err)
56+
57+
if config.SyncMaxErrors.Int() > 0 {
58+
if len(multierr.Errors(merr)) >= config.SyncMaxErrors.Int() {
59+
return merr
60+
}
61+
}
62+
}
63+
}
64+
}
65+
66+
return nil
67+
}

reload.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ func Reload() {
1818
Msg("Failed to load configuration key, ignoring")
1919
} else if reloadedKeys[k].OldValue != nil {
2020
// Skip logging if the key was not previously set (i.e. it was loaded for the first time)
21-
log.Info().
22-
Str("key", reloadedKeys[k].Key).
23-
Interface("oldValue", reloadedKeys[k].OldValue).
24-
Interface("newValue", reloadedKeys[k].NewValue).
25-
Msg("Loaded configuration key")
21+
/*
22+
log.Info().
23+
Str("key", reloadedKeys[k].Key).
24+
Interface("oldValue", reloadedKeys[k].OldValue).
25+
Interface("newValue", reloadedKeys[k].NewValue).
26+
Msg("Loaded configuration key")
27+
*/
2628
}
2729
}
2830
}

0 commit comments

Comments
 (0)