Skip to content

Commit 8ffe65d

Browse files
Backlog/v12 integation index patterns (#2260)
* chore[](): updated go deps * feat[backend](integrations): added index patterns after integration creations * feat[backend](integrations): added remove index pattern on integration removal * chore[](): updated go deps * fix[backend](integrations): added index pattern remove error handling and setup integrations module after opensearch has fully updated * fix[frontend](ngnx): integrations page default to /index.html * fix[frontend](logexplorer): added indexpattern filter dropdown * fix[backend](integrations): fixed indexpattern-integration activation and removal flow * fix[backend](integrations): fixed create indexpattern logic * fix[backend](index_patterns): added index pattern sanitization * fix[backend](integrations): added size verifications after index_pattern filter consult * fix[backend](integrations): added database integration save before opensearch index pattern to avoid --------- Signed-off-by: Alex Sánchez <alex.sanchez@utmstack.com> Co-authored-by: Yorjander Hernandez Vergara <99102374+Kbayero@users.noreply.github.com>
1 parent 2a54993 commit 8ffe65d

22 files changed

Lines changed: 318 additions & 103 deletions

File tree

backend/modules.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,6 @@ func initModules(db *gorm.DB, cfg *config) *modules {
162162
}
163163
datasourcesMod := datasources.NewModule(dsUC, dsGroupUC, dsReconciler, billingMod.License(), agentClient)
164164

165-
integrationsMod := integrations.NewModule(db, cipher,
166-
env.String("INTEGRATIONS_TENANT_DIR", "/workdir/pipeline", false),
167-
dsUC,
168-
)
169-
170165
opensearchMod := opensearchgw.NewModule(db, cfg.esHost != "")
171166
notificationsMod := notifications.NewModule(db, auditMod.Logger())
172167

@@ -184,6 +179,7 @@ func initModules(db *gorm.DB, cfg *config) *modules {
184179
)
185180
}
186181

182+
187183
iamMod := iam.NewModule(authUsecase, userUsecase, roleUsecase, tfaUsecase, apiKeyUsecase, idpUsecase, samlUsecase, cfg.uploadDir)
188184
socAIMod := socai.NewModule(cfg.socAIBaseURL, cfg.internalKey, cipher,
189185
env.String("INTEGRATIONS_TENANT_DIR", "/workdir/pipeline", false))
@@ -196,6 +192,13 @@ func initModules(db *gorm.DB, cfg *config) *modules {
196192
)
197193
adauditMod := adaudit.NewModule(db)
198194

