From 5b51b33749fff47e9c982b83f1bea2d69c7e5786 Mon Sep 17 00:00:00 2001 From: Erwan Guyader Date: Wed, 14 Feb 2024 11:25:06 +0100 Subject: [PATCH 1/2] feat: Add flags helpers for lists flags Simplify the usage of lists flags with typed return values. Lists flags are defined like this: ```json "": { "list": ["", ...] } ``` --- model/feature/flag.go | 32 ++++++++++++++++++++++++++++++++ model/feature/flag_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/model/feature/flag.go b/model/feature/flag.go index fae8036be21..ce442032739 100644 --- a/model/feature/flag.go +++ b/model/feature/flag.go @@ -71,6 +71,38 @@ func (f *Flags) UnmarshalJSON(bytes []byte) error { return nil } +func (f *Flags) GetList(name string) ([]interface{}, error) { + if f.M[name] == nil { + return []interface{}{}, nil + } + + value, ok := f.M[name].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("Flag %s is not a list flag", name) + } + + list, ok := value["list"].([]interface{}) + if !ok { + return nil, fmt.Errorf("Flag %s is not a list flag", name) + } + + return list, nil +} + +func (f *Flags) HasListItem(name, item string) (bool, error) { + list, err := f.GetList(name) + if err != nil { + return false, err + } + + for _, i := range list { + if i == item { + return true, nil + } + } + return false, nil +} + // GetFlags returns the list of feature flags for the given instance. func GetFlags(inst *instance.Instance) (*Flags, error) { sources := make([]*Flags, 0) diff --git a/model/feature/flag_test.go b/model/feature/flag_test.go index 9bf41183463..1e0f83f1c8f 100644 --- a/model/feature/flag_test.go +++ b/model/feature/flag_test.go @@ -8,6 +8,7 @@ import ( "github.com/cozy/cozy-stack/model/instance" "github.com/gofrs/uuid/v5" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func uuidv7() string { @@ -38,3 +39,40 @@ func TestFeatureFlagRatio(t *testing.T) { assert.InDelta(t, 4000, results[float64(4)], 100) assert.InDelta(t, 3000, results[nil], 100) } + +func TestFeatureFlagList(t *testing.T) { + var flags Flags + err := json.Unmarshal( + []byte(`{ + "flag1": { "list": ["other", "val"] }, + "flag2": { "list": ["other"] }, + "flag3": { "list": [] }, + "flag4": ["val"], + "flag5": "val" + }`), + &flags, + ) + require.NoError(t, err) + + // GetList + list, err := flags.GetList("flag1") + require.NoError(t, err) + assert.EqualValues(t, []interface{}{"other", "val"}, list) + _, err = flags.GetList("flag4") + assert.Error(t, err) + _, err = flags.GetList("flag5") + assert.Error(t, err) + + // HasListItem + hasVal, err := flags.HasListItem("flag1", "val") + require.NoError(t, err) + assert.True(t, hasVal) + hasVal, _ = flags.HasListItem("flag2", "val") + assert.False(t, hasVal) + hasVal, _ = flags.HasListItem("flag3", "val") + assert.False(t, hasVal) + _, err = flags.HasListItem("flag4", "val") + assert.Error(t, err) + _, err = flags.HasListItem("flag5", "val") + assert.Error(t, err) +} From 4c01187ab844d55913dd20dca6ac41553a8f90b0 Mon Sep 17 00:00:00 2001 From: Erwan Guyader Date: Thu, 15 Feb 2024 18:42:43 +0100 Subject: [PATCH 2/2] feat: Konnector worker honors skip-maintenance-for The `harvest.skip-maintenance-for` flag can be set to a list of konnector slugs to unlock these in Harvest and allow requesting an execution even if it's maintenance mode has been activated either in the registry or cozy-stack. However, the flag was not honored in the konnector worker thus preventing its execution. We'll now check if the konnector to be executed is part of the list (if set) and skip the maintenance mode when appropriate. --- worker/exec/konnector.go | 41 ++++++++++++++++++++++++++++------- worker/exec/konnector_test.go | 37 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/worker/exec/konnector.go b/worker/exec/konnector.go index b3a66126f09..1753a97214b 100644 --- a/worker/exec/konnector.go +++ b/worker/exec/konnector.go @@ -15,6 +15,7 @@ import ( "github.com/cozy/cozy-stack/model/account" "github.com/cozy/cozy-stack/model/app" + "github.com/cozy/cozy-stack/model/feature" "github.com/cozy/cozy-stack/model/instance" "github.com/cozy/cozy-stack/model/instance/lifecycle" "github.com/cozy/cozy-stack/model/job" @@ -126,6 +127,21 @@ func beforeHookKonnector(j *job.Job) (bool, error) { if err := json.Unmarshal(j.Message, &msg); err == nil { slug = msg.Konnector + + inst, err := lifecycle.GetInstance(j.DomainName()) + if err != nil { + return false, err + } + + flags, err := feature.GetFlags(inst) + if err != nil { + return false, err + } + skipMaintenance, err := flags.HasListItem("harvest.skip-maintenance-for", slug) + if err != nil { + return false, err + } + doc, err := app.GetMaintenanceOptions(slug) if err != nil { j.Logger().Warnf("konnector %q could not get local maintenance status", slug) @@ -136,13 +152,16 @@ func beforeHookKonnector(j *job.Job) (bool, error) { return true, nil } } - j.Logger().Infof("konnector %q has not been triggered because of its maintenance status", slug) - return false, nil - } - inst, err := lifecycle.GetInstance(j.DomainName()) - if err != nil { - return false, err + + if skipMaintenance { + j.Logger().Infof("skipping konnector %q's maintenance", slug) + return true, nil + } else { + j.Logger().Infof("konnector %q has not been triggered because of its maintenance status", slug) + return false, nil + } } + app, err := registry.GetApplication(slug, inst.Registries()) if err != nil { j.Logger().Warnf("konnector %q could not get application to fetch maintenance status", slug) @@ -150,8 +169,14 @@ func beforeHookKonnector(j *job.Job) (bool, error) { if j.Manual && !app.MaintenanceOptions.FlagDisallowManualExec { return true, nil } - j.Logger().Infof("konnector %q has not been triggered because of its maintenance status", slug) - return false, nil + + if skipMaintenance { + j.Logger().Infof("skipping konnector %q's maintenance", slug) + return true, nil + } else { + j.Logger().Infof("konnector %q has not been triggered because of its maintenance status", slug) + return false, nil + } } if msg.BIWebhook { diff --git a/worker/exec/konnector_test.go b/worker/exec/konnector_test.go index 07e55698c4f..9f672ddefe7 100644 --- a/worker/exec/konnector_test.go +++ b/worker/exec/konnector_test.go @@ -442,6 +442,43 @@ echo "{\"type\": \"toto\", \"message\": \"COZY_URL=${COZY_URL}\"}" }) } +func TestBeforeHookKonnector(t *testing.T) { + if testing.Short() { + t.Skip("a couchdb is required for this test: test skipped due to the use of --short flag") + } + + config.UseTestFile(t) + require.NoError(t, loadLocale(), "Could not load default locale translations") + + setup := testutils.NewSetup(t, t.Name()) + slug, err := setup.InstallMiniKonnector() + require.NoError(t, err) + + inst := setup.GetTestInstance() + + t.Run("stack maintenance", func(t *testing.T) { + err := app.ActivateMaintenance(slug, nil) + require.NoError(t, err) + + msg, err := job.NewMessage(map[string]interface{}{ + "konnector": slug, + }) + require.NoError(t, err) + + j := job.NewJob(inst, &job.JobRequest{ + Message: msg, + WorkerType: "konnector", + }) + + shouldExec, err := beforeHookKonnector(j) + assert.False(t, shouldExec) + + testutils.WithFlag(t, inst, "harvest.skip-maintenance-for", map[string]interface{}{"list": []string{slug}}) + shouldExec, err = beforeHookKonnector(j) + assert.True(t, shouldExec) + }) +} + func loadLocale() error { locale := consts.DefaultLocale assetsPath := config.GetConfig().Assets