diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f1135c8fb..00bb0891ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ### Features 1. [17934](https://github.com/influxdata/influxdb/pull/17934): Add ability to delete a stack and all the resources associated with it -1. [17941](https://github.com/influxdata/influxdb/pull/17941): Encorce dns name compliance on all pkger resources' metadata.name field +1. [17941](https://github.com/influxdata/influxdb/pull/17941): Enforce DNS name compliance on all pkger resources' metadata.name field +1. [17989](https://github.com/influxdata/influxdb/pull/17989): Add stateful pkg management with stacks ### Bug Fixes @@ -31,6 +32,7 @@ 1. [17714](https://github.com/influxdata/influxdb/pull/17714): Cloud environments no longer render markdown images, for security reasons. 1. [17321](https://github.com/influxdata/influxdb/pull/17321): Improve UI for sorting resources 1. [17740](https://github.com/influxdata/influxdb/pull/17740): Add single-color color schemes for visualizations +1. [17849](https://github.com/influxdata/influxdb/pull/17849): Move Organization navigation items to user menu. ## v2.0.0-beta.8 [2020-04-10] diff --git a/cmd/influxd/launcher/pkger_test.go b/cmd/influxd/launcher/pkger_test.go index 3601555d5f..6a2f34a030 100644 --- a/cmd/influxd/launcher/pkger_test.go +++ b/cmd/influxd/launcher/pkger_test.go @@ -7,6 +7,7 @@ import ( "io/ioutil" nethttp "net/http" "net/http/httptest" + "sort" "testing" "time" @@ -591,7 +592,7 @@ func TestLauncher_Pkger(t *testing.T) { }) }) - t.Run("apply a pkg with a stack", func(t *testing.T) { + t.Run("apply a pkg with a stack and all resources", func(t *testing.T) { testStackApplyFn := func(t *testing.T) (pkger.Summary, pkger.Stack, func()) { t.Helper() @@ -1038,6 +1039,365 @@ func TestLauncher_Pkger(t *testing.T) { } }) }) + + t.Run("apply should handle cases where users have changed platform data", func(t *testing.T) { + initializeStackPkg := func(t *testing.T, pkg *pkger.Pkg) (influxdb.ID, func(), pkger.Summary) { + t.Helper() + + stack, cleanup := newStackFn(t, pkger.Stack{}) + defer func() { + if t.Failed() { + cleanup() + } + }() + + initialSum, _, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkg, pkger.ApplyWithStackID(stack.ID)) + require.NoError(t, err) + + return stack.ID, cleanup, initialSum + } + + testValidRemoval := func(t *testing.T, stackID influxdb.ID) { + t.Helper() + _, _, err := svc.Apply( + ctx, + l.Org.ID, + l.User.ID, + newPkg( /* empty stack to remove prev resource */ ), + pkger.ApplyWithStackID(stackID), + ) + require.NoError(t, err) + } + + t.Run("when a user has deleted a variable that was previously created by a stack", func(t *testing.T) { + testUserDeletedVariable := func(t *testing.T, actionFn func(t *testing.T, stackID influxdb.ID, initialVarObj pkger.Object, initialSum pkger.Summary)) { + t.Helper() + + obj := newVariableObject("var-1", "", "") + stackID, cleanup, initialSum := initializeStackPkg(t, newPkg(obj)) + defer cleanup() + + require.Len(t, initialSum.Variables, 1) + require.NotZero(t, initialSum.Variables[0].ID) + resourceCheck.mustDeleteVariable(t, influxdb.ID(initialSum.Variables[0].ID)) + + actionFn(t, stackID, obj, initialSum) + } + + t.Run("should create new resource when attempting to update", func(t *testing.T) { + testUserDeletedVariable(t, func(t *testing.T, stackID influxdb.ID, initialVarObj pkger.Object, initialSum pkger.Summary) { + pkg := newPkg(initialVarObj) + updateSum, _, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkg, pkger.ApplyWithStackID(stackID)) + require.NoError(t, err) + + require.Len(t, updateSum.Variables, 1) + initVar, updateVar := initialSum.Variables[0], updateSum.Variables[0] + assert.NotEqual(t, initVar.ID, updateVar.ID) + initVar.ID, updateVar.ID = 0, 0 + assert.Equal(t, initVar, updateVar) + }) + }) + + t.Run("should not error when attempting to remove", func(t *testing.T) { + testUserDeletedVariable(t, func(t *testing.T, stackID influxdb.ID, initialVarObj pkger.Object, initialSum pkger.Summary) { + testValidRemoval(t, stackID) + }) + }) + }) + + t.Run("when a user has deleted a bucket that was previously created by a stack", func(t *testing.T) { + testUserDeletedBucket := func(t *testing.T, actionFn func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary)) { + t.Helper() + + obj := newBucketObject("bucket-1", "", "") + stackID, cleanup, initialSum := initializeStackPkg(t, newPkg(obj)) + defer cleanup() + + require.Len(t, initialSum.Buckets, 1) + require.NotZero(t, initialSum.Buckets[0].ID) + resourceCheck.mustDeleteBucket(t, influxdb.ID(initialSum.Buckets[0].ID)) + + actionFn(t, stackID, obj, initialSum) + } + + t.Run("should create new resource when attempting to update", func(t *testing.T) { + testUserDeletedBucket(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + pkg := newPkg(initialObj) + updateSum, _, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkg, pkger.ApplyWithStackID(stackID)) + require.NoError(t, err) + + require.Len(t, updateSum.Buckets, 1) + intial, updated := initialSum.Buckets[0], updateSum.Buckets[0] + assert.NotEqual(t, intial.ID, updated.ID) + intial.ID, updated.ID = 0, 0 + assert.Equal(t, intial, updated) + }) + }) + + t.Run("should not error when attempting to remove", func(t *testing.T) { + testUserDeletedBucket(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + testValidRemoval(t, stackID) + }) + }) + }) + + t.Run("when a user has deleted a check that was previously created by a stack", func(t *testing.T) { + testUserDeletedCheck := func(t *testing.T, actionFn func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary)) { + t.Helper() + + obj := newCheckDeadmanObject(t, "check-1", "", time.Hour) + stackID, cleanup, initialSum := initializeStackPkg(t, newPkg(obj)) + defer cleanup() + + require.Len(t, initialSum.Checks, 1) + require.NotZero(t, initialSum.Checks[0].Check.GetID()) + resourceCheck.mustDeleteCheck(t, initialSum.Checks[0].Check.GetID()) + + actionFn(t, stackID, obj, initialSum) + } + + t.Run("should create new resource when attempting to update", func(t *testing.T) { + testUserDeletedCheck(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + pkg := newPkg(initialObj) + updateSum, _, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkg, pkger.ApplyWithStackID(stackID)) + require.NoError(t, err) + + require.Len(t, updateSum.Checks, 1) + intial, updated := initialSum.Checks[0].Check, updateSum.Checks[0].Check + assert.NotEqual(t, intial.GetID(), updated.GetID()) + intial.SetID(0) + updated.SetID(0) + assert.Equal(t, intial, updated) + }) + }) + + t.Run("should not error when attempting to remove", func(t *testing.T) { + testUserDeletedCheck(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + testValidRemoval(t, stackID) + }) + }) + }) + + t.Run("when a user has deleted a dashboard that was previously created by a stack", func(t *testing.T) { + testUserDeletedDashboard := func(t *testing.T, actionFn func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary)) { + t.Helper() + + obj := newDashObject("dash-1", "", "") + stackID, cleanup, initialSum := initializeStackPkg(t, newPkg(obj)) + defer cleanup() + + require.Len(t, initialSum.Dashboards, 1) + require.NotZero(t, initialSum.Dashboards[0].ID) + resourceCheck.mustDeleteDashboard(t, influxdb.ID(initialSum.Dashboards[0].ID)) + + actionFn(t, stackID, obj, initialSum) + } + + t.Run("should create new resource when attempting to update", func(t *testing.T) { + testUserDeletedDashboard(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + pkg := newPkg(initialObj) + updateSum, _, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkg, pkger.ApplyWithStackID(stackID)) + require.NoError(t, err) + + require.Len(t, updateSum.Dashboards, 1) + initial, updated := initialSum.Dashboards[0], updateSum.Dashboards[0] + assert.NotEqual(t, initial.ID, updated.ID) + initial.ID, updated.ID = 0, 0 + assert.Equal(t, initial, updated) + }) + }) + + t.Run("should not error when attempting to remove", func(t *testing.T) { + testUserDeletedDashboard(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + testValidRemoval(t, stackID) + }) + }) + }) + + t.Run("when a user has deleted a label that was previously created by a stack", func(t *testing.T) { + testUserDeletedLabel := func(t *testing.T, actionFn func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary)) { + t.Helper() + + obj := newLabelObject("label-1", "", "", "") + stackID, cleanup, initialSum := initializeStackPkg(t, newPkg(obj)) + defer cleanup() + + require.Len(t, initialSum.Labels, 1) + require.NotZero(t, initialSum.Labels[0].ID) + resourceCheck.mustDeleteLabel(t, influxdb.ID(initialSum.Labels[0].ID)) + + actionFn(t, stackID, obj, initialSum) + } + + t.Run("should create new resource when attempting to update", func(t *testing.T) { + testUserDeletedLabel(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + pkg := newPkg(initialObj) + updateSum, _, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkg, pkger.ApplyWithStackID(stackID)) + require.NoError(t, err) + + require.Len(t, updateSum.Labels, 1) + initial, updated := initialSum.Labels[0], updateSum.Labels[0] + assert.NotEqual(t, initial.ID, updated.ID) + initial.ID, updated.ID = 0, 0 + assert.Equal(t, initial, updated) + }) + }) + + t.Run("should not error when attempting to remove", func(t *testing.T) { + testUserDeletedLabel(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + testValidRemoval(t, stackID) + }) + }) + }) + + t.Run("when a user has deleted a notification endpoint that was previously created by a stack", func(t *testing.T) { + testUserDeletedEndpoint := func(t *testing.T, actionFn func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary)) { + t.Helper() + + obj := newEndpointHTTP("endpoint-1", "", "") + stackID, cleanup, initialSum := initializeStackPkg(t, newPkg(obj)) + defer cleanup() + + require.Len(t, initialSum.NotificationEndpoints, 1) + require.NotZero(t, initialSum.NotificationEndpoints[0].NotificationEndpoint.GetID()) + resourceCheck.mustDeleteEndpoint(t, initialSum.NotificationEndpoints[0].NotificationEndpoint.GetID()) + + actionFn(t, stackID, obj, initialSum) + } + + t.Run("should create new resource when attempting to update", func(t *testing.T) { + testUserDeletedEndpoint(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + pkg := newPkg(initialObj) + updateSum, _, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkg, pkger.ApplyWithStackID(stackID)) + require.NoError(t, err) + + require.Len(t, updateSum.NotificationEndpoints, 1) + initial, updated := initialSum.NotificationEndpoints[0].NotificationEndpoint, updateSum.NotificationEndpoints[0].NotificationEndpoint + assert.NotEqual(t, initial.GetID(), updated.GetID()) + initial.SetID(0) + updated.SetID(0) + assert.Equal(t, initial, updated) + }) + }) + + t.Run("should not error when attempting to remove", func(t *testing.T) { + testUserDeletedEndpoint(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + testValidRemoval(t, stackID) + }) + }) + }) + + t.Run("when a user has deleted a notification rule that was previously created by a stack", func(t *testing.T) { + testUserDeletedRule := func(t *testing.T, actionFn func(t *testing.T, stackID influxdb.ID, initialObjects []pkger.Object, initialSum pkger.Summary)) { + t.Helper() + + endpointObj := newEndpointHTTP("endpoint-1", "", "") + ruleObj := newRuleObject(t, "rule-0", "", endpointObj.Name(), "") + stackID, cleanup, initialSum := initializeStackPkg(t, newPkg(endpointObj, ruleObj)) + defer cleanup() + + require.Len(t, initialSum.NotificationEndpoints, 1) + require.NotZero(t, initialSum.NotificationEndpoints[0].NotificationEndpoint.GetID()) + require.Len(t, initialSum.NotificationRules, 1) + require.NotZero(t, initialSum.NotificationRules[0].ID) + resourceCheck.mustDeleteRule(t, influxdb.ID(initialSum.NotificationRules[0].ID)) + + actionFn(t, stackID, []pkger.Object{ruleObj, endpointObj}, initialSum) + } + + t.Run("should create new resource when attempting to update", func(t *testing.T) { + testUserDeletedRule(t, func(t *testing.T, stackID influxdb.ID, initialObjects []pkger.Object, initialSum pkger.Summary) { + pkg := newPkg(initialObjects...) + updateSum, _, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkg, pkger.ApplyWithStackID(stackID)) + require.NoError(t, err) + + require.Len(t, updateSum.NotificationRules, 1) + initial, updated := initialSum.NotificationRules[0], updateSum.NotificationRules[0] + assert.NotEqual(t, initial.ID, updated.ID) + initial.ID, updated.ID = 0, 0 + assert.Equal(t, initial, updated) + }) + }) + + t.Run("should not error when attempting to remove", func(t *testing.T) { + testUserDeletedRule(t, func(t *testing.T, stackID influxdb.ID, _ []pkger.Object, _ pkger.Summary) { + testValidRemoval(t, stackID) + }) + }) + }) + + t.Run("when a user has deleted a task that was previously created by a stack", func(t *testing.T) { + testUserDeletedTask := func(t *testing.T, actionFn func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary)) { + t.Helper() + + obj := newTaskObject("task-1", "", "") + stackID, cleanup, initialSum := initializeStackPkg(t, newPkg(obj)) + defer cleanup() + + require.Len(t, initialSum.Tasks, 1) + require.NotZero(t, initialSum.Tasks[0].ID) + resourceCheck.mustDeleteTask(t, influxdb.ID(initialSum.Tasks[0].ID)) + + actionFn(t, stackID, obj, initialSum) + } + + t.Run("should create new resource when attempting to update", func(t *testing.T) { + testUserDeletedTask(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + pkg := newPkg(initialObj) + updateSum, _, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkg, pkger.ApplyWithStackID(stackID)) + require.NoError(t, err) + + require.Len(t, updateSum.Tasks, 1) + initial, updated := initialSum.Tasks[0], updateSum.Tasks[0] + assert.NotEqual(t, initial.ID, updated.ID) + initial.ID, updated.ID = 0, 0 + assert.Equal(t, initial, updated) + }) + }) + + t.Run("should not error when attempting to remove", func(t *testing.T) { + testUserDeletedTask(t, func(t *testing.T, stackID influxdb.ID, _ pkger.Object, _ pkger.Summary) { + testValidRemoval(t, stackID) + }) + }) + }) + + t.Run("when a user has deleted a telegraf config that was previously created by a stack", func(t *testing.T) { + testUserDeletedTelegraf := func(t *testing.T, actionFn func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary)) { + t.Helper() + + obj := newTelegrafObject("tele-1", "", "") + stackID, cleanup, initialSum := initializeStackPkg(t, newPkg(obj)) + defer cleanup() + + require.Len(t, initialSum.TelegrafConfigs, 1) + require.NotZero(t, initialSum.TelegrafConfigs[0].TelegrafConfig.ID) + resourceCheck.mustDeleteTelegrafConfig(t, initialSum.TelegrafConfigs[0].TelegrafConfig.ID) + + actionFn(t, stackID, obj, initialSum) + } + + t.Run("should create new resource when attempting to update", func(t *testing.T) { + testUserDeletedTelegraf(t, func(t *testing.T, stackID influxdb.ID, initialObj pkger.Object, initialSum pkger.Summary) { + pkg := newPkg(initialObj) + updateSum, _, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkg, pkger.ApplyWithStackID(stackID)) + require.NoError(t, err) + + require.Len(t, updateSum.TelegrafConfigs, 1) + initial, updated := initialSum.TelegrafConfigs[0].TelegrafConfig, updateSum.TelegrafConfigs[0].TelegrafConfig + assert.NotEqual(t, initial.ID, updated.ID) + initial.ID, updated.ID = 0, 0 + assert.Equal(t, initial, updated) + }) + }) + + t.Run("should not error when attempting to remove", func(t *testing.T) { + testUserDeletedTelegraf(t, func(t *testing.T, stackID influxdb.ID, _ pkger.Object, _ pkger.Summary) { + testValidRemoval(t, stackID) + }) + }) + }) + }) }) t.Run("errors incurred during application of package rolls back to state before package", func(t *testing.T) { @@ -1386,6 +1746,7 @@ spec: labels := sum.Labels require.Len(t, labels, 2) + sortLabels(labels) assert.Equal(t, "label-1", labels[0].Name) assert.Equal(t, "the 2nd label", labels[1].Name) @@ -1397,6 +1758,7 @@ spec: checks := sum.Checks require.Len(t, checks, 2) + sortChecks(checks) assert.Equal(t, "check 0 name", checks[0].Check.GetName()) hasLabelAssociations(t, checks[0].LabelAssociations, 1, "label-1") assert.Equal(t, "check-1", checks[1].Check.GetName()) @@ -1691,6 +2053,7 @@ spec: labels := newSum.Labels require.Len(t, labels, 2) + sortLabels(labels) assert.Zero(t, labels[0].ID) assert.Equal(t, "label-1", labels[0].Name) assert.Zero(t, labels[1].ID) @@ -1704,6 +2067,7 @@ spec: checks := newSum.Checks require.Len(t, checks, 2) + sortChecks(checks) assert.Equal(t, "check 0 name", checks[0].Check.GetName()) hasLabelAssociations(t, checks[0].LabelAssociations, 1, "label-1") assert.Equal(t, "check-1", checks[1].Check.GetName()) @@ -2438,6 +2802,11 @@ func (r resourceChecker) mustGetBucket(t *testing.T, getOpt getResourceOptFn) in return bkt } +func (r resourceChecker) mustDeleteBucket(t *testing.T, id influxdb.ID) { + t.Helper() + require.NoError(t, r.tl.BucketService(t).DeleteBucket(ctx, id)) +} + func (r resourceChecker) getCheck(t *testing.T, getOpt getResourceOptFn) (influxdb.Check, error) { t.Helper() @@ -2470,6 +2839,12 @@ func (r resourceChecker) mustGetCheck(t *testing.T, getOpt getResourceOptFn) inf return c } +func (r resourceChecker) mustDeleteCheck(t *testing.T, id influxdb.ID) { + t.Helper() + + require.NoError(t, r.tl.CheckService().DeleteCheck(ctx, id)) +} + func (r resourceChecker) getDashboard(t *testing.T, getOpt getResourceOptFn) (influxdb.Dashboard, error) { t.Helper() @@ -2515,6 +2890,12 @@ func (r resourceChecker) mustGetDashboard(t *testing.T, getOpt getResourceOptFn) return dash } +func (r resourceChecker) mustDeleteDashboard(t *testing.T, id influxdb.ID) { + t.Helper() + + require.NoError(t, r.tl.DashboardService(t).DeleteDashboard(ctx, id)) +} + func (r resourceChecker) getEndpoint(t *testing.T, getOpt getResourceOptFn) (influxdb.NotificationEndpoint, error) { t.Helper() @@ -2557,6 +2938,13 @@ func (r resourceChecker) mustGetEndpoint(t *testing.T, getOpt getResourceOptFn) return e } +func (r resourceChecker) mustDeleteEndpoint(t *testing.T, id influxdb.ID) { + t.Helper() + + _, _, err := r.tl.NotificationEndpointService(t).DeleteNotificationEndpoint(ctx, id) + require.NoError(t, err) +} + func (r resourceChecker) getLabel(t *testing.T, getOpt getResourceOptFn) (influxdb.Label, error) { t.Helper() @@ -2603,6 +2991,11 @@ func (r resourceChecker) mustGetLabel(t *testing.T, getOpt getResourceOptFn) inf return l } +func (r resourceChecker) mustDeleteLabel(t *testing.T, id influxdb.ID) { + t.Helper() + require.NoError(t, r.tl.LabelService(t).DeleteLabel(ctx, id)) +} + func (r resourceChecker) getRule(t *testing.T, getOpt getResourceOptFn) (influxdb.NotificationRule, error) { t.Helper() @@ -2645,6 +3038,12 @@ func (r resourceChecker) mustGetRule(t *testing.T, getOpt getResourceOptFn) infl return rule } +func (r resourceChecker) mustDeleteRule(t *testing.T, id influxdb.ID) { + t.Helper() + + require.NoError(t, r.tl.NotificationRuleService().DeleteNotificationRule(ctx, id)) +} + func (r resourceChecker) getTask(t *testing.T, getOpt getResourceOptFn) (http.Task, error) { t.Helper() @@ -2689,6 +3088,12 @@ func (r resourceChecker) mustGetTask(t *testing.T, getOpt getResourceOptFn) http return task } +func (r resourceChecker) mustDeleteTask(t *testing.T, id influxdb.ID) { + t.Helper() + + require.NoError(t, r.tl.TaskService(t).DeleteTask(ctx, id)) +} + func (r resourceChecker) getTelegrafConfig(t *testing.T, getOpt getResourceOptFn) (influxdb.TelegrafConfig, error) { t.Helper() @@ -2729,6 +3134,12 @@ func (r resourceChecker) mustGetTelegrafConfig(t *testing.T, getOpt getResourceO return tele } +func (r resourceChecker) mustDeleteTelegrafConfig(t *testing.T, id influxdb.ID) { + t.Helper() + + require.NoError(t, r.tl.TelegrafService(t).DeleteTelegrafConfig(ctx, id)) +} + func (r resourceChecker) getVariable(t *testing.T, getOpt getResourceOptFn) (influxdb.Variable, error) { t.Helper() @@ -2774,3 +3185,22 @@ func (r resourceChecker) mustGetVariable(t *testing.T, getOpt getResourceOptFn) require.NoError(t, err) return l } + +func (r resourceChecker) mustDeleteVariable(t *testing.T, id influxdb.ID) { + t.Helper() + + err := r.tl.VariableService(t).DeleteVariable(ctx, id) + require.NoError(t, err) +} + +func sortChecks(checks []pkger.SummaryCheck) { + sort.Slice(checks, func(i, j int) bool { + return checks[i].Check.GetName() < checks[j].Check.GetName() + }) +} + +func sortLabels(labels []pkger.SummaryLabel) { + sort.Slice(labels, func(i, j int) bool { + return labels[i].Name < labels[j].Name + }) +} diff --git a/go.mod b/go.mod index b3a9bfc1db..41dc7a65c2 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/hashicorp/raft v1.0.0 // indirect github.com/hashicorp/vault/api v1.0.2 github.com/influxdata/cron v0.0.0-20191203200038-ded12750aac6 - github.com/influxdata/flux v0.67.1-0.20200506164116-7432bbda91d7 + github.com/influxdata/flux v0.67.1-0.20200507153142-7a0c6ca988e1 github.com/influxdata/httprouter v1.3.1-0.20191122104820-ee83e2772f69 github.com/influxdata/influxql v0.0.0-20180925231337-1cbfca8e56b6 github.com/influxdata/pkg-config v0.2.0 diff --git a/go.sum b/go.sum index bfd6bfdf8f..4fe9139a5d 100644 --- a/go.sum +++ b/go.sum @@ -246,8 +246,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/cron v0.0.0-20191203200038-ded12750aac6 h1:OtjKkeWDjUbyMi82C7XXy7Tvm2LXMwiBBXyFIGNPaGA= github.com/influxdata/cron v0.0.0-20191203200038-ded12750aac6/go.mod h1:XabtPPW2qsCg0tl+kjaPU+cFS+CjQXEXbT1VJvHT4og= -github.com/influxdata/flux v0.67.1-0.20200506164116-7432bbda91d7 h1:9bRTK6KToiAp4UP2cLm06NznCOdMvtnIUSwxgpaDw2s= -github.com/influxdata/flux v0.67.1-0.20200506164116-7432bbda91d7/go.mod h1:AdzL5HnjdFlcBiNz0wE69rSTGRX9CQHqtJUF8ptiDeY= +github.com/influxdata/flux v0.67.1-0.20200507153142-7a0c6ca988e1 h1:tT0L4qNrdcSnfAB4Srb/J4W9m5vZ14VUf22STppqZrc= +github.com/influxdata/flux v0.67.1-0.20200507153142-7a0c6ca988e1/go.mod h1:AdzL5HnjdFlcBiNz0wE69rSTGRX9CQHqtJUF8ptiDeY= github.com/influxdata/goreleaser v0.97.0-influx h1:jT5OrcW7WfS0e2QxfwmTBjhLvpIC9CDLRhNgZJyhj8s= github.com/influxdata/goreleaser v0.97.0-influx/go.mod h1:MnjA0e0Uq6ISqjG1WxxMAl+3VS1QYjILSWVnMYDxasE= github.com/influxdata/httprouter v1.3.1-0.20191122104820-ee83e2772f69 h1:WQsmW0fXO4ZE/lFGIE84G6rIV5SJN3P3sjIXAP1a8eU= diff --git a/pkger/parser.go b/pkger/parser.go index 42e63f8d46..9eb999c179 100644 --- a/pkger/parser.go +++ b/pkger/parser.go @@ -530,7 +530,7 @@ func (p *Pkg) buckets() []*bucket { buckets = append(buckets, b) } - sort.Slice(buckets, func(i, j int) bool { return buckets[i].Name() < buckets[j].Name() }) + sort.Slice(buckets, func(i, j int) bool { return buckets[i].PkgName() < buckets[j].PkgName() }) return buckets } @@ -541,7 +541,7 @@ func (p *Pkg) checks() []*check { checks = append(checks, c) } - sort.Slice(checks, func(i, j int) bool { return checks[i].Name() < checks[j].Name() }) + sort.Slice(checks, func(i, j int) bool { return checks[i].PkgName() < checks[j].PkgName() }) return checks } @@ -574,7 +574,7 @@ func (p *Pkg) notificationEndpoints() []*notificationEndpoint { sort.Slice(endpoints, func(i, j int) bool { ei, ej := endpoints[i], endpoints[j] if ei.kind == ej.kind { - return ei.Name() < ej.Name() + return ei.PkgName() < ej.PkgName() } return ei.kind < ej.kind }) @@ -586,7 +586,7 @@ func (p *Pkg) notificationRules() []*notificationRule { for _, r := range p.mNotificationRules { rules = append(rules, r) } - sort.Slice(rules, func(i, j int) bool { return rules[i].Name() < rules[j].Name() }) + sort.Slice(rules, func(i, j int) bool { return rules[i].PkgName() < rules[j].PkgName() }) return rules } @@ -618,7 +618,7 @@ func (p *Pkg) tasks() []*task { tasks = append(tasks, t) } - sort.Slice(tasks, func(i, j int) bool { return tasks[i].Name() < tasks[j].Name() }) + sort.Slice(tasks, func(i, j int) bool { return tasks[i].PkgName() < tasks[j].PkgName() }) return tasks } @@ -630,7 +630,7 @@ func (p *Pkg) telegrafs() []*telegraf { teles = append(teles, t) } - sort.Slice(teles, func(i, j int) bool { return teles[i].Name() < teles[j].Name() }) + sort.Slice(teles, func(i, j int) bool { return teles[i].PkgName() < teles[j].PkgName() }) return teles } @@ -641,7 +641,7 @@ func (p *Pkg) variables() []*variable { vars = append(vars, v) } - sort.Slice(vars, func(i, j int) bool { return vars[i].Name() < vars[j].Name() }) + sort.Slice(vars, func(i, j int) bool { return vars[i].PkgName() < vars[j].PkgName() }) return vars } diff --git a/pkger/parser_models.go b/pkger/parser_models.go index 01dd54bd93..5276a3875d 100644 --- a/pkger/parser_models.go +++ b/pkger/parser_models.go @@ -1199,7 +1199,7 @@ func (s sortedLabels) Len() int { } func (s sortedLabels) Less(i, j int) bool { - return s[i].Name() < s[j].Name() + return s[i].PkgName() < s[j].PkgName() } func (s sortedLabels) Swap(i, j int) { diff --git a/pkger/parser_test.go b/pkger/parser_test.go index a92a6c2cd5..79618afa0b 100644 --- a/pkger/parser_test.go +++ b/pkger/parser_test.go @@ -27,19 +27,19 @@ func TestParse(t *testing.T) { actual := buckets[0] expectedBucket := SummaryBucket{ - PkgName: "rucket-22", - Name: "display name", - Description: "bucket 2 description", + PkgName: "rucket-11", + Name: "rucket-11", + Description: "bucket 1 description", + RetentionPeriod: time.Hour, LabelAssociations: []SummaryLabel{}, } assert.Equal(t, expectedBucket, actual) actual = buckets[1] expectedBucket = SummaryBucket{ - PkgName: "rucket-11", - Name: "rucket-11", - Description: "bucket 1 description", - RetentionPeriod: time.Hour, + PkgName: "rucket-22", + Name: "display name", + Description: "bucket 2 description", LabelAssociations: []SummaryLabel{}, } assert.Equal(t, expectedBucket, actual) @@ -160,19 +160,7 @@ spec: labels := pkg.Summary().Labels require.Len(t, labels, 3) - expectedLabel0 := SummaryLabel{ - PkgName: "label-3", - Name: "display name", - Properties: struct { - Color string `json:"color"` - Description string `json:"description"` - }{ - Description: "label 3 description", - }, - } - assert.Equal(t, expectedLabel0, labels[0]) - - expectedLabel1 := SummaryLabel{ + expectedLabel := SummaryLabel{ PkgName: "label-1", Name: "label-1", Properties: struct { @@ -183,9 +171,9 @@ spec: Description: "label 1 description", }, } - assert.Equal(t, expectedLabel1, labels[1]) + assert.Equal(t, expectedLabel, labels[0]) - expectedLabel2 := SummaryLabel{ + expectedLabel = SummaryLabel{ PkgName: "label-2", Name: "label-2", Properties: struct { @@ -196,7 +184,19 @@ spec: Description: "label 2 description", }, } - assert.Equal(t, expectedLabel2, labels[2]) + assert.Equal(t, expectedLabel, labels[1]) + + expectedLabel = SummaryLabel{ + PkgName: "label-3", + Name: "display name", + Properties: struct { + Color string `json:"color"` + Description string `json:"description"` + }{ + Description: "label 3 description", + }, + } + assert.Equal(t, expectedLabel, labels[2]) }) }) @@ -3669,6 +3669,21 @@ spec: assert.Equal(t, vals, v.Arguments.Values) } + // validates we support all known variable types + varEquals(t, + "var-const-3", + "constant", + influxdb.VariableConstantValues([]string{"first val"}), + sum.Variables[0], + ) + + varEquals(t, + "var-map-4", + "map", + influxdb.VariableMapValues{"k1": "v1"}, + sum.Variables[1], + ) + varEquals(t, "query var", "query", @@ -3676,21 +3691,6 @@ spec: Query: `buckets() |> filter(fn: (r) => r.name !~ /^_/) |> rename(columns: {name: "_value"}) |> keep(columns: ["_value"])`, Language: "flux", }, - sum.Variables[0], - ) - - // validates we support all known variable types - varEquals(t, - "var-const-3", - "constant", - influxdb.VariableConstantValues([]string{"first val"}), - sum.Variables[1], - ) - - varEquals(t, - "var-map-4", - "map", - influxdb.VariableMapValues{"k1": "v1"}, sum.Variables[2], ) diff --git a/pkger/service.go b/pkger/service.go index fbd6bd685b..6594810f18 100644 --- a/pkger/service.go +++ b/pkger/service.go @@ -12,6 +12,7 @@ import ( "github.com/influxdata/influxdb/v2" ierrors "github.com/influxdata/influxdb/v2/kit/errors" + icheck "github.com/influxdata/influxdb/v2/notification/check" "github.com/influxdata/influxdb/v2/notification/rule" "github.com/influxdata/influxdb/v2/snowflake" "github.com/influxdata/influxdb/v2/task/options" @@ -957,11 +958,9 @@ func (s *Service) dryRunVariables(ctx context.Context, orgID influxdb.ID, vars m } for _, v := range vars { - var existing *influxdb.Variable + existing := mNames[v.parserVar.Name()] if v.ID() != 0 { existing = mIDs[v.ID()] - } else { - existing = mNames[v.parserVar.Name()] } if IsNew(v.stateStatus) && existing != nil { v.stateStatus = StateStatusExists @@ -1095,8 +1094,9 @@ func (s *Service) dryRunResourceLabelMapping(ctx context.Context, state *stateCo ResourceID: ident.id, ResourceType: ident.resourceType, }) - if err != nil { - return nil, err + if err != nil && influxdb.ErrorCode(err) != influxdb.ENotFound { + msgFmt := fmt.Sprintf("failed to find labels mappings for %s resource[%q]", ident.resourceType, ident.id) + return nil, ierrors.Wrap(err, msgFmt) } pkgLabels := labelSlcToMap(pkgResourceLabels) @@ -1235,7 +1235,7 @@ func (s *Service) Apply(ctx context.Context, orgID, userID influxdb.ID, pkg *Pkg func (s *Service) applyState(ctx context.Context, coordinator *rollbackCoordinator, orgID, userID influxdb.ID, state *stateCoordinator, missingSecrets map[string]string) (e error) { endpointApp, ruleApp, err := s.applyNotificationGenerator(ctx, userID, state.rules(), state.endpoints()) if err != nil { - return err + return ierrors.Wrap(err, "failed to setup notification generator") } // each grouping here runs for its entirety, then returns an error that @@ -1340,15 +1340,18 @@ func (s *Service) applyBuckets(ctx context.Context, buckets []*stateBucket) appl func (s *Service) rollbackBuckets(ctx context.Context, buckets []*stateBucket) error { rollbackFn := func(b *stateBucket) error { + if !IsNew(b.stateStatus) && b.existing == nil { + return nil + } + var err error - switch b.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(b.stateStatus): err = ierrors.Wrap(s.bucketSVC.CreateBucket(ctx, b.existing), "rolling back removed bucket") - case StateStatusExists: - rp := b.parserBkt.RetentionRules.RP() + case IsExisting(b.stateStatus): _, err = s.bucketSVC.UpdateBucket(ctx, b.ID(), influxdb.BucketUpdate{ - Description: &b.parserBkt.Description, - RetentionPeriod: &rp, + Description: &b.existing.Description, + RetentionPeriod: &b.existing.RetentionPeriod, }) err = ierrors.Wrap(err, "rolling back existing bucket to previous state") default: @@ -1373,13 +1376,16 @@ func (s *Service) rollbackBuckets(ctx context.Context, buckets []*stateBucket) e } func (s *Service) applyBucket(ctx context.Context, b *stateBucket) (influxdb.Bucket, error) { - switch b.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(b.stateStatus): if err := s.bucketSVC.DeleteBucket(ctx, b.ID()); err != nil { + if influxdb.ErrorCode(err) == influxdb.ENotFound { + return influxdb.Bucket{}, nil + } return influxdb.Bucket{}, fmt.Errorf("failed to delete bucket[%q]: %w", b.ID(), err) } return *b.existing, nil - case StateStatusExists: + case IsExisting(b.stateStatus) && b.existing != nil: rp := b.parserBkt.RetentionRules.RP() newName := b.parserBkt.Name() influxBucket, err := s.bucketSVC.UpdateBucket(ctx, b.ID(), influxdb.BucketUpdate{ @@ -1451,8 +1457,8 @@ func (s *Service) applyChecks(ctx context.Context, checks []*stateCheck) applier func (s *Service) rollbackChecks(ctx context.Context, checks []*stateCheck) error { rollbackFn := func(c *stateCheck) error { var err error - switch c.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(c.stateStatus): err = s.checkSVC.CreateCheck( ctx, influxdb.CheckCreate{ @@ -1462,13 +1468,16 @@ func (s *Service) rollbackChecks(ctx context.Context, checks []*stateCheck) erro c.existing.GetOwnerID(), ) c.id = c.existing.GetID() - case StateStatusNew: - err = s.checkSVC.DeleteCheck(ctx, c.ID()) - default: + case IsExisting(c.stateStatus): + if c.existing == nil { + return nil + } _, err = s.checkSVC.UpdateCheck(ctx, c.ID(), influxdb.CheckCreate{ Check: c.summarize().Check, Status: influxdb.Status(c.parserCheck.status), }) + default: + err = s.checkSVC.DeleteCheck(ctx, c.ID()) } return err } @@ -1488,13 +1497,16 @@ func (s *Service) rollbackChecks(ctx context.Context, checks []*stateCheck) erro } func (s *Service) applyCheck(ctx context.Context, c *stateCheck, userID influxdb.ID) (influxdb.Check, error) { - switch c.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(c.stateStatus): if err := s.checkSVC.DeleteCheck(ctx, c.ID()); err != nil { + if influxdb.ErrorCode(err) == influxdb.ENotFound { + return &icheck.Threshold{Base: icheck.Base{ID: c.ID()}}, nil + } return nil, fmt.Errorf("failed to delete check[%q]: %w", c.ID(), err) } return c.existing, nil - case StateStatusExists: + case IsExisting(c.stateStatus) && c.existing != nil: influxCheck, err := s.checkSVC.UpdateCheck(ctx, c.ID(), influxdb.CheckCreate{ Check: c.summarize().Check, Status: c.parserCheck.Status(), @@ -1559,13 +1571,16 @@ func (s *Service) applyDashboards(ctx context.Context, dashboards []*stateDashbo } func (s *Service) applyDashboard(ctx context.Context, d *stateDashboard) (influxdb.Dashboard, error) { - switch d.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(d.stateStatus): if err := s.dashSVC.DeleteDashboard(ctx, d.ID()); err != nil { + if influxdb.ErrorCode(err) == influxdb.ENotFound { + return influxdb.Dashboard{}, nil + } return influxdb.Dashboard{}, fmt.Errorf("failed to delete dashboard[%q]: %w", d.ID(), err) } return *d.existing, nil - case StateStatusExists: + case IsExisting(d.stateStatus) && d.existing != nil: name := d.parserDash.Name() cells := convertChartsToCells(d.parserDash.Charts) dash, err := s.dashSVC.UpdateDashboard(ctx, d.ID(), influxdb.DashboardUpdate{ @@ -1595,11 +1610,15 @@ func (s *Service) applyDashboard(ctx context.Context, d *stateDashboard) (influx func (s *Service) rollbackDashboards(ctx context.Context, dashs []*stateDashboard) error { rollbackFn := func(d *stateDashboard) error { + if !IsNew(d.stateStatus) && d.existing == nil { + return nil + } + var err error - switch d.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(d.stateStatus): err = ierrors.Wrap(s.dashSVC.CreateDashboard(ctx, d.existing), "rolling back removed dashboard") - case StateStatusExists: + case IsExisting(d.stateStatus): _, err := s.dashSVC.UpdateDashboard(ctx, d.ID(), influxdb.DashboardUpdate{ Name: &d.existing.Name, Description: &d.existing.Description, @@ -1693,11 +1712,15 @@ func (s *Service) applyLabels(ctx context.Context, labels []*stateLabel) applier func (s *Service) rollbackLabels(ctx context.Context, labels []*stateLabel) error { rollbackFn := func(l *stateLabel) error { + if !IsNew(l.stateStatus) && l.existing == nil { + return nil + } + var err error - switch l.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(l.stateStatus): err = s.labelSVC.CreateLabel(ctx, l.existing) - case StateStatusExists: + case IsExisting(l.stateStatus): _, err = s.labelSVC.UpdateLabel(ctx, l.ID(), influxdb.LabelUpdate{ Name: l.parserLabel.Name(), Properties: l.existing.Properties, @@ -1727,10 +1750,10 @@ func (s *Service) applyLabel(ctx context.Context, l *stateLabel) (influxdb.Label influxLabel *influxdb.Label err error ) - switch l.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(l.stateStatus): influxLabel, err = l.existing, s.labelSVC.DeleteLabel(ctx, l.ID()) - case StateStatusExists: + case IsExisting(l.stateStatus) && l.existing != nil: influxLabel, err = s.labelSVC.UpdateLabel(ctx, l.ID(), influxdb.LabelUpdate{ Name: l.parserLabel.Name(), Properties: l.properties(), @@ -1741,6 +1764,9 @@ func (s *Service) applyLabel(ctx context.Context, l *stateLabel) (influxdb.Label influxLabel = &creatLabel err = ierrors.Wrap(s.labelSVC.CreateLabel(ctx, &creatLabel), "creating") } + if influxdb.ErrorCode(err) == influxdb.ENotFound { + return influxdb.Label{}, nil + } if err != nil || influxLabel == nil { return influxdb.Label{}, err } @@ -1768,29 +1794,31 @@ func (s *Service) applyNotificationEndpoints(ctx context.Context, userID influxd } mutex.Do(func() { - endpoints[i].id = influxEndpoint.GetID() - for _, secret := range influxEndpoint.SecretFields() { - switch { - case strings.HasSuffix(secret.Key, "-routing-key"): - if endpoints[i].parserEndpoint.routingKey == nil { - endpoints[i].parserEndpoint.routingKey = new(references) + if influxEndpoint != nil { + endpoints[i].id = influxEndpoint.GetID() + for _, secret := range influxEndpoint.SecretFields() { + switch { + case strings.HasSuffix(secret.Key, "-routing-key"): + if endpoints[i].parserEndpoint.routingKey == nil { + endpoints[i].parserEndpoint.routingKey = new(references) + } + endpoints[i].parserEndpoint.routingKey.Secret = secret.Key + case strings.HasSuffix(secret.Key, "-token"): + if endpoints[i].parserEndpoint.token == nil { + endpoints[i].parserEndpoint.token = new(references) + } + endpoints[i].parserEndpoint.token.Secret = secret.Key + case strings.HasSuffix(secret.Key, "-username"): + if endpoints[i].parserEndpoint.username == nil { + endpoints[i].parserEndpoint.username = new(references) + } + endpoints[i].parserEndpoint.username.Secret = secret.Key + case strings.HasSuffix(secret.Key, "-password"): + if endpoints[i].parserEndpoint.password == nil { + endpoints[i].parserEndpoint.password = new(references) + } + endpoints[i].parserEndpoint.password.Secret = secret.Key } - endpoints[i].parserEndpoint.routingKey.Secret = secret.Key - case strings.HasSuffix(secret.Key, "-token"): - if endpoints[i].parserEndpoint.token == nil { - endpoints[i].parserEndpoint.token = new(references) - } - endpoints[i].parserEndpoint.token.Secret = secret.Key - case strings.HasSuffix(secret.Key, "-username"): - if endpoints[i].parserEndpoint.username == nil { - endpoints[i].parserEndpoint.username = new(references) - } - endpoints[i].parserEndpoint.username.Secret = secret.Key - case strings.HasSuffix(secret.Key, "-password"): - if endpoints[i].parserEndpoint.password == nil { - endpoints[i].parserEndpoint.password = new(references) - } - endpoints[i].parserEndpoint.password.Secret = secret.Key } } rollbackEndpoints = append(rollbackEndpoints, endpoints[i]) @@ -1817,14 +1845,14 @@ func (s *Service) applyNotificationEndpoints(ctx context.Context, userID influxd } func (s *Service) applyNotificationEndpoint(ctx context.Context, e *stateEndpoint, userID influxdb.ID) (influxdb.NotificationEndpoint, error) { - switch e.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(e.stateStatus): _, _, err := s.endpointSVC.DeleteNotificationEndpoint(ctx, e.ID()) - if err != nil { + if err != nil && influxdb.ErrorCode(err) != influxdb.ENotFound { return nil, err } return e.existing, nil - case StateStatusExists: + case IsExisting(e.stateStatus) && e.existing != nil: // stub out userID since we're always using hte http client which will fill it in for us with the token // feels a bit broken that is required. // TODO: look into this userID requirement @@ -1847,6 +1875,9 @@ func (s *Service) applyNotificationEndpoint(ctx context.Context, e *stateEndpoin func (s *Service) rollbackNotificationEndpoints(ctx context.Context, userID influxdb.ID, endpoints []*stateEndpoint) error { rollbackFn := func(e *stateEndpoint) error { + if !IsNew(e.stateStatus) && e.existing == nil { + return nil + } var err error switch e.stateStatus { case StateStatusRemove: @@ -1942,7 +1973,9 @@ func (s *Service) applyNotificationRules(ctx context.Context, userID influxdb.ID } mutex.Do(func() { - rules[i].id = influxRule.GetID() + if influxRule != nil { + rules[i].id = influxRule.GetID() + } rollbackEndpoints = append(rollbackEndpoints, rules[i]) }) @@ -1965,13 +1998,16 @@ func (s *Service) applyNotificationRules(ctx context.Context, userID influxdb.ID } func (s *Service) applyNotificationRule(ctx context.Context, r *stateRule, userID influxdb.ID) (influxdb.NotificationRule, error) { - switch r.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(r.stateStatus): if err := s.ruleSVC.DeleteNotificationRule(ctx, r.ID()); err != nil { + if influxdb.ErrorCode(err) == influxdb.ENotFound { + return nil, nil + } return nil, ierrors.Wrap(err, "failed to remove notification rule") } return r.existing, nil - case StateStatusExists: + case IsExisting(r.stateStatus) && r.existing != nil: ruleCreate := influxdb.NotificationRuleCreate{ NotificationRule: r.toInfluxRule(), Status: r.parserRule.Status(), @@ -1996,6 +2032,10 @@ func (s *Service) applyNotificationRule(ctx context.Context, r *stateRule, userI func (s *Service) rollbackNotificationRules(ctx context.Context, userID influxdb.ID, rules []*stateRule) error { rollbackFn := func(r *stateRule) error { + if !IsNew(r.stateStatus) && r.existing == nil { + return nil + } + existingRuleFn := func(endpointID influxdb.ID) influxdb.NotificationRule { switch rr := r.existing.(type) { case *rule.HTTP: @@ -2143,13 +2183,16 @@ func (s *Service) applyTasks(ctx context.Context, tasks []*stateTask) applier { } func (s *Service) applyTask(ctx context.Context, userID influxdb.ID, t *stateTask) (influxdb.Task, error) { - switch t.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(t.stateStatus): if err := s.taskSVC.DeleteTask(ctx, t.ID()); err != nil { + if influxdb.ErrorCode(err) == influxdb.ENotFound { + return influxdb.Task{}, nil + } return influxdb.Task{}, ierrors.Wrap(err, "failed to delete task") } return *t.existing, nil - case StateStatusExists: + case IsExisting(t.stateStatus) && t.existing != nil: newFlux := t.parserTask.flux() newStatus := string(t.parserTask.Status()) opt := options.Options{ @@ -2191,6 +2234,10 @@ func (s *Service) applyTask(ctx context.Context, userID influxdb.ID, t *stateTas func (s *Service) rollbackTasks(ctx context.Context, tasks []*stateTask) error { rollbackFn := func(t *stateTask) error { + if !IsNew(t.stateStatus) && t.existing == nil { + return nil + } + var err error switch t.stateStatus { case StateStatusRemove: @@ -2293,13 +2340,16 @@ func (s *Service) applyTelegrafs(ctx context.Context, userID influxdb.ID, teles } func (s *Service) applyTelegrafConfig(ctx context.Context, userID influxdb.ID, t *stateTelegraf) (influxdb.TelegrafConfig, error) { - switch t.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(t.stateStatus): if err := s.teleSVC.DeleteTelegrafConfig(ctx, t.ID()); err != nil { + if influxdb.ErrorCode(err) == influxdb.ENotFound { + return influxdb.TelegrafConfig{}, nil + } return influxdb.TelegrafConfig{}, ierrors.Wrap(err, "failed to delete config") } return *t.existing, nil - case StateStatusExists: + case IsExisting(t.stateStatus) && t.existing != nil: cfg := t.summarize().TelegrafConfig updatedConfig, err := s.teleSVC.UpdateTelegrafConfig(ctx, t.ID(), &cfg, userID) if err != nil { @@ -2318,6 +2368,10 @@ func (s *Service) applyTelegrafConfig(ctx context.Context, userID influxdb.ID, t func (s *Service) rollbackTelegrafConfigs(ctx context.Context, userID influxdb.ID, cfgs []*stateTelegraf) error { rollbackFn := func(t *stateTelegraf) error { + if !IsNew(t.stateStatus) && t.existing == nil { + return nil + } + var err error switch t.stateStatus { case StateStatusRemove: @@ -2390,14 +2444,20 @@ func (s *Service) applyVariables(ctx context.Context, vars []*stateVariable) app func (s *Service) rollbackVariables(ctx context.Context, variables []*stateVariable) error { rollbackFn := func(v *stateVariable) error { var err error - switch v.stateStatus { - case StateStatusRemove: + switch { + case IsRemoval(v.stateStatus): + if v.existing == nil { + return nil + } err = ierrors.Wrap(s.varSVC.CreateVariable(ctx, v.existing), "rolling back removed variable") - case StateStatusExists: + case IsExisting(v.stateStatus): + if v.existing == nil { + return nil + } _, err = s.varSVC.UpdateVariable(ctx, v.ID(), &influxdb.VariableUpdate{ - Name: v.parserVar.Name(), - Description: v.parserVar.Description, - Arguments: v.parserVar.influxVarArgs(), + Name: v.existing.Name, + Description: v.existing.Description, + Arguments: v.existing.Arguments, }) err = ierrors.Wrap(err, "rolling back updated variable") default: @@ -2421,23 +2481,28 @@ func (s *Service) rollbackVariables(ctx context.Context, variables []*stateVaria } func (s *Service) applyVariable(ctx context.Context, v *stateVariable) (influxdb.Variable, error) { - switch v.stateStatus { - case StateStatusRemove: - if err := s.varSVC.DeleteVariable(ctx, v.id); err != nil { - return influxdb.Variable{}, err + switch { + case IsRemoval(v.stateStatus): + if err := s.varSVC.DeleteVariable(ctx, v.id); err != nil && influxdb.ErrorCode(err) != influxdb.ENotFound { + return influxdb.Variable{}, ierrors.Wrap(err, "removing existing variable") + } + if v.existing == nil { + return influxdb.Variable{}, nil } return *v.existing, nil - case StateStatusExists: + case IsExisting(v.stateStatus) && v.existing != nil: updatedVar, err := s.varSVC.UpdateVariable(ctx, v.ID(), &influxdb.VariableUpdate{ Name: v.parserVar.Name(), Description: v.parserVar.Description, Arguments: v.parserVar.influxVarArgs(), }) if err != nil { - return influxdb.Variable{}, err + return influxdb.Variable{}, ierrors.Wrap(err, "updating existing variable") } return *updatedVar, nil default: + // when an existing variable (referenced in stack) has been deleted by a user + // then the resource is created anew to get it back to the expected state. influxVar := influxdb.Variable{ OrganizationID: v.orgID, Name: v.parserVar.Name(), @@ -2446,9 +2511,8 @@ func (s *Service) applyVariable(ctx context.Context, v *stateVariable) (influxdb } err := s.varSVC.CreateVariable(ctx, &influxVar) if err != nil { - return influxdb.Variable{}, err + return influxdb.Variable{}, ierrors.Wrap(err, "creating new variable") } - return influxVar, nil } } diff --git a/pkger/service_test.go b/pkger/service_test.go index c2c208e4fa..6b7f45815e 100644 --- a/pkger/service_test.go +++ b/pkger/service_test.go @@ -7,6 +7,7 @@ import ( "fmt" "math/rand" "regexp" + "sort" "strconv" "testing" "time" @@ -1482,6 +1483,12 @@ func TestService(t *testing.T) { } } + sortLabelsByName := func(labels []SummaryLabel) { + sort.Slice(labels, func(i, j int) bool { + return labels[i].Name < labels[j].Name + }) + } + t.Run("with existing resources", func(t *testing.T) { encodeAndDecode := func(t *testing.T, pkg *Pkg) *Pkg { t.Helper() @@ -2659,6 +2666,9 @@ func TestService(t *testing.T) { sum := newPkg.Summary() teles := sum.TelegrafConfigs + sort.Slice(teles, func(i, j int) bool { + return teles[i].TelegrafConfig.Name < teles[j].TelegrafConfig.Name + }) require.Len(t, teles, len(resourcesToClone)) for i := range resourcesToClone { @@ -2853,9 +2863,13 @@ func TestService(t *testing.T) { sum := newPkg.Summary() bkts := sum.Buckets + sort.Slice(bkts, func(i, j int) bool { + return bkts[i].Name < bkts[j].Name + }) require.Len(t, bkts, 2) for i, actual := range bkts { + sortLabelsByName(actual.LabelAssociations) assert.Equal(t, strconv.Itoa((i+1)*10), actual.Name) require.Len(t, actual.LabelAssociations, 2) assert.Equal(t, "label_1", actual.LabelAssociations[0].Name) @@ -2863,6 +2877,7 @@ func TestService(t *testing.T) { } labels := sum.Labels + sortLabelsByName(labels) require.Len(t, labels, 2) assert.Equal(t, "label_1", labels[0].Name) assert.Equal(t, "label_2", labels[1].Name) diff --git a/query/promql/internal/promqltests/go.mod b/query/promql/internal/promqltests/go.mod index b705da6fe4..b24200f576 100644 --- a/query/promql/internal/promqltests/go.mod +++ b/query/promql/internal/promqltests/go.mod @@ -12,7 +12,7 @@ require ( github.com/go-kit/kit v0.10.0 // indirect github.com/google/go-cmp v0.4.0 github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/influxdata/flux v0.67.1-0.20200506164116-7432bbda91d7 + github.com/influxdata/flux v0.67.1-0.20200507153142-7a0c6ca988e1 github.com/influxdata/influxdb/v2 v2.0.0-00010101000000-000000000000 github.com/influxdata/influxql v1.0.1 // indirect github.com/influxdata/promql/v2 v2.12.0 diff --git a/query/promql/internal/promqltests/go.sum b/query/promql/internal/promqltests/go.sum index b9e733550b..af072f9ca1 100644 --- a/query/promql/internal/promqltests/go.sum +++ b/query/promql/internal/promqltests/go.sum @@ -321,8 +321,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/cron v0.0.0-20191203200038-ded12750aac6 h1:OtjKkeWDjUbyMi82C7XXy7Tvm2LXMwiBBXyFIGNPaGA= github.com/influxdata/cron v0.0.0-20191203200038-ded12750aac6/go.mod h1:XabtPPW2qsCg0tl+kjaPU+cFS+CjQXEXbT1VJvHT4og= -github.com/influxdata/flux v0.67.1-0.20200506164116-7432bbda91d7 h1:9bRTK6KToiAp4UP2cLm06NznCOdMvtnIUSwxgpaDw2s= -github.com/influxdata/flux v0.67.1-0.20200506164116-7432bbda91d7/go.mod h1:AdzL5HnjdFlcBiNz0wE69rSTGRX9CQHqtJUF8ptiDeY= +github.com/influxdata/flux v0.67.1-0.20200507153142-7a0c6ca988e1 h1:tT0L4qNrdcSnfAB4Srb/J4W9m5vZ14VUf22STppqZrc= +github.com/influxdata/flux v0.67.1-0.20200507153142-7a0c6ca988e1/go.mod h1:AdzL5HnjdFlcBiNz0wE69rSTGRX9CQHqtJUF8ptiDeY= github.com/influxdata/httprouter v1.3.1-0.20191122104820-ee83e2772f69 h1:WQsmW0fXO4ZE/lFGIE84G6rIV5SJN3P3sjIXAP1a8eU= github.com/influxdata/httprouter v1.3.1-0.20191122104820-ee83e2772f69/go.mod h1:pwymjR6SrP3gD3pRj9RJwdl1j5s3doEEV8gS4X9qSzA= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= diff --git a/ui/cypress/e2e/orgs.test.ts b/ui/cypress/e2e/orgs.test.ts index c0e10cc851..5f749de07e 100644 --- a/ui/cypress/e2e/orgs.test.ts +++ b/ui/cypress/e2e/orgs.test.ts @@ -27,7 +27,8 @@ describe('Orgs', () => { }) it('should be able to rename the org', () => { const extraText = '_my_renamed_org_in_e2e' - cy.getByTestID('nav-item-org').click() + cy.getByTestID('user-nav').click() + cy.getByTestID('user-nav-item-about').click() cy.get('span:contains("About")').click() cy.get('span:contains("Rename")').click() cy.get('button.cf-button.cf-button-danger').click() diff --git a/ui/src/cloud/actions/demodata.ts b/ui/src/cloud/actions/demodata.ts index 9dcbef503a..8eb6d825e1 100644 --- a/ui/src/cloud/actions/demodata.ts +++ b/ui/src/cloud/actions/demodata.ts @@ -34,6 +34,7 @@ import { Dashboard, } from 'src/types' import {reportError} from 'src/shared/utils/errors' +import {getErrorMessage} from 'src/utils/api' export type Actions = | ReturnType @@ -124,7 +125,7 @@ export const getDemoDataBucketMembership = ({ dispatch(notify(demoDataSucceeded(bucketName, url))) } catch (error) { - dispatch(notify(demoDataAddBucketFailed(error))) + dispatch(notify(demoDataAddBucketFailed(getErrorMessage(error)))) reportError(error, { name: 'getDemoDataBucketMembership function', diff --git a/ui/src/cloud/actions/orgsettings.ts b/ui/src/cloud/actions/orgsettings.ts new file mode 100644 index 0000000000..58a1a0bac6 --- /dev/null +++ b/ui/src/cloud/actions/orgsettings.ts @@ -0,0 +1,46 @@ +// API +import {fetchOrgSettings} from 'src/cloud/apis/orgsettings' + +// Constants +import {FREE_ORG_HIDE_UPGRADE_SETTING} from 'src/cloud/constants' + +// Types +import {GetState, OrgSetting} from 'src/types' + +// Selectors +import {getOrg} from 'src/organizations/selectors' + +export const SET_ORG_SETTINGS = 'SET_ORG_SETTINGS' + +export type Action = ReturnType + +export const setOrgSettings = (settings: OrgSetting[] = []) => + ({ + type: SET_ORG_SETTINGS, + payload: {orgSettings: settings}, + } as const) + +export const setFreeOrgSettings = () => + ({ + type: SET_ORG_SETTINGS, + payload: {orgSettings: [FREE_ORG_HIDE_UPGRADE_SETTING]}, + } as const) + +export const getOrgSettings = () => async (dispatch, getState: GetState) => { + try { + const org = getOrg(getState()) + + const response = await fetchOrgSettings(org.id) + + if (response.status !== 200) { + throw new Error( + `Unable to get organization settings: ${response.statusText}` + ) + } + const result = await response.json() + dispatch(setOrgSettings(result.settings)) + } catch (error) { + dispatch(setFreeOrgSettings()) + console.error(error) + } +} diff --git a/ui/src/cloud/apis/orgsettings.ts b/ui/src/cloud/apis/orgsettings.ts new file mode 100644 index 0000000000..1c70cb2a01 --- /dev/null +++ b/ui/src/cloud/apis/orgsettings.ts @@ -0,0 +1,7 @@ +import {getAPIBasepath} from 'src/utils/basepath' +import {OrgSettingsResponse} from 'src/types' + +export const fetchOrgSettings = async ( + orgID: string +): Promise => + await fetch(`${getAPIBasepath()}/api/v2private/orgs/${orgID}/settings`) diff --git a/ui/src/cloud/components/OrgSettings.tsx b/ui/src/cloud/components/OrgSettings.tsx new file mode 100644 index 0000000000..87ac96b84d --- /dev/null +++ b/ui/src/cloud/components/OrgSettings.tsx @@ -0,0 +1,34 @@ +// Libraries +import {PureComponent} from 'react' +import {connect} from 'react-redux' + +// Constants +import {CLOUD} from 'src/shared/constants' + +// Actions +import {getOrgSettings as getOrgSettingsAction} from 'src/cloud/actions/orgsettings' + +interface DispatchProps { + getOrgSettings: typeof getOrgSettingsAction +} + +class OrgSettings extends PureComponent { + public componentDidMount() { + if (CLOUD) { + this.props.getOrgSettings() + } + } + + public render() { + return this.props.children + } +} + +const mdtp: DispatchProps = { + getOrgSettings: getOrgSettingsAction, +} + +export default connect<{}, DispatchProps, {}>( + null, + mdtp +)(OrgSettings) diff --git a/ui/src/cloud/constants/index.ts b/ui/src/cloud/constants/index.ts index 9118e6e493..d4c5a6053b 100644 --- a/ui/src/cloud/constants/index.ts +++ b/ui/src/cloud/constants/index.ts @@ -25,3 +25,13 @@ export const DemoDataTemplates = { DemoDataDashboards[WebsiteMonitoringBucket] ), } + +export const HIDE_UPGRADE_CTA_KEY = 'hide_upgrade_cta' +export const FREE_ORG_HIDE_UPGRADE_SETTING = { + key: HIDE_UPGRADE_CTA_KEY, + value: 'false', +} +export const PAID_ORG_HIDE_UPGRADE_SETTING = { + key: HIDE_UPGRADE_CTA_KEY, + value: 'true', +} diff --git a/ui/src/cloud/reducers/orgsettings.ts b/ui/src/cloud/reducers/orgsettings.ts new file mode 100644 index 0000000000..6f41aaa502 --- /dev/null +++ b/ui/src/cloud/reducers/orgsettings.ts @@ -0,0 +1,25 @@ +import {produce} from 'immer' + +import {Action, SET_ORG_SETTINGS} from 'src/cloud/actions/orgsettings' +import {OrgSetting} from 'src/types' + +import {PAID_ORG_HIDE_UPGRADE_SETTING} from 'src/cloud/constants' + +export interface OrgSettingsState { + settings: OrgSetting[] +} + +export const defaultState: OrgSettingsState = { + settings: [PAID_ORG_HIDE_UPGRADE_SETTING], +} + +export const orgSettingsReducer = ( + state: OrgSettingsState = defaultState, + action: Action +): OrgSettingsState => + produce(state, draftState => { + if (action.type === SET_ORG_SETTINGS) { + draftState.settings = action.payload.orgSettings + } + return + }) diff --git a/ui/src/organizations/components/OrgNavigation.tsx b/ui/src/organizations/components/OrgNavigation.tsx index bc78bd44e3..dc5cd975d9 100644 --- a/ui/src/organizations/components/OrgNavigation.tsx +++ b/ui/src/organizations/components/OrgNavigation.tsx @@ -44,7 +44,7 @@ class OrgNavigation extends PureComponent { link: `/orgs/${orgID}/members`, }, { - text: 'Members', + text: 'Users', id: 'members-quartz', cloudOnly: true, link: `${CLOUD_URL}/organizations/${orgID}${CLOUD_USERS_PATH}`, diff --git a/ui/src/pageLayout/components/UserWidget.tsx b/ui/src/pageLayout/components/UserWidget.tsx index 37ff4528dc..52873cfd08 100644 --- a/ui/src/pageLayout/components/UserWidget.tsx +++ b/ui/src/pageLayout/components/UserWidget.tsx @@ -16,6 +16,7 @@ import { CLOUD_URL, CLOUD_USAGE_PATH, CLOUD_BILLING_PATH, + CLOUD_USERS_PATH, } from 'src/shared/constants' // Types @@ -24,6 +25,8 @@ import {MeState} from 'src/shared/reducers/me' // Selectors import {getOrg} from 'src/organizations/selectors' +import {getNavItemActivation} from '../utils' +import {FeatureFlag} from 'src/shared/utils/featureFlag' interface StateProps { org: Organization @@ -51,12 +54,15 @@ const UserWidget: FC = ({ handleShowOverlay('switch-organizations', {}, handleDismissOverlay) } + const orgPrefix = `/orgs/${org.id}` + return ( - + ( )} @@ -64,6 +70,7 @@ const UserWidget: FC = ({ ( = ({ /> )} /> + + ( + + )} + /> + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> ( )} @@ -89,6 +137,7 @@ const UserWidget: FC = ({ } /> diff --git a/ui/src/pageLayout/constants/navigationHierarchy.ts b/ui/src/pageLayout/constants/navigationHierarchy.ts index d317bfb64d..b2e9cf3b98 100644 --- a/ui/src/pageLayout/constants/navigationHierarchy.ts +++ b/ui/src/pageLayout/constants/navigationHierarchy.ts @@ -1,6 +1,4 @@ import {IconFont} from '@influxdata/clockface' -import {CLOUD_URL, CLOUD_USERS_PATH} from 'src/shared/constants' -import {isFlagEnabled} from 'src/shared/utils/featureFlag' export interface NavItemLink { type: 'link' | 'href' @@ -36,18 +34,6 @@ export interface NavItem { export const generateNavItems = (orgID: string): NavItem[] => { const orgPrefix = `/orgs/${orgID}` - const isMultiUserEnabled = isFlagEnabled('multiUser') - - const quartzMembersHeaderLink: NavItemLink = isMultiUserEnabled - ? { - type: 'href', - location: `${CLOUD_URL}/organizations/${orgID}${CLOUD_USERS_PATH}`, - } - : { - type: 'link', - location: `${orgPrefix}/about`, - } - return [ { id: 'load-data', @@ -121,70 +107,6 @@ export const generateNavItems = (orgID: string): NavItem[] => { }, activeKeywords: ['data-explorer'], }, - { - id: 'org', - testID: 'nav-item-org', - icon: IconFont.UsersTrio, - label: 'Organization', - shortLabel: 'Org', - link: { - type: 'link', - location: `${orgPrefix}/members`, - }, - cloudExclude: true, - activeKeywords: ['members', 'about'], - menu: [ - { - id: 'members', - testID: 'nav-subitem-members', - label: 'Members', - link: { - type: 'link', - location: `${orgPrefix}/members`, - }, - }, - { - id: 'about', - testID: 'nav-subitem-about', - label: 'About', - link: { - type: 'link', - location: `${orgPrefix}/about`, - }, - }, - ], - }, - { - id: 'org-quartz', - testID: 'nav-item-quartz-org', - icon: IconFont.UsersTrio, - label: 'Organization', - shortLabel: 'Org', - cloudOnly: true, - link: quartzMembersHeaderLink, - activeKeywords: ['members', 'about'], - menu: [ - { - id: 'users', - testID: 'nav-subitem-users', - label: 'Members', - featureFlag: 'multiUser', - link: { - type: 'href', - location: `${CLOUD_URL}/organizations/${orgID}${CLOUD_USERS_PATH}`, - }, - }, - { - id: 'about', - testID: 'nav-subitem-about', - label: 'About', - link: { - type: 'link', - location: `${orgPrefix}/about`, - }, - }, - ], - }, { id: 'dashboards', testID: 'nav-item-dashboards', diff --git a/ui/src/pageLayout/containers/TreeNav.tsx b/ui/src/pageLayout/containers/TreeNav.tsx index 4f66edf7aa..523040f8b8 100644 --- a/ui/src/pageLayout/containers/TreeNav.tsx +++ b/ui/src/pageLayout/containers/TreeNav.tsx @@ -11,6 +11,7 @@ import NavHeader from 'src/pageLayout/components/NavHeader' import CloudUpgradeNavBanner from 'src/shared/components/CloudUpgradeNavBanner' import CloudExclude from 'src/shared/components/cloud/CloudExclude' import CloudOnly from 'src/shared/components/cloud/CloudOnly' +import OrgSettings from 'src/cloud/components/OrgSettings' import {FeatureFlag} from 'src/shared/utils/featureFlag' // Constants @@ -67,132 +68,134 @@ class TreeSidebar extends PureComponent { const navItems = generateNavItems(orgID) return ( - } - userElement={} - onToggleClick={handleToggleNavExpansion} - bannerElement={} - > - {navItems.map(item => { - const linkElement = (className: string): JSX.Element => { - if (item.link.type === 'href') { - return - } + + } + userElement={} + onToggleClick={handleToggleNavExpansion} + bannerElement={} + > + {navItems.map(item => { + const linkElement = (className: string): JSX.Element => { + if (item.link.type === 'href') { + return + } + + return + } + let navItemElement = ( + } + label={item.label} + shortLabel={item.shortLabel} + active={getNavItemActivation( + item.activeKeywords, + location.pathname + )} + linkElement={linkElement} + > + {Boolean(item.menu) && ( + + {item.menu.map(menuItem => { + const linkElement = (className: string): JSX.Element => { + if (menuItem.link.type === 'href') { + return ( + + ) + } - return - } - let navItemElement = ( - } - label={item.label} - shortLabel={item.shortLabel} - active={getNavItemActivation( - item.activeKeywords, - location.pathname - )} - linkElement={linkElement} - > - {Boolean(item.menu) && ( - - {item.menu.map(menuItem => { - const linkElement = (className: string): JSX.Element => { - if (menuItem.link.type === 'href') { return ( - ) } - return ( - ) - } - let navSubItemElement = ( - - ) + if (menuItem.cloudExclude) { + navSubItemElement = ( + + {navSubItemElement} + + ) + } - if (menuItem.cloudExclude) { - navSubItemElement = ( - - {navSubItemElement} - - ) - } + if (menuItem.cloudOnly) { + navSubItemElement = ( + + {navSubItemElement} + + ) + } - if (menuItem.cloudOnly) { - navSubItemElement = ( - - {navSubItemElement} - - ) - } + if (menuItem.featureFlag) { + navSubItemElement = ( + + {navSubItemElement} + + ) + } - if (menuItem.featureFlag) { - navSubItemElement = ( - - {navSubItemElement} - - ) - } - - return navSubItemElement - })} - - )} - - ) - - if (item.cloudExclude) { - navItemElement = ( - {navItemElement} + return navSubItemElement + })} + + )} + ) - } - if (item.cloudOnly) { - navItemElement = ( - {navItemElement} - ) - } + if (item.cloudExclude) { + navItemElement = ( + {navItemElement} + ) + } - if (item.featureFlag) { - navItemElement = ( - - {navItemElement} - - ) - } + if (item.cloudOnly) { + navItemElement = ( + {navItemElement} + ) + } - return navItemElement - })} - + if (item.featureFlag) { + navItemElement = ( + + {navItemElement} + + ) + } + + return navItemElement + })} + + ) } } diff --git a/ui/src/shared/components/CloudUpgradeButton.tsx b/ui/src/shared/components/CloudUpgradeButton.tsx index 4e06709d3f..854e73e892 100644 --- a/ui/src/shared/components/CloudUpgradeButton.tsx +++ b/ui/src/shared/components/CloudUpgradeButton.tsx @@ -1,25 +1,56 @@ // Libraries import React, {FC} from 'react' import {Link} from 'react-router' +import {connect} from 'react-redux' // Components import CloudOnly from 'src/shared/components/cloud/CloudOnly' // Constants import {CLOUD_URL, CLOUD_CHECKOUT_PATH} from 'src/shared/constants' +import { + HIDE_UPGRADE_CTA_KEY, + PAID_ORG_HIDE_UPGRADE_SETTING, +} from 'src/cloud/constants' -const CloudUpgradeButton: FC = () => { +// Types +import {AppState, OrgSetting} from 'src/types' + +interface StateProps { + inView: boolean +} + +const CloudUpgradeButton: FC = ({inView}) => { return ( - - Upgrade Now - + {inView ? ( + + Upgrade Now + + ) : null} ) } -export default CloudUpgradeButton +const mstp = ({ + cloud: { + orgSettings: {settings}, + }, +}: AppState) => { + const hideUpgradeButtonSetting = settings.find( + (setting: OrgSetting) => setting.key === HIDE_UPGRADE_CTA_KEY + ) + if ( + !hideUpgradeButtonSetting || + hideUpgradeButtonSetting.value !== PAID_ORG_HIDE_UPGRADE_SETTING.value + ) { + return {inView: true} + } + return {inView: false} +} + +export default connect(mstp)(CloudUpgradeButton) diff --git a/ui/src/shared/components/CloudUpgradeNavBanner.tsx b/ui/src/shared/components/CloudUpgradeNavBanner.tsx index 102989682c..507e51f66d 100644 --- a/ui/src/shared/components/CloudUpgradeNavBanner.tsx +++ b/ui/src/shared/components/CloudUpgradeNavBanner.tsx @@ -1,6 +1,7 @@ // Libraries import React, {FC} from 'react' import {Link} from 'react-router' +import {connect} from 'react-redux' // Components import { @@ -17,44 +18,74 @@ import CloudOnly from 'src/shared/components/cloud/CloudOnly' // Constants import {CLOUD_URL, CLOUD_CHECKOUT_PATH} from 'src/shared/constants' +import { + HIDE_UPGRADE_CTA_KEY, + PAID_ORG_HIDE_UPGRADE_SETTING, +} from 'src/cloud/constants' -const CloudUpgradeNavBanner: FC = () => { +// Types +import {AppState, OrgSetting} from 'src/types' + +interface StateProps { + inView: boolean +} + +const CloudUpgradeNavBanner: FC = ({inView}) => { return ( <> - - - + - - Need more wiggle room? - - - - - Upgrade Now - - - - - - Upgrade Now - - + + Need more wiggle room? + + + + + Upgrade Now + + + + + + Upgrade Now + + + ) : null} ) } -export default CloudUpgradeNavBanner +const mstp = ({ + cloud: { + orgSettings: {settings}, + }, +}: AppState) => { + const hideUpgradeButtonSetting = settings.find( + (setting: OrgSetting) => setting.key === HIDE_UPGRADE_CTA_KEY + ) + if ( + !hideUpgradeButtonSetting || + hideUpgradeButtonSetting.value !== PAID_ORG_HIDE_UPGRADE_SETTING.value + ) { + return {inView: true} + } + return {inView: false} +} + +export default connect(mstp)(CloudUpgradeNavBanner) diff --git a/ui/src/store/configureStore.ts b/ui/src/store/configureStore.ts index 803be750ef..01e4dd55d7 100644 --- a/ui/src/store/configureStore.ts +++ b/ui/src/store/configureStore.ts @@ -37,6 +37,10 @@ import {membersReducer} from 'src/members/reducers' import {autoRefreshReducer} from 'src/shared/reducers/autoRefresh' import {limitsReducer, LimitsState} from 'src/cloud/reducers/limits' import {demoDataReducer, DemoDataState} from 'src/cloud/reducers/demodata' +import { + orgSettingsReducer, + OrgSettingsState, +} from 'src/cloud/reducers/orgsettings' import checksReducer from 'src/checks/reducers' import rulesReducer from 'src/notifications/rules/reducers' import endpointsReducer from 'src/notifications/endpoints/reducers' @@ -58,9 +62,14 @@ export const rootReducer = combineReducers({ ...sharedReducers, autoRefresh: autoRefreshReducer, alertBuilder: alertBuilderReducer, - cloud: combineReducers<{limits: LimitsState; demoData: DemoDataState}>({ + cloud: combineReducers<{ + limits: LimitsState + demoData: DemoDataState + orgSettings: OrgSettingsState + }>({ limits: limitsReducer, demoData: demoDataReducer, + orgSettings: orgSettingsReducer, }), currentPage: currentPageReducer, currentDashboard: currentDashboardReducer, diff --git a/ui/src/types/cloud.ts b/ui/src/types/cloud.ts index e6a31ad6f2..9206a97912 100644 --- a/ui/src/types/cloud.ts +++ b/ui/src/types/cloud.ts @@ -39,3 +39,18 @@ export interface LimitsStatus { status: string } } +export interface OrgSetting { + key: string + value: string +} + +export interface OrgSettings { + orgID: string + settings: OrgSetting[] +} + +export interface OrgSettingsResponse { + status: number + statusText: string + json: () => Promise +} diff --git a/ui/src/types/stores.ts b/ui/src/types/stores.ts index b3046fb184..2e225ed033 100644 --- a/ui/src/types/stores.ts +++ b/ui/src/types/stores.ts @@ -25,6 +25,7 @@ import {LimitsState} from 'src/cloud/reducers/limits' import {AlertBuilderState} from 'src/alerting/reducers/alertBuilder' import {CurrentPage} from 'src/shared/reducers/currentPage' import {DemoDataState} from 'src/cloud/reducers/demodata' +import {OrgSettingsState} from 'src/cloud/reducers/orgsettings' import {ResourceState} from 'src/types' @@ -32,7 +33,11 @@ export interface AppState { alertBuilder: AlertBuilderState app: AppPresentationState autoRefresh: AutoRefreshState - cloud: {limits: LimitsState; demoData: DemoDataState} + cloud: { + limits: LimitsState + demoData: DemoDataState + orgSettings: OrgSettingsState + } currentPage: CurrentPage currentDashboard: CurrentDashboardState dataLoading: DataLoadingState