diff --git a/CHANGELOG.md b/CHANGELOG.md index eff54b105cc..fbacb797d5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features 1. [18888](https://github.com/influxdata/influxdb/pull/18888): Add event source to influx stack operations +1. [18910](https://github.com/influxdata/influxdb/pull/18910): Add uninstall functionality for stacks ### Bug Fixes diff --git a/cmd/influx/template.go b/cmd/influx/template.go index e4b47bbda3f..7ff12e313c6 100644 --- a/cmd/influx/template.go +++ b/cmd/influx/template.go @@ -1780,7 +1780,7 @@ func writeStackRows(tabW *internal.TabWriter, stacks ...pkger.Stack) { tabW.Write(map[string]interface{}{ "ID": stack.ID, "OrgID": stack.OrgID, - "Active": latest.EventType != pkger.StackEventDelete, + "Active": latest.EventType != pkger.StackEventUninstalled, "Name": latest.Name, "Description": latest.Description, "Num Resources": len(latest.Resources), diff --git a/cmd/influx/template_test.go b/cmd/influx/template_test.go index 26c9086aa0c..a61fe60739f 100644 --- a/cmd/influx/template_test.go +++ b/cmd/influx/template_test.go @@ -795,6 +795,10 @@ func (f *fakePkgSVC) ListStacks(ctx context.Context, orgID influxdb.ID, filter p panic("not implemented") } +func (f *fakePkgSVC) UninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (pkger.Stack, error) { + panic("not implemeted") +} + func (f *fakePkgSVC) DeleteStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) error { panic("not implemented") } diff --git a/cmd/influxd/launcher/pkger_test.go b/cmd/influxd/launcher/pkger_test.go index a6e30f567ee..6b19e82920e 100644 --- a/cmd/influxd/launcher/pkger_test.go +++ b/cmd/influxd/launcher/pkger_test.go @@ -250,6 +250,55 @@ func TestLauncher_Pkger(t *testing.T) { return obj } + validateAllResourcesRemoved := func(t *testing.T, summary pkger.Summary) { + t.Helper() + + for _, b := range summary.Buckets { + _, err := resourceCheck.getBucket(t, bySafeID(b.ID)) + assertErrorCode(t, influxdb.ENotFound, err) + } + + for _, c := range summary.Checks { + _, err := resourceCheck.getCheck(t, byID(c.Check.GetID())) + assert.Error(t, err) + } + + for _, d := range summary.Dashboards { + _, err := resourceCheck.getDashboard(t, bySafeID(d.ID)) + assertErrorCode(t, influxdb.ENotFound, err) + } + + for _, l := range summary.Labels { + _, err := resourceCheck.getLabel(t, bySafeID(l.ID)) + assertErrorCode(t, influxdb.ENotFound, err) + } + + for _, e := range summary.NotificationEndpoints { + _, err := resourceCheck.getEndpoint(t, byID(e.NotificationEndpoint.GetID())) + assert.Error(t, err) + } + + for _, r := range summary.NotificationRules { + _, err := resourceCheck.getRule(t, bySafeID(r.ID)) + assert.Error(t, err) + } + + for _, ta := range summary.Tasks { + _, err := resourceCheck.getTask(t, bySafeID(ta.ID)) + assert.Error(t, err) + } + + for _, te := range summary.TelegrafConfigs { + _, err := resourceCheck.getTelegrafConfig(t, byID(te.TelegrafConfig.ID)) + assert.Error(t, err) + } + + for _, v := range summary.Variables { + _, err := resourceCheck.getVariable(t, bySafeID(v.ID)) + assertErrorCode(t, influxdb.ENotFound, err) + } + } + t.Run("managing pkg lifecycle with stacks", func(t *testing.T) { t.Run("list stacks", func(t *testing.T) { stacks, err := svc.ListStacks(ctx, l.Org.ID, pkger.ListFilter{}) @@ -313,8 +362,8 @@ func TestLauncher_Pkger(t *testing.T) { cleanup() }) - t.Run("delete a stack", func(t *testing.T) { - t.Run("should delete the stack and all resources associated with it", func(t *testing.T) { + t.Run("uninstall a stack", func(t *testing.T) { + t.Run("should remove all resources associated with it", func(t *testing.T) { newStack, cleanup := newStackFn(t, pkger.StackCreate{}) defer cleanup() @@ -358,42 +407,77 @@ func TestLauncher_Pkger(t *testing.T) { require.Len(t, sum.Variables, 1) assert.NotZero(t, sum.Variables[0].ID) - deleteStackFn(t, newStack.ID) + _, err = svc.UninstallStack(ctx, struct{ OrgID, UserID, StackID influxdb.ID }{ + OrgID: l.Org.ID, + UserID: l.User.ID, + StackID: newStack.ID, + }) + require.NoError(t, err) matchingStack, err := svc.ReadStack(ctx, newStack.ID) require.NoError(t, err) ev := matchingStack.LatestEvent() - assert.Equal(t, pkger.StackEventDelete, ev.EventType) + assert.Equal(t, pkger.StackEventUninstalled, ev.EventType) assert.Empty(t, ev.Resources) - assert.Empty(t, ev.Sources) - - _, err = resourceCheck.getBucket(t, byID(influxdb.ID(sum.Buckets[0].ID))) - assert.Error(t, err) - _, err = resourceCheck.getCheck(t, byID(sum.Checks[0].Check.GetID())) - assert.Error(t, err) + validateAllResourcesRemoved(t, sum) + }) + }) - _, err = resourceCheck.getDashboard(t, byID(influxdb.ID(sum.Dashboards[0].ID))) - assert.Error(t, err) + t.Run("delete a stack", func(t *testing.T) { + t.Run("should delete the stack and all resources associated with it", func(t *testing.T) { + newStack, cleanup := newStackFn(t, pkger.StackCreate{}) + defer cleanup() - _, err = resourceCheck.getLabel(t, byID(influxdb.ID(sum.Labels[0].ID))) - assert.Error(t, err) + newEndpointPkgName := "non-existent-endpoint" + allResourcesPkg := newTemplate( + newBucketObject("non-existent-bucket", "", ""), + newCheckDeadmanObject(t, "non-existent-check", "", time.Minute), + newDashObject("non-existent-dash", "", ""), + newEndpointHTTP(newEndpointPkgName, "", ""), + newLabelObject("non-existent-label", "", "", ""), + newRuleObject(t, "non-existent-rule", "", newEndpointPkgName, ""), + newTaskObject("non-existent-task", "", ""), + newTelegrafObject("non-existent-tele", "", ""), + newVariableObject("non-existent-var", "", ""), + ) - _, err = resourceCheck.getEndpoint(t, byID(sum.NotificationEndpoints[0].NotificationEndpoint.GetID())) - assert.Error(t, err) + impact, err := svc.Apply(ctx, l.Org.ID, l.User.ID, + pkger.ApplyWithTemplate(allResourcesPkg), + pkger.ApplyWithStackID(newStack.ID), + ) + require.NoError(t, err) - _, err = resourceCheck.getRule(t, byID(influxdb.ID(sum.NotificationRules[0].ID))) - assert.Error(t, err) + sum := impact.Summary - _, err = resourceCheck.getTask(t, byID(influxdb.ID(sum.Tasks[0].ID))) - assert.Error(t, err) + require.Len(t, sum.Buckets, 1) + assert.NotZero(t, sum.Buckets[0].ID) + require.Len(t, sum.Checks, 1) + assert.NotZero(t, sum.Checks[0].Check.GetID()) + require.Len(t, sum.Dashboards, 1) + assert.NotZero(t, sum.Dashboards[0].ID) + require.Len(t, sum.Labels, 1) + assert.NotZero(t, sum.Labels[0].ID) + require.Len(t, sum.NotificationEndpoints, 1) + assert.NotZero(t, sum.NotificationEndpoints[0].NotificationEndpoint.GetID()) + require.Len(t, sum.NotificationRules, 1) + assert.NotZero(t, sum.NotificationRules[0].ID) + require.Len(t, sum.Tasks, 1) + assert.NotZero(t, sum.Tasks[0].ID) + require.Len(t, sum.TelegrafConfigs, 1) + assert.NotZero(t, sum.TelegrafConfigs[0].TelegrafConfig.ID) + require.Len(t, sum.Variables, 1) + assert.NotZero(t, sum.Variables[0].ID) - _, err = resourceCheck.getTelegrafConfig(t, byID(sum.TelegrafConfigs[0].TelegrafConfig.ID)) - assert.Error(t, err) + err = svc.DeleteStack(ctx, struct{ OrgID, UserID, StackID influxdb.ID }{ + OrgID: l.Org.ID, + UserID: l.User.ID, + StackID: newStack.ID, + }) + require.NoError(t, err) - _, err = resourceCheck.getVariable(t, byID(influxdb.ID(sum.Variables[0].ID))) - assert.Error(t, err) + validateAllResourcesRemoved(t, sum) }) t.Run("that has already been deleted should be successful", func(t *testing.T) { @@ -2178,6 +2262,7 @@ func TestLauncher_Pkger(t *testing.T) { t.Run("errors incurred during application of package rolls back to state before package", func(t *testing.T) { stacks, err := svc.ListStacks(ctx, l.Org.ID, pkger.ListFilter{}) require.NoError(t, err) + require.Empty(t, stacks) svc := pkger.NewService( pkger.WithBucketSVC(l.BucketService(t)), @@ -2249,7 +2334,7 @@ func TestLauncher_Pkger(t *testing.T) { afterStacks, err := svc.ListStacks(ctx, l.Org.ID, pkger.ListFilter{}) require.NoError(t, err) - require.Len(t, afterStacks, len(stacks)) + require.Empty(t, afterStacks) }) hasLabelAssociations := func(t *testing.T, associations []pkger.SummaryLabel, numAss int, expectedNames ...string) { @@ -3979,6 +4064,12 @@ func (f *fakeRuleStore) CreateNotificationRule(ctx context.Context, nr influxdb. return f.NotificationRuleStore.CreateNotificationRule(ctx, nr, userID) } +func assertErrorCode(t *testing.T, expected string, err error) { + t.Helper() + assert.Error(t, err) + assert.Equal(t, expected, influxdb.ErrorCode(err)) +} + type resourceChecker struct { tl *TestLauncher } @@ -4008,6 +4099,12 @@ func byName(name string) getResourceOptFn { } } +func bySafeID(id pkger.SafeID) getResourceOptFn { + return func() getResourceOpt { + return getResourceOpt{id: influxdb.ID(id)} + } +} + func (r resourceChecker) getBucket(t *testing.T, getOpt getResourceOptFn) (influxdb.Bucket, error) { t.Helper() diff --git a/http/swagger.yml b/http/swagger.yml index 9dc57042644..1f0d3c1f236 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -4725,7 +4725,7 @@ paths: operationId: UpdateStack tags: - InfluxDB Templates - summary: Update a an InfluxDB Stack + summary: Update an InfluxDB Stack parameters: - in: path name: stack_id @@ -4801,6 +4801,32 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /stacks/{stack_id}/uninstall: + post: + operationId: UninstallStack + tags: + - InfluxDB Templates + summary: Uninstall an InfluxDB Stack + parameters: + - in: path + name: stack_id + required: true + schema: + type: string + description: The stack id + responses: + "200": + description: Influx stack uninstalled + content: + application/json: + schema: + $ref: "#/components/schemas/Stack" + default: + description: Unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /templates/apply: post: operationId: ApplyTemplate diff --git a/pkger/http_remote_service.go b/pkger/http_remote_service.go index 94b975db77b..40564cecd20 100644 --- a/pkger/http_remote_service.go +++ b/pkger/http_remote_service.go @@ -37,6 +37,20 @@ func (s *HTTPRemoteService) InitStack(ctx context.Context, userID influxdb.ID, s return convertRespStackToStack(respBody) } +func (s *HTTPRemoteService) UninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (Stack, error) { + var respBody RespStack + err := s.Client. + Post(httpc.BodyEmpty, RoutePrefixStacks, identifiers.StackID.String(), "/uninstall"). + QueryParams([2]string{"orgID", identifiers.OrgID.String()}). + DecodeJSON(&respBody). + Do(ctx) + if err != nil { + return Stack{}, err + } + + return convertRespStackToStack(respBody) +} + func (s *HTTPRemoteService) DeleteStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) error { return s.Client. Delete(RoutePrefixStacks, identifiers.StackID.String()). @@ -290,8 +304,8 @@ func convertRespStackEvent(ev RespStackEvent) (StackEvent, error) { eventType := StackEventCreate switch ev.EventType { - case "delete": - eventType = StackEventDelete + case "uninstall", "delete": // delete is included to maintain backwards compatibility + eventType = StackEventUninstalled case "update": eventType = StackEventUpdate } diff --git a/pkger/http_server_packages_deprecated_test.go b/pkger/http_server_packages_deprecated_test.go index ce2ab235b2e..e86a93101d5 100644 --- a/pkger/http_server_packages_deprecated_test.go +++ b/pkger/http_server_packages_deprecated_test.go @@ -1350,6 +1350,10 @@ func (f *fakeSVC) InitStack(ctx context.Context, userID influxdb.ID, stack pkger return f.initStackFn(ctx, userID, stack) } +func (f *fakeSVC) UninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (pkger.Stack, error) { + panic("not implemented") +} + func (f *fakeSVC) DeleteStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) error { panic("not implemented yet") } diff --git a/pkger/http_server_stack.go b/pkger/http_server_stack.go index 2f0cba2ce0f..19743831271 100644 --- a/pkger/http_server_stack.go +++ b/pkger/http_server_stack.go @@ -40,6 +40,7 @@ func NewHTTPServerStacks(log *zap.Logger, svc SVC) *HTTPServerStacks { r.Get("/", svr.readStack) r.Delete("/", svr.deleteStack) r.Patch("/", svr.updateStack) + r.Post("/uninstall", svr.uninstallStack) }) } @@ -244,11 +245,10 @@ func (s *HTTPServerStacks) deleteStack(w http.ResponseWriter, r *http.Request) { s.api.Err(w, r, err) return } - userID := auth.GetUserID() err = s.svc.DeleteStack(r.Context(), struct{ OrgID, UserID, StackID influxdb.ID }{ OrgID: orgID, - UserID: userID, + UserID: auth.GetUserID(), StackID: stackID, }) if err != nil { @@ -259,6 +259,38 @@ func (s *HTTPServerStacks) deleteStack(w http.ResponseWriter, r *http.Request) { s.api.Respond(w, r, http.StatusNoContent, nil) } +func (s *HTTPServerStacks) uninstallStack(w http.ResponseWriter, r *http.Request) { + orgID, err := getRequiredOrgIDFromQuery(r.URL.Query()) + if err != nil { + s.api.Err(w, r, err) + return + } + + stackID, err := stackIDFromReq(r) + if err != nil { + s.api.Err(w, r, err) + return + } + + auth, err := pctx.GetAuthorizer(r.Context()) + if err != nil { + s.api.Err(w, r, err) + return + } + + stack, err := s.svc.UninstallStack(r.Context(), struct{ OrgID, UserID, StackID influxdb.ID }{ + OrgID: orgID, + UserID: auth.GetUserID(), + StackID: stackID, + }) + if err != nil { + s.api.Err(w, r, err) + return + } + + s.api.Respond(w, r, http.StatusOK, convertStackToRespStack(stack)) +} + func (s *HTTPServerStacks) readStack(w http.ResponseWriter, r *http.Request) { stackID, err := stackIDFromReq(r) if err != nil { diff --git a/pkger/service.go b/pkger/service.go index 5717d885fdb..1567a04d695 100644 --- a/pkger/service.go +++ b/pkger/service.go @@ -104,15 +104,15 @@ type StackEventType uint const ( StackEventCreate StackEventType = iota StackEventUpdate - StackEventDelete + StackEventUninstalled ) func (e StackEventType) String() string { switch e { case StackEventCreate: return "create" - case StackEventDelete: - return "delete" + case StackEventUninstalled: + return "uninstall" case StackEventUpdate: return "update" default: @@ -125,6 +125,7 @@ const ResourceTypeStack influxdb.ResourceType = "stack" // SVC is the packages service interface. type SVC interface { InitStack(ctx context.Context, userID influxdb.ID, stack StackCreate) (Stack, error) + UninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (Stack, error) DeleteStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) error ListStacks(ctx context.Context, orgID influxdb.ID, filter ListFilter) ([]Stack, error) ReadStack(ctx context.Context, id influxdb.ID) (Stack, error) @@ -386,17 +387,45 @@ func (s *Service) InitStack(ctx context.Context, userID influxdb.ID, stCreate St return newStack, nil } +// UninstallStack will remove all resources associated with the stack. +func (s *Service) UninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (Stack, error) { + uninstalledStack, err := s.uninstallStack(ctx, identifiers) + if err != nil { + return Stack{}, err + } + + ev := uninstalledStack.LatestEvent() + ev.EventType = StackEventUninstalled + ev.Resources = nil + ev.UpdatedAt = s.timeGen.Now() + + uninstalledStack.Events = append(uninstalledStack.Events, ev) + if err := s.store.UpdateStack(ctx, uninstalledStack); err != nil { + s.log.Error("unable to update stack after uninstalling resources", zap.Error(err)) + } + return uninstalledStack, nil +} + // DeleteStack removes a stack and all the resources that have are associated with the stack. func (s *Service) DeleteStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (e error) { - stack, err := s.store.ReadStackByID(ctx, identifiers.StackID) + deletedStack, err := s.uninstallStack(ctx, identifiers) if influxdb.ErrorCode(err) == influxdb.ENotFound { return nil } if err != nil { return err } + + return s.store.DeleteStack(ctx, deletedStack.ID) +} + +func (s *Service) uninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (_ Stack, e error) { + stack, err := s.store.ReadStackByID(ctx, identifiers.StackID) + if err != nil { + return Stack{}, err + } if stack.OrgID != identifiers.OrgID { - return &influxdb.Error{ + return Stack{}, &influxdb.Error{ Code: influxdb.EConflict, Msg: "you do not have access to given stack ID", } @@ -405,7 +434,7 @@ func (s *Service) DeleteStack(ctx context.Context, identifiers struct{ OrgID, Us // providing empty template will remove all applied resources state, err := s.dryRun(ctx, identifiers.OrgID, new(Template), applyOptFromOptFns(ApplyWithStackID(identifiers.StackID))) if err != nil { - return err + return Stack{}, err } coordinator := newRollbackCoordinator(s.log, s.applyReqLimit) @@ -413,14 +442,9 @@ func (s *Service) DeleteStack(ctx context.Context, identifiers struct{ OrgID, Us err = s.applyState(ctx, coordinator, identifiers.OrgID, identifiers.UserID, state, nil) if err != nil { - return err + return Stack{}, err } - - stack.Events = append(stack.Events, StackEvent{ - EventType: StackEventDelete, - UpdatedAt: s.timeGen.Now(), - }) - return s.store.UpdateStack(ctx, stack) + return stack, nil } // ListFilter are filter options for filtering stacks from being returned. diff --git a/pkger/service_auth.go b/pkger/service_auth.go index 5bb1aacee42..a54828ad110 100644 --- a/pkger/service_auth.go +++ b/pkger/service_auth.go @@ -36,6 +36,14 @@ func (s *authMW) InitStack(ctx context.Context, userID influxdb.ID, newStack Sta return s.next.InitStack(ctx, userID, newStack) } +func (s *authMW) UninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (Stack, error) { + err := s.authAgent.IsWritable(ctx, identifiers.OrgID, ResourceTypeStack) + if err != nil { + return Stack{}, err + } + return s.next.UninstallStack(ctx, identifiers) +} + func (s *authMW) DeleteStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) error { err := s.authAgent.IsWritable(ctx, identifiers.OrgID, ResourceTypeStack) if err != nil { diff --git a/pkger/service_logging.go b/pkger/service_logging.go index 5e94f13a230..54e24b9a11d 100644 --- a/pkger/service_logging.go +++ b/pkger/service_logging.go @@ -43,6 +43,24 @@ func (s *loggingMW) InitStack(ctx context.Context, userID influxdb.ID, newStack return s.next.InitStack(ctx, userID, newStack) } +func (s *loggingMW) UninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (_ Stack, err error) { + defer func(start time.Time) { + if err == nil { + return + } + + s.logger.Error( + "failed to uninstall stack", + zap.Error(err), + zap.Stringer("orgID", identifiers.OrgID), + zap.Stringer("userID", identifiers.OrgID), + zap.Stringer("stackID", identifiers.StackID), + zap.Duration("took", time.Since(start)), + ) + }(time.Now()) + return s.next.UninstallStack(ctx, identifiers) +} + func (s *loggingMW) DeleteStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (err error) { defer func(start time.Time) { if err == nil { diff --git a/pkger/service_metrics.go b/pkger/service_metrics.go index 627b891f2f2..ff144cebe96 100644 --- a/pkger/service_metrics.go +++ b/pkger/service_metrics.go @@ -38,6 +38,12 @@ func (s *mwMetrics) InitStack(ctx context.Context, userID influxdb.ID, newStack return stack, rec(err) } +func (s *mwMetrics) UninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (Stack, error) { + rec := s.rec.Record("uninstall_stack") + stack, err := s.next.UninstallStack(ctx, identifiers) + return stack, rec(err) +} + func (s *mwMetrics) DeleteStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) error { rec := s.rec.Record("delete_stack") return rec(s.next.DeleteStack(ctx, identifiers)) diff --git a/pkger/service_tracing.go b/pkger/service_tracing.go index bf7ca09d064..a261393f868 100644 --- a/pkger/service_tracing.go +++ b/pkger/service_tracing.go @@ -27,6 +27,12 @@ func (s *traceMW) InitStack(ctx context.Context, userID influxdb.ID, newStack St return s.next.InitStack(ctx, userID, newStack) } +func (s *traceMW) UninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) (Stack, error) { + span, ctx := tracing.StartSpanFromContext(ctx) + defer span.Finish() + return s.next.UninstallStack(ctx, identifiers) +} + func (s *traceMW) DeleteStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID influxdb.ID }) error { span, ctx := tracing.StartSpanFromContext(ctx) defer span.Finish()