195+
//loaded after opensearch has fully loaded
196+
integrationsMod := integrations.NewModule(db, cipher,
197+
env.String("INTEGRATIONS_TENANT_DIR", "/workdir/pipeline", false),
198+
dsUC,
199+
opensearchMod.GetIndexPatternUsecase(),
200+
)
201+
199202
var mcpModule *mcpmod.Module
200203
if cfg.mcpEnabled {
201204
mcpModule = mcpmod.NewModule(&mcpmod.Deps{

backend/modules/integrations/module.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/threatwinds/go-sdk/catcher"
77
ds_connectors "github.com/utmstack/utmstack/backend/modules/datasources/connectors"
8+
os_connectors "github.com/utmstack/utmstack/backend/modules/opensearch/connectors"
89
"github.com/utmstack/utmstack/backend/modules/integrations/connectors"
910
"github.com/utmstack/utmstack/backend/modules/integrations/repository"
1011
"github.com/utmstack/utmstack/backend/modules/integrations/usecase"
@@ -17,16 +18,23 @@ type Module struct {
1718
tenants *usecase.TenantUsecase
1819
modules connectors.ModuleUsecase
1920
bootstrap *usecase.Bootstrap
21+
opensearch os_connectors.IndexPatternUsecase
2022
}
2123

22-
func NewModule(db *gorm.DB, cipher connectors.Cipher, tenantDir string, datasources ds_connectors.DatasourceUsecase) *Module {
24+
func NewModule(
25+
db *gorm.DB,
26+
cipher connectors.Cipher,
27+
tenantDir string,
28+
datasources ds_connectors.DatasourceUsecase,
29+
opensearch os_connectors.IndexPatternUsecase,
30+
) *Module {
2331
store := repository.NewTenantStore(tenantDir)
2432
schema := repository.NewCodeSchemaProvider() // field schema lives in code
2533
verif := verifier.NewBackendVerifier()
2634

2735
moduleRepo := repository.NewModuleRepository(db)
2836
// store doubles as the tenant-file toggler: enable/disable <module>.yaml(.disabled).
29-
moduleUC := usecase.NewModuleUsecase(moduleRepo, store)
37+
moduleUC := usecase.NewModuleUsecase(moduleRepo, store,opensearch)
3038

3139
return &Module{
3240
db: db,

backend/modules/integrations/usecase/module.go

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,30 @@ package usecase
33
import (
44
"context"
55
"fmt"
6-
6+
"github.com/threatwinds/go-sdk/catcher"
77
"github.com/utmstack/utmstack/backend/modules/integrations/connectors"
88
"github.com/utmstack/utmstack/backend/modules/integrations/domain"
99
"github.com/utmstack/utmstack/backend/modules/integrations/dto"
10+
11+
os_connectors "github.com/utmstack/utmstack/backend/modules/opensearch/connectors"
12+
os_dto "github.com/utmstack/utmstack/backend/modules/opensearch/dto"
1013
)
1114

1215
type moduleUsecase struct {
1316
repo connectors.ModuleRepository
1417
tenantFileTog connectors.TenantFileToggler
18+
opensearch os_connectors.IndexPatternUsecase
1519
}
1620

1721
func NewModuleUsecase(
1822
repo connectors.ModuleRepository,
1923
tenantFile connectors.TenantFileToggler,
24+
opensearch os_connectors.IndexPatternUsecase,
2025
) connectors.ModuleUsecase {
2126
return &moduleUsecase{
2227
repo: repo,
2328
tenantFileTog: tenantFile,
29+
opensearch: opensearch,
2430
}
2531
}
2632

@@ -51,6 +57,32 @@ func (u *moduleUsecase) ActivateDeactivate(ctx context.Context, req dto.ModuleAc
5157
return nil, err
5258
}
5359

60+
results,count,err :=u.opensearch.List(ctx,os_dto.IndexPatternFilters{
61+
ModuleEquals: &module.ModuleName,
62+
})
63+
if err != nil {
64+
return nil,err
65+
}
66+
if count > 0 && len(results)>0 {
67+
//if module has index patterns
68+
pattern:=results[0]
69+
70+
if _,err := u.opensearch.Update(ctx,os_dto.UpdateIndexPatternRequest{
71+
ID: pattern.ID,
72+
Pattern: pattern.Pattern,
73+
PatternModule: pattern.PatternModule,
74+
IsActive: &fresh.ModuleActive,
75+
PatternSystem:pattern.PatternSystem,
76+
});err!=nil{
77+
catcher.Error("failed to toggle is active for module's index pattern, rolling back", err, map[string]any{"module":module.ModuleName,"pattern":pattern} )
78+
module.ModuleActive = !active
79+
if err_ := u.repo.Save(ctx, module); err_ != nil {
80+
catcher.Error("failed to change module status after index pattern activation toggle", err, map[string]any{"module":module.ModuleName,"pattern":pattern} )
81+
return nil, err
82+
}
83+
return nil,err
84+
}
85+
}
5486
resp := dto.FromModule(*fresh)
5587
return &resp, nil
5688
}
@@ -128,9 +160,22 @@ func (u *moduleUsecase) Create(ctx context.Context, req dto.CreateModuleRequest)
128160
ModuleActive: false,
129161
IsSystem: false,
130162
}
163+
164+
131165
if err := u.repo.Save(ctx, m); err != nil {
132166
return nil, err
133167
}
168+
169+
if _,err:= u.opensearch.Create(ctx,os_dto.CreateIndexPatternRequest{
170+
PatternModule: &req.ModuleName,
171+
});err!=nil{
172+
//opensearch insertion fails, rolling back database
173+
if err:=u.repo.Delete(ctx,m.ID);err!=nil{
174+
catcher.Error("failed to remove module after index pattern creation fail", err, map[string]any{"module":m.ModuleName} )
175+
}
176+
return nil, err
177+
}
178+
134179
resp := dto.FromModule(*m)
135180
return &resp, nil
136181
}
@@ -163,5 +208,30 @@ func (u *moduleUsecase) Delete(ctx context.Context, id int64) error {
163208
if m.IsSystem {
164209
return domain.ErrSystemModule
165210
}
166-
return u.repo.Delete(ctx, id)
211+
212+
results,count,err :=u.opensearch.List(ctx,os_dto.IndexPatternFilters{
213+
ModuleEquals: &m.ModuleName,
214+
})
215+
if err != nil {
216+
return err
217+
}
218+
if count == 0 || len(results)==0{
219+
return fmt.Errorf("module %s index pattern not found.",m.ModuleName)
220+
}
221+
222+
err= u.repo.Delete(ctx, id)
223+
if err!=nil{
224+
return err
225+
}
226+
227+
pattern:=results[0]
228+
if err:=u.opensearch.Delete(ctx,pattern.ID);err!=nil{
229+
//if index patter deletion fails integration must be restored
230+
if err:=u.repo.Save(ctx,m);err!=nil{
231+
catcher.Error("failed to restore module after index pattern remove fail", err, map[string]any{"module":m.ModuleName} )
232+
}
233+
return err
234+
}
235+
236+
return nil
167237
}

backend/modules/opensearch/connectors/repository.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type GatewayRepository interface {
1212
PropertyValuesWithCount(ctx context.Context, index, field string, filters map[string]any, top int, orderByCount, sortAsc bool) (map[string]int64, error)
1313
IndexProperties(ctx context.Context, pattern string) ([]dto.IndexPropertyType, error)
1414
Indices(ctx context.Context, pattern string) ([]dto.IndexInfo, error)
15+
EnsureIndexPattern(ctx context.Context, pattern string) error
1516
DeleteIndices(ctx context.Context, indices []string) error
1617
Search(ctx context.Context, index string, query map[string]any, from, size int, sortField, sortOrder string) ([]map[string]any, int64, error)
1718
Count(ctx context.Context, index string, query map[string]any) (int64, error)

backend/modules/opensearch/repository/index_pattern_pg.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ func NewIndexPatternRepository(db *gorm.DB) connectors.IndexPatternRepository {
1919
}
2020

2121
func (r *pgIndexPatternRepository) Create(ctx context.Context, p *domain.UtmIndexPattern) error {
22+
sanitized, err := ensureIndexPattern(ctx, p.Pattern)
23+
if err != nil {
24+
return err
25+
}
26+
p.Pattern = sanitized
2227
return r.db.WithContext(ctx).Create(p).Error
2328
}
2429

backend/modules/opensearch/repository/opensearch_gateway.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,33 @@ package repository
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
8+
"net/http"
9+
"regexp"
710
"sort"
811
"strconv"
912
"strings"
1013

1114
osdk "github.com/threatwinds/go-sdk/os"
1215
"github.com/utmstack/utmstack/backend/modules/opensearch/connectors"
1316
"github.com/utmstack/utmstack/backend/modules/opensearch/dto"
17+
"github.com/utmstack/utmstack/backend/pkg/constants"
1418
)
1519

20+
var (
21+
reInvalidPatternChars = regexp.MustCompile(`[^a-z0-9_\-*]+`)
22+
reCollapsePatternDash = regexp.MustCompile(`-+`)
23+
)
24+
25+
func sanitizePattern(pattern string) string {
26+
s := strings.ToLower(strings.TrimSpace(pattern))
27+
s = reInvalidPatternChars.ReplaceAllString(s, "-")
28+
s = reCollapsePatternDash.ReplaceAllString(s, "-")
29+
s = strings.Trim(s, "-_")
30+
return s
31+
}
32+
1633
type osGatewayRepo struct {
1734
mapper *osdk.FieldMapper
1835
}
@@ -135,6 +152,60 @@ func (r *osGatewayRepo) Indices(ctx context.Context, pattern string) ([]dto.Inde
135152
return out, nil
136153
}
137154

155+
func (r *osGatewayRepo) EnsureIndexPattern(ctx context.Context, pattern string) error {
156+
_, err := ensureIndexPattern(ctx, pattern)
157+
return err
158+
}
159+
160+
func ensureIndexPattern(ctx context.Context, pattern string) (string, error) {
161+
sanitized := sanitizePattern(pattern)
162+
if sanitized == "" || sanitized == "*" {
163+
return "", errors.New("opensearch: pattern is empty")
164+
}
165+
name := templateNameFor(sanitized)
166+
if name == "" {
167+
return "", fmt.Errorf("opensearch: pattern %q has no valid template name", sanitized)
168+
}
169+
path := "_index_template/" + name
170+
171+
_, status, err := osdk.DoRequest(ctx, http.MethodGet, path, nil)
172+
if err != nil {
173+
return "", err
174+
}
175+
if status == http.StatusOK {
176+
return sanitized, nil
177+
}
178+
if status != http.StatusNotFound {
179+
return "", fmt.Errorf("opensearch: probe %q: status %d", path, status)
180+
}
181+
182+
body := map[string]any{
183+
"index_patterns": []string{sanitized},
184+
"priority": 0,
185+
"template": map[string]any{
186+
"settings": map[string]any{
187+
"index.mapping.total_fields.limit": constants.OS_INDEX_FIELD_LIMIT,
188+
"number_of_shards": constants.OS_INDEX_NUMBER_OF_SHARDS,
189+
"number_of_replicas": constants.OS_INDEX_NUMBER_OF_REPLICAS,
190+
},
191+
},
192+
}
193+
data, status, err := osdk.DoRequest(ctx, http.MethodPut, path, body)
194+
if err != nil {
195+
return "", err
196+
}
197+
if status < 200 || status >= 300 {
198+
return "", fmt.Errorf("opensearch: create index pattern %q: status %d: %s", sanitized, status, string(data))
199+
}
200+
return sanitized, nil
201+
}
202+
203+
func templateNameFor(pattern string) string {
204+
n := strings.ReplaceAll(pattern, "*", "")
205+
n = strings.Trim(n, "-_.")
206+
return n
207+
}
208+
138209
func (r *osGatewayRepo) DeleteIndices(ctx context.Context, indices []string) error {
139210
for _, idx := range indices {
140211
if err := osdk.DeleteIndex(ctx, idx); err != nil {

backend/modules/opensearch/repository/opensearch_ism.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
osdk "github.com/threatwinds/go-sdk/os"
1010
"github.com/utmstack/utmstack/backend/modules/opensearch/domain"
1111
"github.com/utmstack/utmstack/backend/modules/opensearch/dto"
12+
"github.com/utmstack/utmstack/backend/pkg/constants"
1213
)
1314

1415
// ISMClient performs Index State Management (and snapshot/mapping) calls against
@@ -78,8 +79,6 @@ func (c *ISMClient) UpdatePolicy(ctx context.Context, policy domain.IndexPolicy,
7879
return nil
7980
}
8081

81-
const fieldLimit = 50000
82-
8382
func (c *ISMClient) EnsureFieldLimitTemplate(ctx context.Context, patterns []string) error {
8483
if len(patterns) == 0 {
8584
return nil
@@ -90,9 +89,9 @@ func (c *ISMClient) EnsureFieldLimitTemplate(ctx context.Context, patterns []str
9089
"priority": 0,
9190
"template": map[string]any{
9291
"settings": map[string]any{
93-
"index.mapping.total_fields.limit": fieldLimit,
94-
"number_of_shards": 3,
95-
"number_of_replicas": 0,
92+
"index.mapping.total_fields.limit": constants.OS_INDEX_FIELD_LIMIT,
93+
"number_of_shards": constants.OS_INDEX_NUMBER_OF_SHARDS,
94+
"number_of_replicas": constants.OS_INDEX_NUMBER_OF_REPLICAS,
9695
},
9796
},
9897
}
@@ -108,7 +107,7 @@ func (c *ISMClient) EnsureFieldLimitTemplate(ctx context.Context, patterns []str
108107

109108
func (c *ISMClient) RaiseFieldLimit(ctx context.Context, pattern string) error {
110109
path := fmt.Sprintf("%s/_settings", pattern)
111-
body := map[string]any{"index.mapping.total_fields.limit": fieldLimit}
110+
body := map[string]any{"index.mapping.total_fields.limit": constants.OS_INDEX_FIELD_LIMIT}
112111
data, status, err := c.do(ctx, http.MethodPut, path, body)
113112
if err != nil {
114113
return err

backend/modules/opensearch/usecase/index_pattern.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ package usecase
33
import (
44
"context"
55
"errors"
6+
"fmt"
7+
"strings"
68

79
"github.com/threatwinds/go-sdk/catcher"
810
"github.com/utmstack/utmstack/backend/modules/opensearch/connectors"
911
"github.com/utmstack/utmstack/backend/modules/opensearch/domain"
1012
"github.com/utmstack/utmstack/backend/modules/opensearch/dto"
13+
"github.com/utmstack/utmstack/backend/pkg/constants"
1114
)
1215

1316
var errIDMustBeAbsent = errors.New("id must be absent on create")
17+
var errInvalidPattern = errors.New("invalid pattern")
1418

1519
type indexPatternUsecase struct {
1620
repo connectors.IndexPatternRepository
@@ -36,6 +40,15 @@ func (u *indexPatternUsecase) Create(ctx context.Context, req dto.CreateIndexPat
3640
}
3741
systemFalse := false
3842

43+
if req.Pattern == "" && req.PatternModule != nil {
44+
normalized := strings.ReplaceAll(strings.Trim(strings.ToLower(*req.PatternModule), ""), " ", "-")
45+
req.Pattern = fmt.Sprintf("%s-%s-*", constants.CUSTOM_INDEX_PREFIX, normalized)
46+
}
47+
48+
if req.Pattern == "" || req.Pattern == " " {
49+
return nil, errInvalidPattern
50+
}
51+
3952
p := &domain.UtmIndexPattern{
4053
Pattern: req.Pattern,
4154
PatternModule: req.PatternModule,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package constants
2+
3+
const(
4+
CUSTOM_INDEX_PREFIX = "custom";
5+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package constants
2+
3+
const (
4+
OS_INDEX_FIELD_LIMIT = 50000
5+
OS_INDEX_NUMBER_OF_SHARDS = 3
6+
OS_INDEX_NUMBER_OF_REPLICAS = 0
7+
)

0 commit comments

Comments
 (0)