Skip to content

Commit a9d4fc4

Browse files
authored
Merge pull request #14 from flant/flant/refresh-index-patterns
Обновление существующих index-patterns
2 parents 38f3056 + a417cca commit a9d4fc4

File tree

11 files changed

+196
-13
lines changed

11 files changed

+196
-13
lines changed

.github/workflows/docker.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,4 @@ jobs:
4949
versionflags=-X 'main.appVersion=${{ github.ref_name }}'
5050
push: ${{ github.event_name != 'pull_request' }}
5151
tags: ${{ steps.meta.outputs.tags }}
52-
labels: ${{ steps.meta.outputs.labels }}
52+
labels: ${{ steps.meta.outputs.labels }}

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ jobs:
3535
generate_release_notes: true
3636
token: ${{ secrets.GITHUB_TOKEN }}
3737
files: |
38-
build/*
38+
build/*

ARCHITECTURE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,12 @@ osctl/
567567

568568
### 13. **indexpatterns** - Управление Kibana index patterns
569569

570+
**Обновление паттернов**
571+
Выполняется до создания паттернов.
572+
1. Получаем список существующих index patterns - имя и ID
573+
2. Через запрос в API OpenSearchDasboard `GET /api/index_patterns/_fields_for_wildcard?pattern={ИМЯ}&meta_fields=_source&meta_fields=_id&meta_fields=_type&meta_fields=_index&meta_fields=_score` получаем актуальный маппинг для индекса
574+
3. Обновляем паттерн по его ID - `PUT /api/saved_objects/index-pattern/{ID}"`
575+
570576
**Алгоритм (multitenancy):**
571577

572578
1. **Загрузка конфигурации тенантов**: Получаем `--indexpatterns-kibana-tenants-config` (по умолчанию `osctltenants.yaml`)
@@ -605,6 +611,7 @@ osctl/
605611
- Использует `--indexpatterns-kibana-tenants-config` для пути к конфигу тенантов
606612
- Использует `--kibana-index-regex` для построения паттернов в single-tenant режиме
607613
- Использует `--indexpatterns-recoverer-enabled` для создания `extracted_*` pattern (только в single-tenant режиме)
614+
- Использует `--indexpatterns-refresh-enabled` для обновления существующих index patterns
608615
- `extracted_*` по соображениям безопасности не создается в режиме multitenancy
609616

610617
### 14. **datasource** - Создание Kibana data sources

ENVIRONMENT_VARIABLES_AND_FLAGS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@
5151
- `coldstorage`
5252
- `extracteddelete`
5353
- `danglingchecker`
54-
- `sharding`
55-
- `indexpatterns`
56-
- `datasource`
54+
- `sharding`
55+
- `indexpatterns`
56+
- `datasource`
5757

5858
### Примеры использования:
5959

@@ -181,6 +181,7 @@ osctl --action=snapshot
181181
| `--indexpatterns-kibana-multitenancy` | `INDEXPATTERNS_KIBANA_MULTITENANCY` | Режим multitenancy | `false` |
182182
| `--indexpatterns-kibana-tenants-config` | `INDEXPATTERNS_KIBANA_TENANTS_CONFIG` | Путь к YAML с тенантами и index patterns | `osctltenants.yaml` |
183183
| `--indexpatterns-recoverer-enabled` | `INDEXPATTERNS_RECOVERER_ENABLED` | Создавать `extracted_*` с ссылкой на data-source | `false` |
184+
| `--indexpatterns-refresh-enabled` | `INDEXPATTERNS_REFRESH_ENABLED` | Обновлять существующие index patterns| `false` |
184185
| `--dry-run` | `DRY_RUN` | Показать создаваемые index patterns без создания | `false` |
185186

186187
Примечание: Список тенантов берётся из `--indexpatterns-kibana-tenants-config` (`INDEXPATTERNS_KIBANA_TENANTS_CONFIG`).

commands/indexpatterns.go

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"osctl/pkg/config"
7+
"osctl/pkg/kibana"
78
"osctl/pkg/logging"
89
"osctl/pkg/opensearch"
910
"osctl/pkg/utils"
@@ -29,6 +30,9 @@ func init() {
2930
}
3031

3132
func runIndexPatterns(cmd *cobra.Command, args []string) error {
33+
var refreshedPatterns []string
34+
var failedRefreshedPatterns []string
35+
3236
cfg := config.GetConfig()
3337

3438
if cfg.GetOSDURL() == "" {
@@ -40,6 +44,37 @@ func runIndexPatterns(cmd *cobra.Command, args []string) error {
4044
return fmt.Errorf("failed to create OpenSearch client: %v", err)
4145
}
4246

47+
if cfg.GetIndexPatternsRefreshEnabled() {
48+
49+
user := cfg.GetKibanaUser()
50+
pass := cfg.GetKibanaPass()
51+
_, _, existingIdsTitiles, err := getExistingIndexPatternTitles(osClient, ".kibana*")
52+
if err != nil {
53+
return err
54+
}
55+
kb := kibana.NewClient(utils.NormalizeURL(cfg.GetOSDURL()), user, pass, cfg.GetTimeout())
56+
logger.Info(fmt.Sprintf("Will refresh %d existing index-patterns", len(existingIdsTitiles)))
57+
for ip_id, ip_title := range existingIdsTitiles {
58+
if ip_title == "*" {
59+
logger.Warn("index-pattern '*' is too general and will be ignored")
60+
continue
61+
}
62+
if cfg.GetDryRun() {
63+
logger.Info(fmt.Sprintf("DRY RUN: Would refresh index-pattern %s:%s", ip_id, ip_title))
64+
refreshedPatterns = append(refreshedPatterns, ip_title)
65+
} else {
66+
logger.Info(fmt.Sprintf("Refreshing index-pattern %s:%s", ip_id, ip_title))
67+
if err := kb.RefreshIndexPattern(ip_id, ip_title); err == nil {
68+
logger.Info(fmt.Sprintf("Successfully refreshed index-pattern %s:%s", ip_id, ip_title))
69+
refreshedPatterns = append(refreshedPatterns, ip_title)
70+
} else {
71+
logger.Error(fmt.Sprintf("Failed to refresh index-pattern %s:%s: %s", ip_id, ip_title, err))
72+
failedRefreshedPatterns = append(failedRefreshedPatterns, ip_title)
73+
}
74+
}
75+
}
76+
77+
}
4378
var createdPatterns []string
4479

4580
if cfg.GetIndexPatternsKibanaMultitenancy() {
@@ -68,7 +103,7 @@ func runIndexPatterns(cmd *cobra.Command, args []string) error {
68103
logger.Info(fmt.Sprintf("Skip tenant %s: .kibana alias not found", t.Name))
69104
continue
70105
}
71-
existing, existingTitles, err := getExistingIndexPatternTitles(osClient, tenantIndex)
106+
existing, existingTitles, _, err := getExistingIndexPatternTitles(osClient, tenantIndex)
72107
if err != nil {
73108
return err
74109
}
@@ -142,8 +177,9 @@ func runIndexPatterns(cmd *cobra.Command, args []string) error {
142177
needed = append(needed, m[1]+"-*")
143178
}
144179
}
180+
145181
logger.Info(fmt.Sprintf("Required patterns (%d): %s", len(needed), strings.Join(needed, ", ")))
146-
existing, existingTitles, err := getExistingIndexPatternTitles(osClient, ".kibana")
182+
existing, existingTitles, _, err := getExistingIndexPatternTitles(osClient, ".kibana")
147183
if err != nil {
148184
return err
149185
}
@@ -226,6 +262,23 @@ func runIndexPatterns(cmd *cobra.Command, args []string) error {
226262
logger.Info(strings.Repeat("=", 60))
227263
logger.Info("INDEX PATTERNS SUMMARY")
228264
logger.Info(strings.Repeat("=", 60))
265+
266+
if len(refreshedPatterns) > 0 {
267+
logger.Info(fmt.Sprintf("Refreshed: %d index patterns", len(refreshedPatterns)))
268+
for _, name := range refreshedPatterns {
269+
logger.Info(fmt.Sprintf(" ✓ %s", name))
270+
}
271+
} else {
272+
logger.Info("No index patterns were refreshed")
273+
}
274+
275+
if len(failedRefreshedPatterns) > 0 {
276+
logger.Info(fmt.Sprintf("Failed while refresh: %d index patterns", len(failedRefreshedPatterns)))
277+
for _, name := range failedRefreshedPatterns {
278+
logger.Info(fmt.Sprintf(" ✕ %s", name))
279+
}
280+
}
281+
229282
if len(createdPatterns) > 0 {
230283
logger.Info(fmt.Sprintf("Created: %d index patterns", len(createdPatterns)))
231284
for _, name := range createdPatterns {
@@ -240,22 +293,28 @@ func runIndexPatterns(cmd *cobra.Command, args []string) error {
240293
return nil
241294
}
242295

243-
func getExistingIndexPatternTitles(osClient *opensearch.Client, index string) (map[string]struct{}, []string, error) {
296+
func getExistingIndexPatternTitles(osClient *opensearch.Client, index string) (map[string]struct{}, []string, map[string]string, error) {
244297
sr, err := osClient.Search(index, "q=type:index-pattern&size=1000")
245298
if err != nil {
246-
return nil, nil, err
299+
return nil, nil, nil, err
247300
}
248301
existing := map[string]struct{}{}
302+
exist_map_id_title := map[string]string{}
249303
titles := []string{}
250304
for _, h := range sr.Hits.Hits {
251305
if src, ok := h.Source["index-pattern"].(map[string]any); ok {
252306
if t, ok := src["title"].(string); ok {
253307
if _, seen := existing[t]; !seen {
254308
existing[t] = struct{}{}
255309
titles = append(titles, t)
310+
311+
}
312+
if _, seen := exist_map_id_title[h.ID]; !seen {
313+
p_id := strings.Split(h.ID, ":")
314+
exist_map_id_title[p_id[1]] = t
256315
}
257316
}
258317
}
259318
}
260-
return existing, titles, nil
319+
return existing, titles, exist_map_id_title, nil
261320
}

config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ recoverer_date_format: "%d-%m-%Y"
5555
kibana_index_regex: '^([\w-]+)-([\w-]*)(\d{4}[\.-]\d{2}[\.-]\d{2}(?:[\.-]\d{2})*)$'
5656
indexpatterns_kibana_multitenancy: false
5757
indexpatterns_recoverer_enabled: true
58+
indexpatterns_refresh_enabled: false
5859

5960
# indicesdelete:
6061
indicesdelete_check_snapshots: true

example/helm-example/templates/osctl/02-osctl-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ data:
101101
{{- else }}
102102
indexpatterns_recoverer_enabled: false
103103
{{- end }}
104+
indexpatterns_refresh_enabled: {{ .Values.indexpatterns.refresh_enabled | default true | quote }}
104105
105106
# indicesdelete:
106107
indicesdelete_check_snapshots: true

pkg/config/config.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type Config struct {
6565
IndexPatternsKibanaMultitenancy string
6666
IndexPatternsKibanaTenantsConfig string
6767
IndexPatternsRecovererEnabled string
68+
IndexPatternsRefreshEnabled string
6869
SnapshotsBackfillIndicesList string
6970
MaxConcurrentSnapshots string
7071
}
@@ -181,6 +182,7 @@ func LoadConfig(cmd *cobra.Command, commandName string) error {
181182
IndexPatternsKibanaMultitenancy: getValue(cmd, "indexpatterns-kibana-multitenancy", "INDEXPATTERNS_KIBANA_MULTITENANCY", viper.GetString("indexpatterns_kibana_multitenancy")),
182183
IndexPatternsKibanaTenantsConfig: getValue(cmd, "indexpatterns-kibana-tenants-config", "INDEXPATTERNS_KIBANA_TENANTS_CONFIG", viper.GetString("indexpatterns_kibana_tenants_config")),
183184
IndexPatternsRecovererEnabled: getValue(cmd, "indexpatterns-recoverer-enabled", "INDEXPATTERNS_RECOVERER_ENABLED", viper.GetString("indexpatterns_recoverer_enabled")),
185+
IndexPatternsRefreshEnabled: getValue(cmd, "indexpatterns-refresh-enabled", "INDEXPATTERNS_REFRESH_ENABLED", viper.GetString("indexpatterns_refresh_enabled")),
184186
SnapshotsBackfillIndicesList: getValue(cmd, "indices-list", "SNAPSHOTS_BACKFILL_INDICES_LIST", viper.GetString("snapshots_backfill_indices_list")),
185187
MaxConcurrentSnapshots: getValue(cmd, "max-concurrent-snapshots", "MAX_CONCURRENT_SNAPSHOTS", viper.GetString("max_concurrent_snapshots")),
186188
}
@@ -198,6 +200,12 @@ func LoadConfig(cmd *cobra.Command, commandName string) error {
198200
if repoToUse == "" {
199201
return fmt.Errorf("snap-repo is required (or set snapshot-manual-repo) for %s", commandName)
200202
}
203+
case "indexpatterns":
204+
if parseBoolWithDefault(configInstance.IndexPatternsRefreshEnabled, "indexpatterns_refresh_enabled") {
205+
if configInstance.KibanaUser == "" || configInstance.KibanaPass == "" {
206+
return fmt.Errorf("kibana-user and kibana-pass are required when indexpatterns-refresh-enabled is true")
207+
}
208+
}
201209
}
202210

203211
return nil
@@ -250,7 +258,8 @@ func setDefaults() {
250258
viper.SetDefault("kibana_multidomain_enabled", false)
251259
viper.SetDefault("datasource_name", "recoverer")
252260
viper.SetDefault("datasource_endpoint", "https://opendistro-recoverer:9200")
253-
viper.SetDefault("max_concurrent_snapshots", 3)
261+
viper.SetDefault("indexpatterns_refresh_enabled", false)
262+
viper.SetDefault("max_concurrent_snapshots", 3)
254263
}
255264

256265
func GetAvailableActions() []string {
@@ -427,6 +436,10 @@ func (c *Config) GetIndexPatternsRecovererEnabled() bool {
427436
return parseBoolWithDefault(c.IndexPatternsRecovererEnabled, "indexpatterns_recoverer_enabled")
428437
}
429438

439+
func (c *Config) GetIndexPatternsRefreshEnabled() bool {
440+
return parseBoolWithDefault(c.IndexPatternsRefreshEnabled, "indexpatterns_refresh_enabled")
441+
}
442+
430443
func (c *Config) GetShardingTargetSizeGiB() int {
431444
value := parseIntWithDefault(c.ShardingTargetSizeGiB, "sharding_target_size_gib")
432445
if value < 1 || value > 50 {
@@ -665,6 +678,7 @@ var CommandFlags = map[string][]FlagDefinition{
665678
{"indexpatterns-kibana-multitenancy", "bool", false, "Enable multitenancy mode", []string{}},
666679
{"indexpatterns-kibana-tenants-config", "string", "osctltenants.yaml", "Path to YAML tenants and patterns", []string{}},
667680
{"indexpatterns-recoverer-enabled", "bool", false, "Enable recoverer extracted_* pattern creation", []string{}},
681+
{"indexpatterns-refresh-enabled", "bool", false, "Enable refreshing for existing index patterns", []string{}},
668682
{"dry-run", "bool", false, "Show what index patterns would be created without creating", []string{}},
669683
},
670684
"snapshotsdelete": {

pkg/kibana/service.go

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"net/http"
99
"net/url"
10+
"osctl/pkg/utils"
1011
"strings"
1112
)
1213

@@ -21,6 +22,10 @@ type FindResponse struct {
2122
SavedObjects []SavedObject `json:"saved_objects"`
2223
}
2324

25+
type IPFields struct {
26+
Fields []map[string]any `json:"fields"`
27+
}
28+
2429
func (c *Client) FindSavedObjects(tenant string, objType string, perPage int) (*FindResponse, error) {
2530
params := url.Values{}
2631
params.Set("type", objType)
@@ -68,7 +73,10 @@ func (c *Client) CreateDataSource(tenant, title, endpoint, user, password string
6873
},
6974
},
7075
}
71-
b, _ := json.Marshal(body)
76+
b, err := json.Marshal(body)
77+
if err != nil {
78+
return fmt.Errorf("failed to marshal data source body: %w", err)
79+
}
7280
req, err := http.NewRequest("POST", u, bytes.NewReader(b))
7381
if err != nil {
7482
return err
@@ -88,3 +96,90 @@ func (c *Client) CreateDataSource(tenant, title, endpoint, user, password string
8896
}
8997
return nil
9098
}
99+
100+
func (c *Client) GetActualMappingForIndexPattern(title string) ([]byte, error) {
101+
if title == "" {
102+
return nil, fmt.Errorf("index pattern title cannot be empty")
103+
}
104+
105+
u := fmt.Sprintf("%s/api/index_patterns/_fields_for_wildcard?pattern=%s&meta_fields=_source&meta_fields=_id&meta_fields=_type&meta_fields=_index&meta_fields=_score", c.baseURL, title)
106+
req, err := http.NewRequest("GET", u, nil)
107+
if err != nil {
108+
return nil, err
109+
}
110+
req.Header.Set("Accept", "*/*")
111+
112+
resp, err := c.do(req)
113+
if err != nil {
114+
return nil, err
115+
}
116+
defer resp.Body.Close()
117+
if resp.StatusCode >= 300 {
118+
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
119+
return nil, fmt.Errorf("kibana get index mapping failed with: %s — %s", resp.Status, strings.TrimSpace(string(snippet)))
120+
}
121+
122+
body, err := io.ReadAll(resp.Body)
123+
if err != nil {
124+
return nil, err
125+
}
126+
var fields IPFields
127+
err = json.Unmarshal(body, &fields)
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
fields_string, err := json.Marshal(fields.Fields)
133+
if err != nil {
134+
return nil, err
135+
}
136+
return fields_string, nil
137+
}
138+
139+
func (c *Client) RefreshIndexPattern(id string, title string) error {
140+
if id == "" {
141+
return fmt.Errorf("index pattern id cannot be empty")
142+
}
143+
if title == "" {
144+
return fmt.Errorf("index pattern title cannot be empty")
145+
}
146+
fields, err := c.GetActualMappingForIndexPattern(title)
147+
if err != nil {
148+
return err
149+
}
150+
151+
if len(fields) == 0 {
152+
return fmt.Errorf("no fields found for index pattern %s (index may not exist)", title)
153+
}
154+
155+
u := fmt.Sprintf("%s/api/saved_objects/index-pattern/%s", c.baseURL, id)
156+
body := map[string]any{
157+
"attributes": map[string]any{
158+
"title": title,
159+
"version": utils.PatternVersion(),
160+
"timeFieldName": "@timestamp",
161+
"fields": string(fields),
162+
},
163+
}
164+
b, err := json.Marshal(body)
165+
if err != nil {
166+
return fmt.Errorf("failed to marshal index pattern body: %w", err)
167+
}
168+
169+
req, err := http.NewRequest("PUT", u, bytes.NewReader(b))
170+
if err != nil {
171+
return err
172+
}
173+
req.Header.Set("Content-Type", "application/json")
174+
resp, err := c.do(req)
175+
if err != nil {
176+
return err
177+
}
178+
defer resp.Body.Close()
179+
180+
if resp.StatusCode >= 300 {
181+
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
182+
return fmt.Errorf("kibana refresh index pattern failed: %s — %s", resp.Status, strings.TrimSpace(string(snippet)))
183+
}
184+
return nil
185+
}

pkg/opensearch/indices.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ func (c *Client) GetIndicesWithFields(pattern, fields string, sortBy ...string)
5252
if sortParam != "" {
5353
url += fmt.Sprintf("&s=%s", sortParam)
5454
}
55-
5655
req, err := http.NewRequest("GET", url, nil)
5756
if err != nil {
5857
return nil, fmt.Errorf("failed to create request: %v", err)

0 commit comments

Comments
 (0)