Skip to content

Commit 6898c2c

Browse files
authored
Merge pull request #364 from rohanpm/commit-mode
Support different commit modes [RHELDST-20490]
2 parents d41c9a7 + 22a9651 commit 6898c2c

18 files changed

+277
-100
lines changed

README.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ exodus-aware drop-in replacement for rsync.
1010
- [Installation](#installation)
1111
- [Configuration](#configuration)
1212
- [Usage](#usage)
13-
- [Differences from rsync](#differences-from-rsync)
14-
- [Publish modes](#publish-modes)
15-
- [Standalone publish](#standalone-publish)
16-
- [Joined publish](#joined-publish)
13+
- [Differences from rsync](#differences-from-rsync)
14+
- [Publish modes](#publish-modes)
15+
- [Standalone publish](#standalone-publish)
16+
- [Joined publish](#joined-publish)
1717
- [License](#license)
1818

1919
<!-- /TOC -->
@@ -87,6 +87,25 @@ gwurl: https://exodus-gw.example.com
8787
# The exodus-gw environment may be set using an environment variable.
8888
gwenv: prod
8989

90+
# The commit mode for exodus-gw publish objects, one of the following:
91+
#
92+
# "" or "auto" (default):
93+
# Commit occurs only if exodus-rsync created the publish (i.e. is operating
94+
# in "standalone publish" mode). A phase2 (full) commit will be used.
95+
#
96+
# "none":
97+
# Commit never occurs, regardless of whether exodus-rsync created
98+
# the publish.
99+
#
100+
# "phase1", "phase2", <other>...:
101+
# A commit of this type occurs, regardless of whether exodus-rsync created
102+
# the publish. See exodus-gw documentation for details on the behavior
103+
# of each mode:
104+
# https://release-engineering.github.io/exodus-gw/api.html#operation/commit_publish__env__publish__publish_id__commit_post
105+
#
106+
# The `--exodus-commit=MODE` option overrides this value.
107+
gwcommit: auto
108+
90109
###############################################################################
91110
# Environment configuration
92111
###############################################################################
@@ -254,6 +273,7 @@ is a summary of the differences:
254273
| -------- | ----- |
255274
| --exodus-conf=PATH | use this configuration file |
256275
| --exodus-publish=ID | join content to an existing publish (see "Publish modes") |
276+
| --exodus-commit=MODE | commit mode for publish (see `gwcommit` in config file) |
257277
| --exodus-diag | diagnostic mode, outputs various info for troubleshooting |
258278

259279
- exodus-rsync supports only the following rsync arguments, most of which do not have any
@@ -362,9 +382,11 @@ exposed from the CDN or that *none* of them are exposed at all, even if we are i
362382
in the middle of publishing. None of the published content becomes visible from the CDN until
363383
the "commit" operation occurs, which exposes all content at once.
364384

385+
More complex scenarios are possible when specifying a custom commit mode via
386+
the `gwcommit` config file option or the `--exodus-commit` argument.
365387
See [the exodus-gw documentation](https://release-engineering.github.io/exodus-gw/api.html#section/Atomicity)
366-
for more information on the atomicity guarantees when publishing with
367-
exodus-rsync and exodus-gw.
388+
for more information on the supported commit modes and the atomicity
389+
guarantees when publishing with exodus-rsync and exodus-gw.
368390

369391
## License
370392

internal/args/parse.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ type ExodusConfig struct {
7575

7676
Publish string `help:"ID of existing exodus-gw publish to join." validate:"omitempty,uuid"`
7777

78+
Commit string `help:"Commit publish using this mode" validate:"omitempty,max=20"`
79+
7880
Diag bool `help:"Diagnostic mode, dumps various information about the environment."`
7981
}
8082

internal/cmd/cmd_gw_errors_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func setupFailedCommit(ctrl *gomock.Controller, client *gw.MockClient) {
6868
publish.EXPECT().AddItems(gomock.Any(), gomock.Any()).Return(nil)
6969

7070
// Committing fails
71-
publish.EXPECT().Commit(gomock.Any()).Return(fmt.Errorf("simulated error"))
71+
publish.EXPECT().Commit(gomock.Any(), gomock.Any()).Return(fmt.Errorf("simulated error"))
7272
}
7373

7474
func setupFailedJoinPublish(ctrl *gomock.Controller, client *gw.MockClient) {

internal/cmd/cmd_sync_test.go

Lines changed: 108 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ type FakeClient struct {
5555
}
5656

5757
type FakePublish struct {
58-
items []gw.ItemInput
59-
committed int
60-
id string
58+
items []gw.ItemInput
59+
committed int
60+
commitmodes []string
61+
frozen bool
62+
id string
6163
}
6264

6365
type BrokenPublish struct {
@@ -111,7 +113,7 @@ func (c *FakeClient) WhoAmI(context.Context) (map[string]interface{}, error) {
111113
}
112114

113115
func (p *FakePublish) AddItems(ctx context.Context, items []gw.ItemInput) error {
114-
if p.committed != 0 {
116+
if p.frozen {
115117
return fmt.Errorf("attempted to modify committed publish")
116118
}
117119
p.items = append(p.items, items...)
@@ -122,12 +124,16 @@ func (p *BrokenPublish) AddItems(_ context.Context, _ []gw.ItemInput) error {
122124
return fmt.Errorf("invalid publish")
123125
}
124126

125-
func (p *BrokenPublish) Commit(_ context.Context) error {
127+
func (p *BrokenPublish) Commit(_ context.Context, _ string) error {
126128
return fmt.Errorf("invalid publish")
127129
}
128130

129-
func (p *FakePublish) Commit(ctx context.Context) error {
131+
func (p *FakePublish) Commit(ctx context.Context, mode string) error {
132+
if mode == "" || mode == "phase2" {
133+
p.frozen = true
134+
}
130135
p.committed++
136+
p.commitmodes = append(p.commitmodes, mode)
131137
return nil
132138
}
133139

@@ -148,72 +154,102 @@ func TestMainTypicalSync(t *testing.T) {
148154
SetConfig(t, CONFIG)
149155
ctrl := MockController(t)
150156

151-
mockGw := gw.NewMockInterface(ctrl)
152-
ext.gw = mockGw
153-
154-
client := FakeClient{blobs: make(map[string]string)}
155-
mockGw.EXPECT().NewClient(gomock.Any(), EnvMatcher{"best-env"}).Return(&client, nil)
156-
157-
srcPath := path.Clean(wd + "/../../test/data/srctrees/just-files")
158-
159-
args := []string{
160-
"rsync",
161-
srcPath + "/",
162-
"exodus:/some/target",
163-
}
164-
165-
got := Main(args)
166-
167-
// It should complete successfully.
168-
if got != 0 {
169-
t.Error("returned incorrect exit code", got)
170-
}
171-
172-
// Check paths of some blobs we expected to deal with.
173-
binPath := client.blobs["c66f610d98b2c9fe0175a3e99ba64d7fc7de45046515ff325be56329a9347dd6"]
174-
helloPath := client.blobs["5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"]
175-
176-
// It should have uploaded the binary from here
177-
if binPath != srcPath+"/subdir/some-binary" {
178-
t.Error("binary uploaded from unexpected path", binPath)
179-
}
180-
181-
// For the hello file, since there were two copies, it's undefined which one of them
182-
// was used for the upload - but should be one of them.
183-
if helloPath != srcPath+"/hello-copy-one" && helloPath != srcPath+"/hello-copy-two" {
184-
t.Error("hello uploaded from unexpected path", helloPath)
185-
}
186-
187-
// It should have created one publish.
188-
if len(client.publishes) != 1 {
189-
t.Error("expected to create 1 publish, instead created", len(client.publishes))
190-
}
191-
192-
p := client.publishes[0]
193-
194-
// Build up a URI => Key mapping of what was published
195-
itemMap := make(map[string]string)
196-
for _, item := range p.items {
197-
if _, ok := itemMap[item.WebURI]; ok {
198-
t.Error("tried to publish this URI more than once:", item.WebURI)
199-
}
200-
itemMap[item.WebURI] = item.ObjectKey
201-
}
202-
203-
// It should have been exactly this
204-
expectedItems := map[string]string{
205-
"/some/target/subdir/some-binary": "c66f610d98b2c9fe0175a3e99ba64d7fc7de45046515ff325be56329a9347dd6",
206-
"/some/target/hello-copy-one": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
207-
"/some/target/hello-copy-two": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
208-
}
209-
210-
if !reflect.DeepEqual(itemMap, expectedItems) {
211-
t.Error("did not publish expected items, published:", itemMap)
212-
}
213-
214-
// It should have committed the publish (once)
215-
if p.committed != 1 {
216-
t.Error("expected to commit publish (once), instead p.committed ==", p.committed)
157+
tests := []struct {
158+
name string
159+
commitArg string
160+
expectedCommits int
161+
expectedCommitMode string
162+
}{
163+
{"typical", "", 1, ""},
164+
165+
{"explicit autocommit", "--exodus-commit=auto", 1, ""},
166+
167+
{"no commit", "--exodus-commit=none", 0, ""},
168+
169+
{"commit specified mode", "--exodus-commit=xyz", 1, "xyz"},
170+
}
171+
172+
for _, tt := range tests {
173+
t.Run(tt.name, func(t *testing.T) {
174+
mockGw := gw.NewMockInterface(ctrl)
175+
ext.gw = mockGw
176+
177+
client := FakeClient{blobs: make(map[string]string)}
178+
mockGw.EXPECT().NewClient(gomock.Any(), EnvMatcher{"best-env"}).Return(&client, nil)
179+
180+
srcPath := path.Clean(wd + "/../../test/data/srctrees/just-files")
181+
182+
args := []string{
183+
"rsync",
184+
srcPath + "/",
185+
}
186+
187+
if tt.commitArg != "" {
188+
args = append(args, tt.commitArg)
189+
}
190+
args = append(args, "exodus:/some/target")
191+
192+
got := Main(args)
193+
194+
// It should complete successfully.
195+
if got != 0 {
196+
t.Error("returned incorrect exit code", got)
197+
}
198+
199+
// Check paths of some blobs we expected to deal with.
200+
binPath := client.blobs["c66f610d98b2c9fe0175a3e99ba64d7fc7de45046515ff325be56329a9347dd6"]
201+
helloPath := client.blobs["5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"]
202+
203+
// It should have uploaded the binary from here
204+
if binPath != srcPath+"/subdir/some-binary" {
205+
t.Error("binary uploaded from unexpected path", binPath)
206+
}
207+
208+
// For the hello file, since there were two copies, it's undefined which one of them
209+
// was used for the upload - but should be one of them.
210+
if helloPath != srcPath+"/hello-copy-one" && helloPath != srcPath+"/hello-copy-two" {
211+
t.Error("hello uploaded from unexpected path", helloPath)
212+
}
213+
214+
// It should have created one publish.
215+
if len(client.publishes) != 1 {
216+
t.Error("expected to create 1 publish, instead created", len(client.publishes))
217+
}
218+
219+
p := client.publishes[0]
220+
221+
// Build up a URI => Key mapping of what was published
222+
itemMap := make(map[string]string)
223+
for _, item := range p.items {
224+
if _, ok := itemMap[item.WebURI]; ok {
225+
t.Error("tried to publish this URI more than once:", item.WebURI)
226+
}
227+
itemMap[item.WebURI] = item.ObjectKey
228+
}
229+
230+
// It should have been exactly this
231+
expectedItems := map[string]string{
232+
"/some/target/subdir/some-binary": "c66f610d98b2c9fe0175a3e99ba64d7fc7de45046515ff325be56329a9347dd6",
233+
"/some/target/hello-copy-one": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
234+
"/some/target/hello-copy-two": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
235+
}
236+
237+
if !reflect.DeepEqual(itemMap, expectedItems) {
238+
t.Error("did not publish expected items, published:", itemMap)
239+
}
240+
241+
// It should have committed (or not) as expected
242+
if p.committed != tt.expectedCommits {
243+
t.Error("expected ", tt.expectedCommits, " commits, got ", p.committed)
244+
}
245+
246+
// If a commit happened at all, it should have used the mode we expect.
247+
if tt.expectedCommits != 0 {
248+
if p.commitmodes[0] != tt.expectedCommitMode {
249+
t.Error("expected ", tt.expectedCommitMode, " commit mode, got ", p.commitmodes[0])
250+
}
251+
}
252+
})
217253
}
218254
}
219255

internal/cmd/exodus.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,28 @@ func webURI(srcPath string, srcTree string, destTree string, srcIsDir bool) stri
5858
return path.Join(destTree, relPath)
5959
}
6060

61+
func commitMode(cfg conf.Config, args args.Config) (bool, string) {
62+
// Calculates effective commit mode for current run, given arguments and config.
63+
//
64+
// Returns:
65+
// bool: true if commit should happen at all
66+
// string: the commit_mode argument which should be passed to exodus-gw
67+
// (can be empty if no argument should be passed)
68+
mode := cfg.GwCommit()
69+
if mode == "" || mode == "auto" {
70+
// 'auto' means commit with server-default mode if and only if we
71+
// created the publish object during this run, which we did if no
72+
// publish ID was passed in args.
73+
return args.Publish == "", ""
74+
}
75+
if mode == "none" {
76+
// 'none' means don't ever commit
77+
return false, ""
78+
}
79+
// Anything else means commit, using the given value as the commit mode
80+
return true, mode
81+
}
82+
6183
func exodusMain(ctx context.Context, cfg conf.Config, args args.Config) int {
6284
logger := log.FromContext(ctx)
6385

@@ -223,10 +245,10 @@ func exodusMain(ctx context.Context, cfg conf.Config, args args.Config) int {
223245

224246
logger.F("publish", publish.ID(), "items", len(publishItems)).Info("Added publish items")
225247

226-
if args.Publish == "" {
227-
// We created the publish, then we should commit it.
228-
logger.F("publish", publish.ID()).Info("Preparing to commit publish")
229-
err = publish.Commit(ctx)
248+
shouldCommit, mode := commitMode(cfg, args)
249+
if shouldCommit {
250+
logger.F("publish", publish.ID(), "mode", mode).Info("Preparing to commit publish")
251+
err = publish.Commit(ctx, mode)
230252
if err != nil {
231253
logger.F("error", err).Error("can't commit publish")
232254
return 71

internal/conf/conf.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ type Config interface {
4040
// Max number of items to include in a single HTTP request to exodus-gw.
4141
GwBatchSize() int
4242

43+
// Commit mode for publishes.
44+
GwCommit() string
45+
4346
// Execution mode for rsync.
4447
RsyncMode() string
4548

internal/conf/conf_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ gwurl: $TEST_EXODUS_GW_URL
3232
gwcert: global-cert
3333
gwkey: global-key
3434
gwbatchsize: 100
35+
gwcommit: abc
3536
strip: dest:/foo
3637
3738
environments:
3839
- prefix: dest:/foo/bar/baz
3940
gwenv: $TEST_EXODUS_GW_ENV
4041
gwkey: override-key
4142
gwpollinterval: 123
43+
gwcommit: cba
4244
rsyncmode: mixed
4345
strip: dest:/foo/bar
4446
uploadthreads: 6
@@ -97,6 +99,7 @@ environments:
9799
assertEqual("global gwkey", cfg.GwKey(), "global-key")
98100
assertEqual("global gwenv", cfg.GwEnv(), "global-env")
99101
assertEqual("global gwpollinterval", cfg.GwPollInterval(), 5000)
102+
assertEqual("global gwcommit", cfg.GwCommit(), "abc")
100103
assertEqual("global rsyncmode", cfg.RsyncMode(), "exodus")
101104
assertEqual("global strip", cfg.Strip(), "dest:/foo")
102105
assertEqual("global uploadthreads", cfg.UploadThreads(), 4)
@@ -105,6 +108,7 @@ environments:
105108
assertEqual("env gwenv", env.GwEnv(), "one-env")
106109
assertEqual("env gwkey", env.GwKey(), "override-key")
107110
assertEqual("env gwpollinterval", env.GwPollInterval(), 123)
111+
assertEqual("env gwcommit", env.GwCommit(), "cba")
108112
assertEqual("env rsyncmode", env.RsyncMode(), "mixed")
109113
assertEqual("env strip", env.Strip(), "dest:/foo/bar")
110114
assertEqual("env uploadthreads", env.UploadThreads(), 6)

0 commit comments

Comments
 (0)