From 5c006b6d090d8791616e1f268cfc6c2c7896de83 Mon Sep 17 00:00:00 2001 From: Mike Rostermund Date: Thu, 30 May 2024 09:05:31 +0200 Subject: [PATCH 1/8] Remove deprecated/obsolete functionality (#159) --- api/cluster.go | 116 ------------------------------------------------- 1 file changed, 116 deletions(-) diff --git a/api/cluster.go b/api/cluster.go index 4abbd187..c5f4271b 100644 --- a/api/cluster.go +++ b/api/cluster.go @@ -40,11 +40,6 @@ type IngestPartition struct { NodeIds []int } -type StoragePartition struct { - Id int - NodeIds []int -} - type Cluster struct { Nodes []ClusterNode ClusterInfoAgeSeconds float64 @@ -57,7 +52,6 @@ type Cluster struct { TargetMissingSegmentSize float64 TargetProperlyReplicatedSegmentSize float64 IngestPartitions []IngestPartition - StoragePartitions []StoragePartition // Deprecated: returns dummy data as of LogScale 1.88 } func (c *Client) Clusters() *Clusters { return &Clusters{client: c} } @@ -71,92 +65,6 @@ func (c *Clusters) Get() (Cluster, error) { return query.Cluster, err } -type StoragePartitionInput struct { - ID graphql.Int `json:"id"` - NodeIDs []graphql.Int `json:"nodeIds"` -} - -type IngestPartitionInput struct { - ID graphql.Int `json:"id"` - NodeIDs []graphql.Int `json:"nodeIds"` -} - -// Deprecated: returns dummy data as of LogScale 1.88 -func (c *Clusters) UpdateStoragePartitionScheme(desired []StoragePartitionInput) error { - var mutation struct { - UpdateStoragePartitionScheme struct { - // We have to make a selection, so just take __typename - Typename graphql.String `graphql:"__typename"` - } `graphql:"updateStoragePartitionScheme(partitions: $partitions)"` - } - - variables := map[string]interface{}{ - "partitions": desired, - } - - return c.client.Mutate(&mutation, variables) -} - -// Deprecated: returns dummy data as of LogScale 1.80 -func (c *Clusters) UpdateIngestPartitionScheme(desired []IngestPartitionInput) error { - var mutation struct { - UpdateStoragePartitionScheme struct { - // We have to make a selection, so just take __typename - Typename graphql.String `graphql:"__typename"` - } `graphql:"updateIngestPartitionScheme(partitions: $partitions)"` - } - - variables := map[string]interface{}{ - "partitions": desired, - } - - return c.client.Mutate(&mutation, variables) -} - -// Deprecated: returns dummy data as of LogScale 1.88 -func (c *Clusters) StartDataRedistribution() error { - var mutation struct { - StartDataRedistribution struct { - // We have to make a selection, so just take __typename - Typename graphql.String `graphql:"__typename"` - } `graphql:"startDataRedistribution"` - } - - return c.client.Mutate(&mutation, nil) -} - -// Deprecated: returns dummy data as of LogScale 1.88 -func (c *Clusters) ClusterMoveStorageRouteAwayFromNode(nodeID int) error { - var mutation struct { - ClusterMoveStorageRouteAwayFromNode struct { - // We have to make a selection, so just take __typename - Typename graphql.String `graphql:"__typename"` - } `graphql:"clusterMoveStorageRouteAwayFromNode(nodeID: $id)"` - } - - variables := map[string]interface{}{ - "id": graphql.Int(nodeID), - } - - return c.client.Mutate(&mutation, variables) -} - -// Deprecated: returns dummy data as of LogScale 1.80 -func (c *Clusters) ClusterMoveIngestRoutesAwayFromNode(nodeID int) error { - var mutation struct { - ClusterMoveIngestRoutesAwayFromNode struct { - // We have to make a selection, so just take __typename - Typename graphql.String `graphql:"__typename"` - } `graphql:"clusterMoveIngestRoutesAwayFromNode(nodeID: $id)"` - } - - variables := map[string]interface{}{ - "id": graphql.Int(nodeID), - } - - return c.client.Mutate(&mutation, variables) -} - type ClusterNodes struct { client *Client } @@ -213,27 +121,3 @@ func (n *ClusterNodes) Unregister(nodeID int, force bool) error { return n.client.Mutate(&mutation, variables) } - -// Deprecated: returns dummy data as of LogScale 1.80 -func (c *Clusters) SuggestedIngestPartitions() ([]IngestPartitionInput, error) { - var query struct { - Cluster struct { - SuggestedIngestPartitions []IngestPartitionInput `graphql:"suggestedIngestPartitions"` - } `graphql:"cluster"` - } - - err := c.client.Query(&query, nil) - return query.Cluster.SuggestedIngestPartitions, err -} - -// Deprecated: returns dummy data as of LogScale 1.88 -func (c *Clusters) SuggestedStoragePartitions() ([]StoragePartitionInput, error) { - var query struct { - Cluster struct { - SuggestedStoragePartitions []StoragePartitionInput `graphql:"suggestedStoragePartitions"` - } `graphql:"cluster"` - } - - err := c.client.Query(&query, nil) - return query.Cluster.SuggestedStoragePartitions, err -} From 51093549637b52e568ec2c9d9016d252cf7b9d8c Mon Sep 17 00:00:00 2001 From: Mike Rostermund Date: Thu, 6 Jun 2024 13:28:54 +0200 Subject: [PATCH 2/8] Fixes for goreleaser v2 --- .github/workflows/ci.yaml | 5 +++-- .github/workflows/release.yml | 5 +++-- .goreleaser.yaml | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8212a915..f354605f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,9 +15,10 @@ jobs: with: go-version: '1.22' - name: Build snapshot with GoReleaser - uses: goreleaser/goreleaser-action@v4 + uses: goreleaser/goreleaser-action@v6 with: - version: latest + distribution: goreleaser + version: "~> v2" args: build --clean --snapshot - name: Run Gosec Security Scanner run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 714c7c5a..c3318c6a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,9 +28,10 @@ jobs: go-version: '1.22' - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v4 + uses: goreleaser/goreleaser-action@v6 with: - version: latest + distribution: goreleaser + version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 9d100616..6bf3debe 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,3 +1,4 @@ +version: 2 project_name: humioctl builds: @@ -30,7 +31,7 @@ brews: name: homebrew-humio token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" url_template: "https://github.com/humio/cli/releases/download/{{ .Tag }}/{{ .ArtifactName }}" - folder: Formula + directory: Formula homepage: https://humio.com/ description: Manage and Stream Logs to Humio test: | From 15157fb54ac2306294b03f9d875b74516b815b6f Mon Sep 17 00:00:00 2001 From: Mike Rostermund Date: Thu, 6 Jun 2024 13:33:11 +0200 Subject: [PATCH 3/8] Fix github workflow deprecations on Node16 -> Node20 --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/codeql-analysis.yml | 12 ++++++++---- .github/workflows/release.yml | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f354605f..f40a2e20 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: '1.22' - name: Build snapshot with GoReleaser diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 97f584b0..525ae527 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,15 +30,19 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 + - uses: actions/setup-go@v5 + with: + go-version: '1.22.2' + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -49,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -63,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3318c6a..95940789 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - @@ -23,7 +23,7 @@ jobs: echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: '1.22' - From 9b1db68658bee24eba1e3a1dedc68935e25b1f66 Mon Sep 17 00:00:00 2001 From: Mike Rostermund Date: Tue, 11 Jun 2024 09:46:43 +0200 Subject: [PATCH 4/8] Use parsers v2 API (#161) Fallback to deprecated functionality for versions prior to LogScale 1.129.0 --- api/internal/humiographql/filter-alerts.go | 4 +- api/internal/humiographql/parsers.go | 79 +++++ api/parsers.go | 332 +++++++++++++++++---- api/status.go | 18 ++ cmd/humioctl/parsers.go | 1 + cmd/humioctl/parsers_get.go | 37 +++ cmd/humioctl/parsers_install.go | 6 +- cmd/humioctl/parsers_remove.go | 2 +- go.mod | 1 + go.sum | 2 + 10 files changed, 425 insertions(+), 57 deletions(-) create mode 100644 api/internal/humiographql/parsers.go create mode 100644 cmd/humioctl/parsers_get.go diff --git a/api/internal/humiographql/filter-alerts.go b/api/internal/humiographql/filter-alerts.go index bd458c09..0407231a 100644 --- a/api/internal/humiographql/filter-alerts.go +++ b/api/internal/humiographql/filter-alerts.go @@ -1,6 +1,8 @@ package humiographql -import graphql "github.com/cli/shurcooL-graphql" +import ( + graphql "github.com/cli/shurcooL-graphql" +) type FilterAlert struct { ID graphql.String `graphql:"id"` diff --git a/api/internal/humiographql/parsers.go b/api/internal/humiographql/parsers.go new file mode 100644 index 00000000..109d8a32 --- /dev/null +++ b/api/internal/humiographql/parsers.go @@ -0,0 +1,79 @@ +package humiographql + +import ( + graphql "github.com/cli/shurcooL-graphql" +) + +type UpdateParserScriptInput struct { + Script graphql.String `json:"script"` +} + +type ParserTestEventInput struct { + RawString graphql.String `json:"rawString"` +} + +type FieldHasValueInput struct { + FieldName graphql.String `json:"fieldName"` + ExpectedValue graphql.String `json:"expectedValue"` +} + +type ParserTestCaseOutputAssertionsInput struct { + FieldsNotPresent []graphql.String `json:"fieldsNotPresent"` + FieldsHaveValues []FieldHasValueInput `json:"fieldsHaveValues"` +} + +type ParserTestCaseAssertionsForOutputInput struct { + OutputEventIndex graphql.Int `json:"outputEventIndex"` + Assertions ParserTestCaseOutputAssertionsInput `json:"assertions"` +} + +type ParserTestCaseInput struct { + Event ParserTestEventInput `json:"event"` + OutputAssertions []ParserTestCaseAssertionsForOutputInput `json:"outputAssertions"` +} + +type ParserTestEvent struct { + RawString graphql.String `graphql:"rawString"` +} + +type FieldHasValue struct { + FieldName graphql.String `graphql:"fieldName"` + ExpectedValue graphql.String `graphql:"expectedValue"` +} + +type ParserTestCaseOutputAssertions struct { + FieldsNotPresent []string `graphql:"fieldsNotPresent"` + FieldsHaveValues []FieldHasValue `graphql:"fieldsHaveValues"` +} + +type ParserTestCaseAssertionsForOutput struct { + OutputEventIndex graphql.Int `graphql:"outputEventIndex"` + Assertions ParserTestCaseOutputAssertions `graphql:"assertions"` +} + +type ParserTestCase struct { + Event ParserTestEvent `graphql:"event"` + OutputAssertions []ParserTestCaseAssertionsForOutput `graphql:"outputAssertions"` +} + +type Parser struct { + ID graphql.String `graphql:"id"` + Name graphql.String `graphql:"name"` + DisplayName graphql.String `graphql:"displayName"` + Description graphql.String `graphql:"description""` + IsBuiltIn graphql.Boolean `graphql:"isBuiltIn"` + Script graphql.String `graphql:"script"` + FieldsToTag []graphql.String `graphql:"fieldsToTag"` + FieldsToBeRemovedBeforeParsing []graphql.String `graphql:"fieldsToBeRemovedBeforeParsing"` + TestCases []ParserTestCase `graphql:"testCases"` +} + +type CreateParserInputV2 struct { + Name graphql.String `json:"name"` + Script graphql.String `json:"script""` + TestCases []ParserTestCaseInput `json:"testCases"` + RepositoryName RepoOrViewName `json:"repositoryName"` + FieldsToTag []graphql.String `json:"fieldsToTag"` + FieldsToBeRemovedBeforeParsing []graphql.String `json:"fieldsToBeRemovedBeforeParsing"` + AllowOverwritingExistingParser graphql.Boolean `json:"allowOverwritingExistingParser"` +} diff --git a/api/parsers.go b/api/parsers.go index cf82a29f..b74560e9 100644 --- a/api/parsers.go +++ b/api/parsers.go @@ -1,19 +1,35 @@ package api -import graphql "github.com/cli/shurcooL-graphql" +import ( + "fmt" + graphql "github.com/cli/shurcooL-graphql" + "github.com/humio/cli/api/internal/humiographql" +) + +const LogScaleVersionWithParserAPIv2 = "1.129.0" + +type ParserTestEvent struct { + RawString string `json:"rawString" yaml:"rawString"` +} + +type ParserTestCaseAssertions struct { + OutputEventIndex int `json:"outputEventIndex" yaml:"outputEventIndex"` + FieldsNotPresent []string `json:"fieldsNotPresent" yaml:"fieldsNotPresent"` + FieldsHaveValues map[string]string `json:"fieldsHaveValues" yaml:"fieldsHaveValues"` +} type ParserTestCase struct { - Input string - Output map[string]string + Event ParserTestEvent `json:"event" yaml:"event"` + Assertions []ParserTestCaseAssertions `json:"assertions" yaml:"assertions"` } type Parser struct { - ID string - Name string - Tests []string `yaml:",omitempty"` - Example string `yaml:",omitempty"` - Script string `yaml:",flow"` - TagFields []string `yaml:",omitempty"` + ID string + Name string + Script string `json:"script" yaml:",flow"` + TestCases []ParserTestCase `json:"testCases" yaml:"testCases"` + FieldsToTag []string `json:"tagFields" yaml:"tagFields"` + FieldsToBeRemovedBeforeParsing []string `json:"fieldsToBeRemovedBeforeParsing,omitempty" yaml:"fieldsToBeRemovedBeforeParsing"` } type Parsers struct { @@ -47,12 +63,42 @@ func (p *Parsers) List(repositoryName string) ([]ParserListItem, error) { return parsers, err } -func (p *Parsers) Remove(repositoryName string, parserName string) error { +func (p *Parsers) Delete(repositoryName string, parserName string) error { + status, err := p.client.Status() + if err != nil { + return err + } + + atLeast, err := status.AtLeast(LogScaleVersionWithParserAPIv2) + if err != nil { + return err + } + if !atLeast { + var mutation struct { + RemoveParser struct { + // We have to make a selection, so just take __typename + Typename graphql.String `graphql:"__typename"` + } `graphql:"removeParser(input: { id: $id, repositoryName: $repositoryName })"` + } + + parser, err := p.client.Parsers().Get(repositoryName, parserName) + if err != nil { + return err + } + + variables := map[string]interface{}{ + "repositoryName": graphql.String(repositoryName), + "id": graphql.String(parser.ID), + } + + return p.client.Mutate(&mutation, variables) + } + var mutation struct { - RemoveParser struct { + DeleteParser struct { // We have to make a selection, so just take __typename Typename graphql.String `graphql:"__typename"` - } `graphql:"removeParser(input: { id: $id, repositoryName: $repositoryName })"` + } `graphql:"deleteParser(input: { id: $id, repositoryName: $repositoryName })"` } parser, err := p.client.Parsers().Get(repositoryName, parserName) @@ -61,80 +107,217 @@ func (p *Parsers) Remove(repositoryName string, parserName string) error { } variables := map[string]interface{}{ - "repositoryName": graphql.String(repositoryName), + "repositoryName": humiographql.RepoOrViewName(repositoryName), "id": graphql.String(parser.ID), } return p.client.Mutate(&mutation, variables) } -func (p *Parsers) Add(repositoryName string, parser *Parser, force bool) error { +func (p *Parsers) Add(repositoryName string, newParser *Parser, allowOverwritingExistingParser bool) (*Parser, error) { + if newParser == nil { + return nil, fmt.Errorf("newFilterAlert must not be nil") + } - var mutation struct { - CreateParser struct { - // We have to make a selection, so just take __typename - Typename graphql.String `graphql:"__typename"` - } `graphql:"createParser(input: { name: $name, repositoryName: $repositoryName, testData: $testData, tagFields: $tagFields, sourceCode: $sourceCode, force: $force})"` + status, err := p.client.Status() + if err != nil { + return nil, err } - tagFieldsGQL := make([]graphql.String, len(parser.TagFields)) + atLeast, err := status.AtLeast(LogScaleVersionWithParserAPIv2) + if err != nil { + return nil, err + } + if !atLeast { + var mutation struct { + CreateParser struct { + // We have to make a selection, so just take __typename + Parser humiographql.Parser `graphql:"parser"` + } `graphql:"createParser(input: { name: $name, repositoryName: $repositoryName, testData: $testData, tagFields: $tagFields, sourceCode: $sourceCode, force: $force})"` + } + + testDataGQL := make([]graphql.String, len(newParser.TestCases)) + for i := range newParser.TestCases { + testDataGQL[i] = graphql.String(newParser.TestCases[i].Event.RawString) + } + tagFieldsGQL := make([]graphql.String, len(newParser.FieldsToTag)) + for i := range newParser.FieldsToTag { + tagFieldsGQL[i] = graphql.String(newParser.FieldsToTag[i]) + } + + variables := map[string]interface{}{ + "name": graphql.String(newParser.Name), + "sourceCode": graphql.String(newParser.Script), + "repositoryName": graphql.String(repositoryName), + "testData": testDataGQL, + "tagFields": tagFieldsGQL, + "force": graphql.Boolean(allowOverwritingExistingParser), + } + + err = p.client.Mutate(&mutation, variables) + if err != nil { + return nil, err + } + + parser := mapHumioGraphqlParserToParser(mutation.CreateParser.Parser) + + return &parser, nil + } - for i, field := range parser.TagFields { - tagFieldsGQL[i] = graphql.String(field) + var mutation struct { + humiographql.Parser `graphql:"createParserV2(input: $input)"` } - testsGQL := make([]graphql.String, len(parser.Tests)) + fieldsToTagGQL := make([]graphql.String, len(newParser.FieldsToTag)) + for i, field := range newParser.FieldsToTag { + fieldsToTagGQL[i] = graphql.String(field) + } + fieldsToBeRemovedBeforeParsingGQL := make([]graphql.String, len(newParser.FieldsToBeRemovedBeforeParsing)) + for i, field := range newParser.FieldsToBeRemovedBeforeParsing { + fieldsToBeRemovedBeforeParsingGQL[i] = graphql.String(field) + } + testCasesGQL := make([]humiographql.ParserTestCaseInput, len(newParser.TestCases)) + for i := range newParser.TestCases { + testCasesGQL[i] = mapParserTestCaseToInput(newParser.TestCases[i]) + } - for i, field := range parser.Tests { - testsGQL[i] = graphql.String(field) + createParser := humiographql.CreateParserInputV2{ + Name: graphql.String(newParser.Name), + Script: graphql.String(newParser.Script), + TestCases: testCasesGQL, + RepositoryName: humiographql.RepoOrViewName(repositoryName), + FieldsToTag: fieldsToTagGQL, + FieldsToBeRemovedBeforeParsing: fieldsToBeRemovedBeforeParsingGQL, + AllowOverwritingExistingParser: graphql.Boolean(allowOverwritingExistingParser), } variables := map[string]interface{}{ - "name": graphql.String(parser.Name), - "sourceCode": graphql.String(parser.Script), - "repositoryName": graphql.String(repositoryName), - "testData": testsGQL, - "tagFields": tagFieldsGQL, - "force": graphql.Boolean(force), + "input": createParser, } - return p.client.Mutate(&mutation, variables) + err = p.client.Mutate(&mutation, variables) + if err != nil { + return nil, err + } + + parser := mapHumioGraphqlParserToParser(mutation.Parser) + + return &parser, nil +} + +func mapParserTestCaseToInput(p ParserTestCase) humiographql.ParserTestCaseInput { + parserTestCaseAssertionsForOutputInput := make([]humiographql.ParserTestCaseAssertionsForOutputInput, len(p.Assertions)) + for i := range p.Assertions { + fieldsNotPresent := make([]graphql.String, len(p.Assertions[i].FieldsNotPresent)) + for i := range p.Assertions[i].FieldsNotPresent { + fieldsNotPresent[i] = graphql.String(p.Assertions[i].FieldsNotPresent[i]) + } + fieldsHaveValuesInput := make([]humiographql.FieldHasValueInput, len(p.Assertions[i].FieldsHaveValues)) + for field, value := range p.Assertions[i].FieldsHaveValues { + fieldsHaveValuesInput[i] = humiographql.FieldHasValueInput{ + FieldName: graphql.String(field), + ExpectedValue: graphql.String(value), + } + } + parserTestCaseAssertionsForOutputInput[i] = humiographql.ParserTestCaseAssertionsForOutputInput{ + OutputEventIndex: graphql.Int(p.Assertions[i].OutputEventIndex), + Assertions: humiographql.ParserTestCaseOutputAssertionsInput{ + FieldsNotPresent: fieldsNotPresent, + FieldsHaveValues: fieldsHaveValuesInput, + }, + } + } + return humiographql.ParserTestCaseInput{ + Event: humiographql.ParserTestEventInput{RawString: graphql.String(p.Event.RawString)}, + OutputAssertions: parserTestCaseAssertionsForOutputInput, + } } func (p *Parsers) Get(repositoryName string, parserName string) (*Parser, error) { + status, err := p.client.Status() + if err != nil { + return nil, err + } + + atLeast, err := status.AtLeast(LogScaleVersionWithParserAPIv2) + if err != nil { + return nil, err + } + if !atLeast { + var query struct { + Repository struct { + Parser *struct { + ID string + Name string + SourceCode string + TestData []string + TagFields []string + } `graphql:"parser(name: $parserName)"` + } `graphql:"repository(name: $repositoryName)"` + } + + variables := map[string]interface{}{ + "parserName": graphql.String(parserName), + "repositoryName": graphql.String(repositoryName), + } + + err := p.client.Query(&query, variables) + if err != nil { + return nil, err + } + + if query.Repository.Parser == nil { + return nil, ParserNotFound(parserName) + } + + parser := Parser{ + ID: query.Repository.Parser.ID, + Name: query.Repository.Parser.Name, + Script: query.Repository.Parser.SourceCode, + FieldsToTag: query.Repository.Parser.TagFields, + } + parser.TestCases = make([]ParserTestCase, len(query.Repository.Parser.TestData)) + for i := range query.Repository.Parser.TestData { + parser.TestCases[i] = ParserTestCase{ + Event: ParserTestEvent{RawString: query.Repository.Parser.TestData[i]}, + } + } + + return &parser, nil + } + + parserList, err := p.List(repositoryName) + if err != nil { + return nil, err + } + parserID := "" + for i := range parserList { + if parserList[i].Name == parserName { + parserID = parserList[i].ID + break + } + } + if parserID == "" { + return nil, ParserNotFound(parserName) + } + var query struct { Repository struct { - Parser *struct { - ID string - Name string - SourceCode string - TestData []string - TagFields []string - } `graphql:"parser(name: $parserName)"` + Parser *humiographql.Parser `graphql:"parser(id: $parserID)"` } `graphql:"repository(name: $repositoryName)"` } variables := map[string]interface{}{ - "parserName": graphql.String(parserName), + "parserID": graphql.String(parserID), "repositoryName": graphql.String(repositoryName), } - err := p.client.Query(&query, variables) + err = p.client.Query(&query, variables) if err != nil { return nil, err } - if query.Repository.Parser == nil { - return nil, ParserNotFound(parserName) - } - - parser := Parser{ - ID: query.Repository.Parser.ID, - Name: query.Repository.Parser.Name, - Tests: query.Repository.Parser.TestData, - Script: query.Repository.Parser.SourceCode, - TagFields: query.Repository.Parser.TagFields, - } + parser := mapHumioGraphqlParserToParser(*query.Repository.Parser) return &parser, nil } @@ -167,3 +350,48 @@ func (p *Parsers) Export(repositoryName string, parserName string) (string, erro return query.Repository.Parser.YamlTemplate, nil } + +func mapHumioGraphqlParserToParser(input humiographql.Parser) Parser { + var fieldsToTag = make([]string, len(input.FieldsToTag)) + for i := range input.FieldsToTag { + fieldsToTag[i] = string(input.FieldsToTag[i]) + } + + var fieldsToBeRemovedBeforeParsing = make([]string, len(input.FieldsToBeRemovedBeforeParsing)) + for i := range input.FieldsToBeRemovedBeforeParsing { + fieldsToBeRemovedBeforeParsing[i] = string(input.FieldsToBeRemovedBeforeParsing[i]) + } + + var testCases = make([]ParserTestCase, len(input.TestCases)) + for i := range input.TestCases { + var assertions = make([]ParserTestCaseAssertions, len(input.TestCases[i].OutputAssertions)) + for j := range input.TestCases[i].OutputAssertions { + var fieldsHaveValues = make(map[string]string, len(input.TestCases[i].OutputAssertions[j].Assertions.FieldsHaveValues)) + for k := range input.TestCases[i].OutputAssertions[j].Assertions.FieldsHaveValues { + fieldName := string(input.TestCases[i].OutputAssertions[j].Assertions.FieldsHaveValues[k].FieldName) + expectedValue := string(input.TestCases[i].OutputAssertions[j].Assertions.FieldsHaveValues[k].ExpectedValue) + fieldsHaveValues[fieldName] = expectedValue + } + + assertions[j] = ParserTestCaseAssertions{ + OutputEventIndex: int(input.TestCases[i].OutputAssertions[j].OutputEventIndex), + FieldsNotPresent: input.TestCases[i].OutputAssertions[j].Assertions.FieldsNotPresent, + FieldsHaveValues: fieldsHaveValues, + } + } + + testCases[i] = ParserTestCase{ + Event: ParserTestEvent{RawString: string(input.TestCases[i].Event.RawString)}, + Assertions: assertions, + } + } + + return Parser{ + ID: string(input.ID), + Name: string(input.Name), + Script: string(input.Script), + TestCases: testCases, + FieldsToTag: fieldsToTag, + FieldsToBeRemovedBeforeParsing: fieldsToBeRemovedBeforeParsing, + } +} diff --git a/api/status.go b/api/status.go index 80cf0aa3..3b1eb975 100644 --- a/api/status.go +++ b/api/status.go @@ -5,6 +5,9 @@ import ( "fmt" "io" "net/http" + "strings" + + "github.com/Masterminds/semver/v3" ) type StatusResponse struct { @@ -16,6 +19,21 @@ func (s StatusResponse) IsDown() bool { return s.Status != "OK" && s.Status != "WARN" } +func (s StatusResponse) AtLeast(ver string) (bool, error) { + assumeLatest := true + version := strings.Split(s.Version, "-") + constraint, err := semver.NewConstraint(fmt.Sprintf(">= %s", ver)) + if err != nil { + return assumeLatest, fmt.Errorf("could not parse constraint of `%s`: %w", fmt.Sprintf(">= %s", ver), err) + } + semverVersion, err := semver.NewVersion(version[0]) + if err != nil { + return assumeLatest, fmt.Errorf("could not parse version of `%s`: %w", version[0], err) + } + + return constraint.Check(semverVersion), nil +} + func (c *Client) Status() (*StatusResponse, error) { resp, err := c.HTTPRequest(http.MethodGet, "api/v1/status", nil) diff --git a/cmd/humioctl/parsers.go b/cmd/humioctl/parsers.go index 21eb1c25..827386dc 100644 --- a/cmd/humioctl/parsers.go +++ b/cmd/humioctl/parsers.go @@ -28,6 +28,7 @@ func newParsersCmd() *cobra.Command { cmd.AddCommand(newParsersListCmd()) cmd.AddCommand(newParsersRemoveCmd()) cmd.AddCommand(newParsersExportCmd()) + cmd.AddCommand(newParsersShowCmd()) return cmd } diff --git a/cmd/humioctl/parsers_get.go b/cmd/humioctl/parsers_get.go new file mode 100644 index 00000000..e79a871b --- /dev/null +++ b/cmd/humioctl/parsers_get.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "github.com/humio/cli/cmd/internal/format" + "github.com/spf13/cobra" + "strings" +) + +func newParsersShowCmd() *cobra.Command { + cmd := cobra.Command{ + Use: "show ", + Short: "Show details for a parser in a repository.", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + repoName := args[0] + parserName := args[1] + client := NewApiClient(cmd) + + parser, err := client.Parsers().Get(repoName, parserName) + exitOnError(cmd, err, "Error fetching parser") + + details := [][]format.Value{ + {format.String("ID"), format.String(parser.ID)}, + {format.String("Name"), format.String(parser.Name)}, + {format.String("Script"), format.String(parser.Script)}, + {format.String("TagFields"), format.String(strings.Join(parser.FieldsToTag, "\n"))}, + {format.String("FieldsToBeRemovedBeforeParsing"), format.String(strings.Join(parser.FieldsToBeRemovedBeforeParsing, "\n"))}, + {format.String("TestCases"), format.String(fmt.Sprintf("%+v", parser.TestCases))}, + } + + printDetailsTable(cmd, details) + }, + } + + return &cmd +} diff --git a/cmd/humioctl/parsers_install.go b/cmd/humioctl/parsers_install.go index 654b08b8..bb727bc9 100644 --- a/cmd/humioctl/parsers_install.go +++ b/cmd/humioctl/parsers_install.go @@ -25,7 +25,7 @@ import ( ) func newParsersInstallCmd() *cobra.Command { - var force bool + var allowOverwritingExistingParser bool var filePath, url, name string cmd := cobra.Command{ @@ -70,12 +70,12 @@ Use the --force flag to update existing parsers with conflicting names. parser.Name = name } - err = client.Parsers().Add(repositoryName, &parser, force) + _, err = client.Parsers().Add(repositoryName, &parser, allowOverwritingExistingParser) exitOnError(cmd, err, "Error installing parser") }, } - cmd.Flags().BoolVarP(&force, "force", "f", false, "Overrides any parser with the same name. This can be used for updating parser that are already installed. (See --name)") + cmd.Flags().BoolVar(&allowOverwritingExistingParser, "allow-overwriting-existing-parser", false, "Overrides any parser with the same name. This can be used for updating parser that are already installed. (See --name)") cmd.Flags().StringVar(&filePath, "file", "", "The local file path to the parser to install.") cmd.Flags().StringVar(&url, "url", "", "A URL to fetch the parser file from.") cmd.Flags().StringVarP(&name, "name", "n", "", "Install the parser under a specific name, ignoring the `name` attribute in the parser file.") diff --git a/cmd/humioctl/parsers_remove.go b/cmd/humioctl/parsers_remove.go index 4f137e73..66122ea1 100644 --- a/cmd/humioctl/parsers_remove.go +++ b/cmd/humioctl/parsers_remove.go @@ -30,7 +30,7 @@ func newParsersRemoveCmd() *cobra.Command { parser := args[1] client := NewApiClient(cmd) - err := client.Parsers().Remove(repo, parser) + err := client.Parsers().Delete(repo, parser) exitOnError(cmd, err, "Error removing parser") fmt.Fprintf(cmd.OutOrStdout(), "Successfully removed parser %q from repository %q\n", parser, repo) diff --git a/go.mod b/go.mod index 09676075..73edf16c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/humio/cli go 1.22 require ( + github.com/Masterminds/semver/v3 v3.2.1 github.com/cli/shurcooL-graphql v0.0.4 github.com/gofrs/uuid v3.2.0+incompatible github.com/hpcloud/tail v1.0.0 diff --git a/go.sum b/go.sum index a72ade7f..edd52469 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= From 284f2bef333aa8c7c469fdeff39358a09f80118b Mon Sep 17 00:00:00 2001 From: Brad Sherwood Date: Tue, 25 Jun 2024 18:10:30 +0930 Subject: [PATCH 5/8] Add enable/disable automatic search option on repos and view (#163) --- api/repositories.go | 17 +++++++++++++++ api/views.go | 42 ++++++++++++++++++++++++++---------- cmd/humioctl/repos.go | 1 + cmd/humioctl/repos_update.go | 17 +++++++++++++-- cmd/humioctl/views_list.go | 8 ++++--- cmd/humioctl/views_update.go | 16 +++++++++++++- 6 files changed, 84 insertions(+), 17 deletions(-) diff --git a/api/repositories.go b/api/repositories.go index 4583fe4c..a37b70d6 100644 --- a/api/repositories.go +++ b/api/repositories.go @@ -21,6 +21,7 @@ type Repository struct { StorageRetentionSizeGB float64 `graphql:"storageSizeBasedRetention"` SpaceUsed int64 `graphql:"compressedByteSize"` S3ArchivingConfiguration humiographql.S3Configuration `graphql:"s3ArchivingConfiguration"` + AutomaticSearch bool `graphql:"automaticSearch"` } func (c *Client) Repositories() *Repositories { return &Repositories{client: c} } @@ -329,3 +330,19 @@ func (r *Repositories) UpdateS3ArchivingConfiguration(name string, bucket string return r.client.Mutate(&mutation, variables) } + +func (r *Repositories) UpdateAutomaticSearch(name string, automaticSearch bool) error { + var mutation struct { + SetAutomaticSearching struct { + // We have to make a selection, so just take __typename + Typename graphql.String `graphql:"__typename"` + } `graphql:"setAutomaticSearching(name: $name, automaticSearch: $automaticSearch)"` + } + + variables := map[string]interface{}{ + "name": graphql.String(name), + "automaticSearch": graphql.Boolean(automaticSearch), + } + + return r.client.Mutate(&mutation, variables) +} diff --git a/api/views.go b/api/views.go index 966be271..7f058b9b 100644 --- a/api/views.go +++ b/api/views.go @@ -17,9 +17,10 @@ type ViewConnection struct { } type ViewQueryData struct { - Name string - Description string - ViewInfo struct { + Name string + Description string + AutomaticSearch bool + ViewInfo struct { Connections []struct { Repository struct{ Name string } Filter string @@ -28,9 +29,10 @@ type ViewQueryData struct { } type View struct { - Name string - Description string - Connections []ViewConnection + Name string + Description string + Connections []ViewConnection + AutomaticSearch bool } func (c *Client) Views() *Views { return &Views{client: c} } @@ -58,17 +60,19 @@ func (c *Views) Get(name string) (*View, error) { } view := View{ - Name: query.Result.Name, - Description: query.Result.Description, - Connections: connections, + Name: query.Result.Name, + Description: query.Result.Description, + Connections: connections, + AutomaticSearch: query.Result.AutomaticSearch, } return &view, nil } type ViewListItem struct { - Name string - Typename string `graphql:"__typename"` + Name string + Typename string `graphql:"__typename"` + AutomaticSearch bool } func (c *Views) List() ([]ViewListItem, error) { @@ -152,3 +156,19 @@ func (c *Views) UpdateDescription(name string, description string) error { return c.client.Mutate(&mutation, variables) } + +func (c *Views) UpdateAutomaticSearch(name string, automaticSearch bool) error { + var mutation struct { + SetAutomaticSearching struct { + // We have to make a selection, so just take __typename + Typename graphql.String `graphql:"__typename"` + } `graphql:"setAutomaticSearching(name: $name, automaticSearch: $automaticSearch)"` + } + + variables := map[string]interface{}{ + "name": graphql.String(name), + "automaticSearch": graphql.Boolean(automaticSearch), + } + + return c.client.Mutate(&mutation, variables) +} diff --git a/cmd/humioctl/repos.go b/cmd/humioctl/repos.go index 314c7bff..9527e5cd 100644 --- a/cmd/humioctl/repos.go +++ b/cmd/humioctl/repos.go @@ -49,6 +49,7 @@ func printRepoDetailsTable(cmd *cobra.Command, repo api.Repository) { {format.String("S3 Archiving Bucket"), format.String(repo.S3ArchivingConfiguration.Bucket)}, {format.String("S3 Archiving Region"), format.String(repo.S3ArchivingConfiguration.Region)}, {format.String("S3 Archiving Format"), format.String(repo.S3ArchivingConfiguration.Format)}, + {format.String("Automatic Search"), format.Bool(repo.AutomaticSearch)}, } printDetailsTable(cmd, details) diff --git a/cmd/humioctl/repos_update.go b/cmd/humioctl/repos_update.go index b6b8d787..604db016 100644 --- a/cmd/humioctl/repos_update.go +++ b/cmd/humioctl/repos_update.go @@ -21,7 +21,7 @@ import ( ) func newReposUpdateCmd() *cobra.Command { - var allowDataDeletionFlag, enableS3ArchivingFlag, disableS3ArchivingFlag bool + var allowDataDeletionFlag, enableS3ArchivingFlag, disableS3ArchivingFlag, enableAutomaticSearchFlag, disableAutomaticSearchFlag bool var descriptionFlag, s3ArchivingBucketFlag, s3ArchivingRegionFlag, s3ArchivingFormatFlag stringPtrFlag var retentionTimeFlag, ingestSizeBasedRetentionFlag, storageSizeBasedRetentionFlag float64PtrFlag @@ -34,7 +34,8 @@ func newReposUpdateCmd() *cobra.Command { client := NewApiClient(cmd) if descriptionFlag.value == nil && retentionTimeFlag.value == nil && ingestSizeBasedRetentionFlag.value == nil && storageSizeBasedRetentionFlag.value == nil && - !enableS3ArchivingFlag && !disableS3ArchivingFlag && s3ArchivingBucketFlag.value == nil && s3ArchivingRegionFlag.value == nil && s3ArchivingFormatFlag.value == nil { + !enableS3ArchivingFlag && !disableS3ArchivingFlag && s3ArchivingBucketFlag.value == nil && s3ArchivingRegionFlag.value == nil && s3ArchivingFormatFlag.value == nil && + !enableAutomaticSearchFlag && !disableAutomaticSearchFlag { exitOnError(cmd, fmt.Errorf("you must specify at least one flag to update"), "Nothing specified to update") } if descriptionFlag.value != nil { @@ -69,6 +70,15 @@ func newReposUpdateCmd() *cobra.Command { exitOnError(cmd, err, "Error enabling S3 archiving") } + if disableAutomaticSearchFlag { + err := client.Repositories().UpdateAutomaticSearch(repoName, false) + exitOnError(cmd, err, "Error disabling automatic search") + } + if enableAutomaticSearchFlag { + err := client.Repositories().UpdateAutomaticSearch(repoName, true) + exitOnError(cmd, err, "Error enabling automatic search") + } + fmt.Fprintf(cmd.OutOrStdout(), "Successfully updated repository %q\n", repoName) }, } @@ -85,6 +95,9 @@ func newReposUpdateCmd() *cobra.Command { cmd.Flags().Var(&s3ArchivingFormatFlag, "s3-archiving-format", "The S3 archiving format to be used for S3 Archiving. Formats: RAW, NDJSON") cmd.MarkFlagsRequiredTogether("s3-archiving-bucket", "s3-archiving-region", "s3-archiving-format") cmd.MarkFlagsMutuallyExclusive("enable-s3-archiving", "disable-s3-archiving") + cmd.Flags().BoolVar(&enableAutomaticSearchFlag, "enable-automatic-search", false, "Enable automatic search for the repository.") + cmd.Flags().BoolVar(&disableAutomaticSearchFlag, "disable-automatic-search", false, "Disable automatic search for the repository.") + cmd.MarkFlagsMutuallyExclusive("enable-automatic-search", "disable-automatic-search") return &cmd } diff --git a/cmd/humioctl/views_list.go b/cmd/humioctl/views_list.go index 264081dc..aaf3fca3 100644 --- a/cmd/humioctl/views_list.go +++ b/cmd/humioctl/views_list.go @@ -37,14 +37,16 @@ func newViewsListCmd() *cobra.Command { for i, view := range views { if viewOnly { if view.Typename == viewTypeName { - rows[i] = []format.Value{format.String(view.Name)} + rows[i] = []format.Value{format.String(view.Name), + format.Bool(view.AutomaticSearch)} } } else { - rows[i] = []format.Value{format.String(view.Name)} + rows[i] = []format.Value{format.String(view.Name), + format.Bool(view.AutomaticSearch)} } } - printOverviewTable(cmd, []string{"Name"}, rows) + printOverviewTable(cmd, []string{"Name", "Automatic Search"}, rows) }, } diff --git a/cmd/humioctl/views_update.go b/cmd/humioctl/views_update.go index fc2fbac7..7554c695 100644 --- a/cmd/humioctl/views_update.go +++ b/cmd/humioctl/views_update.go @@ -25,6 +25,8 @@ import ( ) func newViewsUpdateCmd() *cobra.Command { + var enableAutomaticSearchFlag, disableAutomaticSearchFlag bool + connsFlag := []string{} connections := []api.ViewConnectionInput{} description := "" @@ -47,7 +49,7 @@ namely "repo1" and "repo2": viewName := args[0] client := NewApiClient(cmd) - if len(connsFlag) == 0 && description == "" { + if len(connsFlag) == 0 && description == "" && !enableAutomaticSearchFlag && !disableAutomaticSearchFlag { exitOnError(cmd, fmt.Errorf("you must specify at least one flag"), "Nothing specified to update") } @@ -77,12 +79,24 @@ namely "repo1" and "repo2": exitOnError(cmd, err, "Error updating view description") } + if disableAutomaticSearchFlag { + err := client.Views().UpdateAutomaticSearch(viewName, false) + exitOnError(cmd, err, "Error disabling automatic search") + } + if enableAutomaticSearchFlag { + err := client.Views().UpdateAutomaticSearch(viewName, true) + exitOnError(cmd, err, "Error enabling automatic search") + } + fmt.Fprintf(cmd.OutOrStdout(), "Successfully updated view %q\n", viewName) }, } cmd.Flags().StringArrayVar(&connsFlag, "connection", connsFlag, "Sets a repository connection with the chosen filter in format: =") cmd.Flags().StringVar(&description, "description", description, "Sets the view description.") + cmd.Flags().BoolVar(&enableAutomaticSearchFlag, "enable-automatic-search", false, "Enable automatic search for the view.") + cmd.Flags().BoolVar(&disableAutomaticSearchFlag, "disable-automatic-search", false, "Disable automatic search for the view.") + cmd.MarkFlagsMutuallyExclusive("enable-automatic-search", "disable-automatic-search") return &cmd } From 517aa797c42d3df6eefff027e2bab4d91ea40353 Mon Sep 17 00:00:00 2001 From: fjerlov-cs <158591046+fjerlov-cs@users.noreply.github.com> Date: Fri, 28 Jun 2024 10:48:53 +0200 Subject: [PATCH 6/8] Scheduled searches (#165) --- api/error.go | 16 +- api/internal/humiographql/alerts.go | 4 +- api/internal/humiographql/scheduled-search.go | 56 ++++ api/internal/humiographql/user.go | 7 + api/scheduled-search.go | 242 ++++++++++++++++++ cmd/humioctl/root.go | 1 + cmd/humioctl/scheduled_searches.go | 34 +++ cmd/humioctl/scheduled_searches_export.go | 67 +++++ cmd/humioctl/scheduled_searches_install.go | 79 ++++++ cmd/humioctl/scheduled_searches_list.go | 60 +++++ cmd/humioctl/scheduled_searches_remove.go | 57 +++++ cmd/humioctl/scheduled_searches_show.go | 70 +++++ 12 files changed, 686 insertions(+), 7 deletions(-) create mode 100644 api/internal/humiographql/scheduled-search.go create mode 100644 api/internal/humiographql/user.go create mode 100644 api/scheduled-search.go create mode 100644 cmd/humioctl/scheduled_searches.go create mode 100644 cmd/humioctl/scheduled_searches_export.go create mode 100644 cmd/humioctl/scheduled_searches_install.go create mode 100644 cmd/humioctl/scheduled_searches_list.go create mode 100644 cmd/humioctl/scheduled_searches_remove.go create mode 100644 cmd/humioctl/scheduled_searches_show.go diff --git a/api/error.go b/api/error.go index 75dab0e1..258d35d0 100644 --- a/api/error.go +++ b/api/error.go @@ -7,10 +7,11 @@ import ( type EntityType string const ( - EntityTypeParser EntityType = "parser" - EntityTypeAction EntityType = "action" - EntityTypeAlert EntityType = "alert" - EntityTypeFilterAlert EntityType = "filter-alert" + EntityTypeParser EntityType = "parser" + EntityTypeAction EntityType = "action" + EntityTypeAlert EntityType = "alert" + EntityTypeFilterAlert EntityType = "filter-alert" + EntityTypeScheduledSearch EntityType = "scheduled-search" ) func (e EntityType) String() string { @@ -61,3 +62,10 @@ func FilterAlertNotFound(name string) error { key: name, } } + +func ScheduledSearchNotFound(name string) error { + return EntityNotFound{ + entityType: EntityTypeScheduledSearch, + key: name, + } +} \ No newline at end of file diff --git a/api/internal/humiographql/alerts.go b/api/internal/humiographql/alerts.go index 52eb6da5..8a4ed73b 100644 --- a/api/internal/humiographql/alerts.go +++ b/api/internal/humiographql/alerts.go @@ -17,9 +17,7 @@ type Alert struct { Labels []graphql.String `graphql:"labels"` LastError graphql.String `graphql:"lastError"` QueryOwnership QueryOwnership `graphql:"queryOwnership"` - RunAsUser struct { - ID graphql.String `graphql:"id"` - } `graphql:"runAsUser"` + RunAsUser User `graphql:"runAsUser"` } type CreateAlert struct { diff --git a/api/internal/humiographql/scheduled-search.go b/api/internal/humiographql/scheduled-search.go new file mode 100644 index 00000000..5577f77e --- /dev/null +++ b/api/internal/humiographql/scheduled-search.go @@ -0,0 +1,56 @@ +package humiographql + +import graphql "github.com/cli/shurcooL-graphql" + +type ScheduledSearch struct { + ID graphql.String `json:"id"` + Name graphql.String `json:"name"` + Description graphql.String `json:"description,omitempty"` + QueryString graphql.String `json:"queryString"` + Start graphql.String `json:"start"` + End graphql.String `json:"end"` + TimeZone graphql.String `json:"timezone"` + Schedule graphql.String `json:"schedule"` + BackfillLimit graphql.Int `json:"backfillLimit"` + Enabled graphql.Boolean `json:"enabled"` + ActionsV2 []Action `json:"actionsV2"` + RunAsUser User `json:"runAsUser,omitempty"` + TimeOfNextPlannedExecution Long `json:"timeOfNextPlannedExecution"` + Labels []graphql.String `json:"labels"` + QueryOwnership QueryOwnership `json:"queryOwnership"` +} + +type CreateScheduledSearch struct { + ViewName graphql.String `json:"viewName"` + Name graphql.String `json:"name"` + Description graphql.String `json:"description,omitempty"` + QueryString graphql.String `json:"queryString"` + QueryStart graphql.String `json:"queryStart"` + QueryEnd graphql.String `json:"queryEnd"` + Schedule graphql.String `json:"schedule"` + TimeZone graphql.String `json:"timeZone"` + BackfillLimit graphql.Int `json:"backfillLimit"` + Enabled graphql.Boolean `json:"enabled"` + ActionsIdsOrNames []graphql.String `json:"actions"` + Labels []graphql.String `json:"labels,omitempty"` + RunAsUserID graphql.String `json:"runAsUserId,omitempty"` + QueryOwnership QueryOwnershipType `json:"queryOwnershipType,omitempty"` +} + +type UpdateScheduledSearch struct { + ViewName graphql.String `json:"viewName"` + ID graphql.String `json:"id"` + Name graphql.String `json:"name"` + Description graphql.String `json:"description,omitempty"` + QueryString graphql.String `json:"queryString"` + QueryStart graphql.String `json:"queryStart"` + QueryEnd graphql.String `json:"queryEnd"` + Schedule graphql.String `json:"schedule"` + TimeZone graphql.String `json:"timeZone"` + BackfillLimit graphql.Int `json:"backfillLimit"` + Enabled graphql.Boolean `json:"enabled"` + ActionsIdsOrNames []graphql.String `json:"actions"` + Labels []graphql.String `json:"labels"` + RunAsUserID graphql.String `json:"runAsUserId,omitempty"` + QueryOwnership QueryOwnershipType `json:"queryOwnershipType"` +} diff --git a/api/internal/humiographql/user.go b/api/internal/humiographql/user.go new file mode 100644 index 00000000..3bbc0c8f --- /dev/null +++ b/api/internal/humiographql/user.go @@ -0,0 +1,7 @@ +package humiographql + +import graphql "github.com/cli/shurcooL-graphql" + +type User struct { + ID graphql.String `graphql:"id"` +} diff --git a/api/scheduled-search.go b/api/scheduled-search.go new file mode 100644 index 00000000..cba1ade1 --- /dev/null +++ b/api/scheduled-search.go @@ -0,0 +1,242 @@ +package api + +import ( + "fmt" + graphql "github.com/cli/shurcooL-graphql" + "github.com/humio/cli/api/internal/humiographql" +) + +type ScheduledSearch struct { + ID string `graphql:"id" yaml:"-" json:"id"` + Name string `graphql:"name" yaml:"name" json:"name"` + Description string `graphql:"description" yaml:"description,omitempty" json:"description,omitempty"` + QueryString string `graphql:"queryString" yaml:"queryString" json:"queryString"` + QueryStart string `graphql:"queryStart" yaml:"queryStart" json:"queryStart"` + QueryEnd string `graphql:"queryEnd" yaml:"queryEnd" json:"queryEnd"` + TimeZone string `graphql:"timeZone" yaml:"timeZone" json:"timeZone"` + Schedule string `graphql:"schedule" yaml:"schedule" json:"schedule"` + BackfillLimit int `graphql:"backfillLimit" yaml:"backfillLimit" json:"backfillLimit"` + Enabled bool `graphql:"enabled" yaml:"enabled" json:"enabled"` + ActionNames []string `graphql:"actionNames" yaml:"actionNames" json:"actionNames"` + RunAsUserID string `graphql:"runAsUserId" yaml:"runAsUserId,omitempty" json:"runAsUserId,omitempty"` + Labels []string `graphql:"labels" yaml:"labels" json:"labels"` + QueryOwnershipType string `graphql:"queryOwnership" yaml:"queryOwnershipType" json:"queryOwnershipType"` +} + +type ScheduledSearches struct { + client *Client +} + +func (c *Client) ScheduledSearches() *ScheduledSearches { return &ScheduledSearches{client: c} } + +func (a *ScheduledSearches) List(viewName string) ([]ScheduledSearch, error) { + var query struct { + SearchDomain struct { + ScheduledSearches []humiographql.ScheduledSearch `graphql:"scheduledSearches"` + } `graphql:"searchDomain(name: $viewName)"` + } + + variables := map[string]any{ + "viewName": graphql.String(viewName), + } + + err := a.client.Query(&query, variables) + if err != nil { + return nil, err + } + + var scheduledSearches = make([]ScheduledSearch, len(query.SearchDomain.ScheduledSearches)) + for i := range query.SearchDomain.ScheduledSearches { + scheduledSearches[i] = mapHumioGraphqlScheduledSearchToScheduledSearch(query.SearchDomain.ScheduledSearches[i]) + } + + return scheduledSearches, err +} + +func (a *ScheduledSearches) Update(viewName string, updateScheduledSearch *ScheduledSearch) (*ScheduledSearch, error) { + if updateScheduledSearch == nil { + return nil, fmt.Errorf("updateScheduledSearch must not be nil") + } + + if updateScheduledSearch.ID == "" { + return nil, fmt.Errorf("updateScheduledSearch must have non-empty ID") + } + + var mutation struct { + humiographql.ScheduledSearch `graphql:"updateScheduledSearch(input: $input)"` + } + + actionNames := make([]graphql.String, len(updateScheduledSearch.ActionNames)) + for i, actionName := range updateScheduledSearch.ActionNames { + actionNames[i] = graphql.String(actionName) + } + + labels := make([]graphql.String, len(updateScheduledSearch.Labels)) + for i, label := range updateScheduledSearch.Labels { + labels[i] = graphql.String(label) + } + + updateAlert := humiographql.UpdateScheduledSearch{ + ViewName: graphql.String(viewName), + ID: graphql.String(updateScheduledSearch.ID), + Name: graphql.String(updateScheduledSearch.Name), + Description: graphql.String(updateScheduledSearch.Description), + QueryString: graphql.String(updateScheduledSearch.QueryString), + QueryStart: graphql.String(updateScheduledSearch.QueryStart), + QueryEnd: graphql.String(updateScheduledSearch.QueryEnd), + Schedule: graphql.String(updateScheduledSearch.Schedule), + TimeZone: graphql.String(updateScheduledSearch.TimeZone), + BackfillLimit: graphql.Int(updateScheduledSearch.BackfillLimit), + Enabled: graphql.Boolean(updateScheduledSearch.Enabled), + ActionsIdsOrNames: actionNames, + Labels: labels, + RunAsUserID: graphql.String(updateScheduledSearch.RunAsUserID), + QueryOwnership: humiographql.QueryOwnershipType(updateScheduledSearch.QueryOwnershipType), + } + + variables := map[string]any{ + "input": updateAlert, + } + + err := a.client.Mutate(&mutation, variables) + if err != nil { + return nil, err + } + + scheduledSearch := mapHumioGraphqlScheduledSearchToScheduledSearch(mutation.ScheduledSearch) + + return &scheduledSearch, nil +} + +func (a *ScheduledSearches) Create(viewName string, newScheduledSearch *ScheduledSearch) (*ScheduledSearch, error) { + if newScheduledSearch == nil { + return nil, fmt.Errorf("newScheduledSearch must not be nil") + } + + var mutation struct { + humiographql.ScheduledSearch `graphql:"createScheduledSearch(input: $input)"` + } + + actionNames := make([]graphql.String, len(newScheduledSearch.ActionNames)) + for i, actionName := range newScheduledSearch.ActionNames { + actionNames[i] = graphql.String(actionName) + } + + labels := make([]graphql.String, len(newScheduledSearch.Labels)) + for i, label := range newScheduledSearch.Labels { + labels[i] = graphql.String(label) + } + + createScheduledSearch := humiographql.CreateScheduledSearch{ + ViewName: graphql.String(viewName), + Name: graphql.String(newScheduledSearch.Name), + Description: graphql.String(newScheduledSearch.Description), + QueryString: graphql.String(newScheduledSearch.QueryString), + QueryStart: graphql.String(newScheduledSearch.QueryStart), + QueryEnd: graphql.String(newScheduledSearch.QueryEnd), + Schedule: graphql.String(newScheduledSearch.Schedule), + TimeZone: graphql.String(newScheduledSearch.TimeZone), + BackfillLimit: graphql.Int(newScheduledSearch.BackfillLimit), + Enabled: graphql.Boolean(newScheduledSearch.Enabled), + ActionsIdsOrNames: actionNames, + Labels: labels, + RunAsUserID: graphql.String(newScheduledSearch.RunAsUserID), + QueryOwnership: humiographql.QueryOwnershipType(newScheduledSearch.QueryOwnershipType), + } + + variables := map[string]any{ + "input": createScheduledSearch, + } + + err := a.client.Mutate(&mutation, variables) + if err != nil { + return nil, err + } + + scheduledSearch := mapHumioGraphqlScheduledSearchToScheduledSearch(mutation.ScheduledSearch) + + return &scheduledSearch, nil +} + +func (a *ScheduledSearches) Delete(viewName, scheduledSearchID string) error { + if scheduledSearchID == "" { + return fmt.Errorf("scheduledSearchID is empty") + } + + var mutation struct { + DidDelete bool `graphql:"deleteScheduledSearch(input: { viewName: $viewName, id: $id })"` + } + + variables := map[string]any{ + "viewName": graphql.String(viewName), + "id": graphql.String(scheduledSearchID), + } + + err := a.client.Mutate(&mutation, variables) + + if !mutation.DidDelete { + return fmt.Errorf("unable to remove scheduled search in repo/view '%s' with id '%s'", viewName, scheduledSearchID) + } + + return err +} + +func (a *ScheduledSearches) Get(viewName string, scheduledSearchId string) (*ScheduledSearch, error) { + var query struct { + SearchDomain struct { + ScheduledSearch humiographql.ScheduledSearch `graphql:"scheduledSearch(id: $scheduledSearchId)"` + } `graphql:"searchDomain(name: $viewName) "` + } + + variables := map[string]any{ + "viewName": graphql.String(viewName), + "scheduledSearchId": graphql.String(scheduledSearchId), + } + + err := a.client.Query(&query, variables) + if err != nil { + return nil, err + } + + scheduledSearch := mapHumioGraphqlScheduledSearchToScheduledSearch(query.SearchDomain.ScheduledSearch) + + return &scheduledSearch, nil +} + +func mapHumioGraphqlScheduledSearchToScheduledSearch(input humiographql.ScheduledSearch) ScheduledSearch { + var queryOwnershipType, runAsUserID string + switch input.QueryOwnership.QueryOwnershipTypeName { + case humiographql.QueryOwnershipTypeNameOrganization: + queryOwnershipType = QueryOwnershipTypeOrganization + case humiographql.QueryOwnershipTypeNameUser: + queryOwnershipType = QueryOwnershipTypeUser + runAsUserID = string(input.QueryOwnership.ID) + } + + var actionNames = make([]string, len(input.ActionsV2)) + for i := range input.ActionsV2 { + actionNames[i] = string(input.ActionsV2[i].Name) + } + + var labels = make([]string, len(input.Labels)) + for i := range input.Labels { + labels[i] = string(input.Labels[i]) + } + + return ScheduledSearch{ + ID: string(input.ID), + Name: string(input.Name), + Description: string(input.Description), + QueryString: string(input.QueryString), + QueryStart: string(input.Start), + QueryEnd: string(input.End), + TimeZone: string(input.TimeZone), + Schedule: string(input.Schedule), + BackfillLimit: int(input.BackfillLimit), + ActionNames: actionNames, + Labels: labels, + Enabled: bool(input.Enabled), + QueryOwnershipType: queryOwnershipType, + RunAsUserID: runAsUserID, + } +} diff --git a/cmd/humioctl/root.go b/cmd/humioctl/root.go index 5598f989..912909ac 100644 --- a/cmd/humioctl/root.go +++ b/cmd/humioctl/root.go @@ -122,6 +122,7 @@ Common Management Commands: rootCmd.AddCommand(newActionsCmd()) rootCmd.AddCommand(newAlertsCmd()) rootCmd.AddCommand(newFilterAlertsCmd()) + rootCmd.AddCommand(newScheduledSearchesCmd()) rootCmd.AddCommand(newPackagesCmd()) rootCmd.AddCommand(newGroupsCmd()) rootCmd.AddCommand(newFilesCmd()) diff --git a/cmd/humioctl/scheduled_searches.go b/cmd/humioctl/scheduled_searches.go new file mode 100644 index 00000000..34868705 --- /dev/null +++ b/cmd/humioctl/scheduled_searches.go @@ -0,0 +1,34 @@ +// Copyright © 2024 CrowdStrike +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/spf13/cobra" +) + +func newScheduledSearchesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "scheduled-searches", + Short: "Manage scheduled searches", + } + + cmd.AddCommand(newScheduledSearchesListCmd()) + cmd.AddCommand(newScheduledSearchesInstallCmd()) + cmd.AddCommand(newScheduledSearchesExportCmd()) + cmd.AddCommand(newScheduledSearchesRemoveCmd()) + cmd.AddCommand(newScheduledSearchesShowCmd()) + + return cmd +} diff --git a/cmd/humioctl/scheduled_searches_export.go b/cmd/humioctl/scheduled_searches_export.go new file mode 100644 index 00000000..9dcee503 --- /dev/null +++ b/cmd/humioctl/scheduled_searches_export.go @@ -0,0 +1,67 @@ +// Copyright © 2024 CrowdStrike +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/humio/cli/api" + "os" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +func newScheduledSearchesExportCmd() *cobra.Command { + var outputName string + + cmd := cobra.Command{ + Use: "export [flags] ", + Short: "Export a scheduled search in to a file.", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + view := args[0] + scheduledSearchName := args[1] + client := NewApiClient(cmd) + + if outputName == "" { + outputName = scheduledSearchName + } + + scheduledSearches, err := client.ScheduledSearches().List(view) + exitOnError(cmd, err, "Could not list scheduled searches") + + var scheduledSearch api.ScheduledSearch + for _, ss := range scheduledSearches { + if ss.Name == scheduledSearchName { + scheduledSearch = ss + } + } + + if scheduledSearch.ID == "" { + exitOnError(cmd, api.ScheduledSearchNotFound(scheduledSearchName), "Could not find scheduled search") + } + + yamlData, err := yaml.Marshal(&scheduledSearch) + exitOnError(cmd, err, "Failed to serialize the scheduled search") + + outFilePath := outputName + ".yaml" + err = os.WriteFile(outFilePath, yamlData, 0600) + exitOnError(cmd, err, "Error saving the scheduled search file") + }, + } + + cmd.Flags().StringVarP(&outputName, "output", "o", "", "The file path where the scheduled search should be written. Defaults to ./.yaml") + + return &cmd +} diff --git a/cmd/humioctl/scheduled_searches_install.go b/cmd/humioctl/scheduled_searches_install.go new file mode 100644 index 00000000..2fd6f2d3 --- /dev/null +++ b/cmd/humioctl/scheduled_searches_install.go @@ -0,0 +1,79 @@ +// Copyright © 2024 CrowdStrike +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + + "github.com/humio/cli/api" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +func newScheduledSearchesInstallCmd() *cobra.Command { + var ( + filePath, url string + ) + + cmd := cobra.Command{ + Use: "install [flags] ", + Short: "Installs a scheduled search in a view", + Long: `Install a scheduled search from a URL or from a local file. + +The install command allows you to install scheduled searches from a URL or from a local file, e.g. + + $ humioctl scheduled-searches install viewName --url=https://example.com/acme/scheduled-search.yaml + + $ humioctl scheduled-searches install viewName --file=./scheduled-searches.yaml +`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var content []byte + var err error + + // Check that we got the right number of argument + // if we only got you must supply --file or --url. + if l := len(args); l == 1 { + if filePath != "" { + content, err = getBytesFromFile(filePath) + } else if url != "" { + content, err = getBytesFromURL(url) + } else { + cmd.Printf("You must specify a path using --file or --url\n") + os.Exit(1) + } + } + exitOnError(cmd, err, "Could to load the scheduled search") + + client := NewApiClient(cmd) + viewName := args[0] + + var scheduledSearch api.ScheduledSearch + err = yaml.Unmarshal(content, &scheduledSearch) + exitOnError(cmd, err, "Could not unmarshal the scheduled search") + + _, err = client.ScheduledSearches().Create(viewName, &scheduledSearch) + exitOnError(cmd, err, "Could not create the scheduled search") + + fmt.Fprintln(cmd.OutOrStdout(), "Scheduled search created") + }, + } + + cmd.Flags().StringVar(&filePath, "file", "", "The local file path to the scheduled search to install.") + cmd.Flags().StringVar(&url, "url", "", "A URL to fetch the scheduled search file from.") + cmd.MarkFlagsMutuallyExclusive("file", "url") + return &cmd +} diff --git a/cmd/humioctl/scheduled_searches_list.go b/cmd/humioctl/scheduled_searches_list.go new file mode 100644 index 00000000..cfb9e537 --- /dev/null +++ b/cmd/humioctl/scheduled_searches_list.go @@ -0,0 +1,60 @@ +// Copyright © 2024 CrowdStrike +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/humio/cli/cmd/internal/format" + "github.com/spf13/cobra" + "strings" +) + +func newScheduledSearchesListCmd() *cobra.Command { + cmd := cobra.Command{ + Use: "list ", + Short: "List all scheduled searches in a view.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + view := args[0] + client := NewApiClient(cmd) + + scheduledSearches, err := client.ScheduledSearches().List(view) + exitOnError(cmd, err, "Error fetching scheduled searches") + + var rows = make([][]format.Value, len(scheduledSearches)) + for i := range scheduledSearches { + scheduledSearch := scheduledSearches[i] + rows[i] = []format.Value{ + format.String(scheduledSearch.ID), + format.String(scheduledSearch.Name), + format.String(scheduledSearch.Description), + format.String(scheduledSearch.QueryStart), + format.String(scheduledSearch.QueryEnd), + format.String(scheduledSearch.TimeZone), + format.String(scheduledSearch.Schedule), + format.Int(scheduledSearch.BackfillLimit), + format.String(strings.Join(scheduledSearch.ActionNames, ", ")), + format.String(strings.Join(scheduledSearch.Labels, ", ")), + format.Bool(scheduledSearch.Enabled), + format.String(scheduledSearch.RunAsUserID), + format.String(scheduledSearch.QueryOwnershipType), + } + } + + printOverviewTable(cmd, []string{"ID", "Name", "Description", "Query Start", "Query End", "Time Zone", "Schedule", "Backfill Limit", "Action Names", "Labels", "Enabled", "Run As User ID", "Query Ownership Type"}, rows) + }, + } + + return &cmd +} diff --git a/cmd/humioctl/scheduled_searches_remove.go b/cmd/humioctl/scheduled_searches_remove.go new file mode 100644 index 00000000..57322e64 --- /dev/null +++ b/cmd/humioctl/scheduled_searches_remove.go @@ -0,0 +1,57 @@ +// Copyright © 2024 CrowdStrike +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "github.com/humio/cli/api" + + "github.com/spf13/cobra" +) + +func newScheduledSearchesRemoveCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "remove ", + Short: "Removes a scheduled search.", + Long: `Removes the scheduled search with name '' in the view with name ''.`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + viewName := args[0] + scheduledSearchName := args[1] + client := NewApiClient(cmd) + + scheduledSearches, err := client.ScheduledSearches().List(viewName) + exitOnError(cmd, err, "Could not list scheduled searches") + + var scheduledSearch api.ScheduledSearch + for _, ss := range scheduledSearches { + if ss.Name == scheduledSearchName { + scheduledSearch = ss + } + } + + if scheduledSearch.ID == "" { + exitOnError(cmd, api.ScheduledSearchNotFound(scheduledSearchName), "Could not find scheduled search") + } + + err = client.ScheduledSearches().Delete(viewName, scheduledSearch.ID) + exitOnError(cmd, err, "Could not remove scheduled search") + + fmt.Fprintf(cmd.OutOrStdout(), "Successfully removed scheduled search %q from view %q\n", scheduledSearchName, viewName) + }, + } + + return cmd +} diff --git a/cmd/humioctl/scheduled_searches_show.go b/cmd/humioctl/scheduled_searches_show.go new file mode 100644 index 00000000..e65726f9 --- /dev/null +++ b/cmd/humioctl/scheduled_searches_show.go @@ -0,0 +1,70 @@ +// Copyright © 2024 CrowdStrike +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/humio/cli/api" + "github.com/humio/cli/cmd/internal/format" + "github.com/spf13/cobra" + "strings" +) + +func newScheduledSearchesShowCmd() *cobra.Command { + cmd := cobra.Command{ + Use: "show ", + Short: "Show details about a scheduled search in a view.", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + view := args[0] + name := args[1] + client := NewApiClient(cmd) + + scheduledSearches, err := client.ScheduledSearches().List(view) + exitOnError(cmd, err, "Could not list scheduled searches") + + var scheduledSearch api.ScheduledSearch + for _, ss := range scheduledSearches { + if ss.Name == name { + scheduledSearch = ss + } + } + + if scheduledSearch.ID == "" { + exitOnError(cmd, api.ScheduledSearchNotFound(name), "Could not find scheduled search") + } + + details := [][]format.Value{ + {format.String("ID"), format.String(scheduledSearch.ID)}, + {format.String("Name"), format.String(scheduledSearch.Name)}, + {format.String("Description"), format.String(scheduledSearch.Description)}, + {format.String("Query String"), format.String(scheduledSearch.QueryString)}, + {format.String("Query Start"), format.String(scheduledSearch.QueryStart)}, + {format.String("Query End"), format.String(scheduledSearch.QueryEnd)}, + {format.String("Time Zone"), format.String(scheduledSearch.TimeZone)}, + {format.String("Schedule"), format.String(scheduledSearch.Schedule)}, + {format.String("Backfill Limit"), format.Int(scheduledSearch.BackfillLimit)}, + {format.String("Enabled"), format.Bool(scheduledSearch.Enabled)}, + {format.String("Actions"), format.String(strings.Join(scheduledSearch.ActionNames, ", "))}, + {format.String("Run As User ID"), format.String(scheduledSearch.RunAsUserID)}, + {format.String("Labels"), format.String(strings.Join(scheduledSearch.Labels, ", "))}, + {format.String("Query Ownership Type"), format.String(scheduledSearch.QueryOwnershipType)}, + } + + printDetailsTable(cmd, details) + }, + } + + return &cmd +} From b70d822c53b3fab7c331de00c67f510e30c6afa5 Mon Sep 17 00:00:00 2001 From: fjerlov-cs <158591046+fjerlov-cs@users.noreply.github.com> Date: Thu, 4 Jul 2024 14:02:45 +0200 Subject: [PATCH 7/8] Filter alerts - Add ThrottleTimeSeconds and ThrottleField (#166) --- api/filter-alerts.go | 82 ++++++++++++---------- api/internal/humiographql/filter-alerts.go | 60 +++++++++------- cmd/humioctl/filter_alerts_list.go | 4 +- cmd/humioctl/filter_alerts_show.go | 2 + 4 files changed, 83 insertions(+), 65 deletions(-) diff --git a/api/filter-alerts.go b/api/filter-alerts.go index 3a45f3ec..7bdcfdd2 100644 --- a/api/filter-alerts.go +++ b/api/filter-alerts.go @@ -7,15 +7,17 @@ import ( ) type FilterAlert struct { - ID string `graphql:"id" yaml:"-" json:"id"` - Name string `graphql:"name" yaml:"name" json:"name"` - Description string `graphql:"description" yaml:"description,omitempty" json:"description,omitempty"` - QueryString string `graphql:"queryString" yaml:"queryString" json:"queryString"` - ActionNames []string `graphql:"actionNames" yaml:"actionNames" json:"actionNames"` - Labels []string `graphql:"labels" yaml:"labels" json:"labels"` - Enabled bool `graphql:"enabled" yaml:"enabled" json:"enabled"` - QueryOwnershipType string `graphql:"queryOwnership" yaml:"queryOwnershipType" json:"queryOwnershipType"` - RunAsUserID string `graphql:"runAsUserId" yaml:"runAsUserId,omitempty" json:"runAsUserId,omitempty"` + ID string `graphql:"id" yaml:"-" json:"id"` + Name string `graphql:"name" yaml:"name" json:"name"` + Description string `graphql:"description" yaml:"description,omitempty" json:"description,omitempty"` + QueryString string `graphql:"queryString" yaml:"queryString" json:"queryString"` + ActionNames []string `graphql:"actionNames" yaml:"actionNames" json:"actionNames"` + Labels []string `graphql:"labels" yaml:"labels" json:"labels"` + Enabled bool `graphql:"enabled" yaml:"enabled" json:"enabled"` + QueryOwnershipType string `graphql:"queryOwnership" yaml:"queryOwnershipType" json:"queryOwnershipType"` + ThrottleTimeSeconds int `graphql:"throttleTimeSeconds" yaml:"throttleTimeSeconds,omitempty" json:"throttleTimeSeconds,omitempty"` + ThrottleField string `graphql:"throttleField" yaml:"throttleField,omitempty" json:"throttleField"` + RunAsUserID string `graphql:"runAsUserId" yaml:"runAsUserId,omitempty" json:"runAsUserId,omitempty"` } type FilterAlerts struct { @@ -72,16 +74,18 @@ func (fa *FilterAlerts) Update(viewName string, updatedFilterAlert *FilterAlert) } updateAlert := humiographql.UpdateFilterAlert{ - ViewName: humiographql.RepoOrViewName(viewName), - ID: graphql.String(updatedFilterAlert.ID), - Name: graphql.String(updatedFilterAlert.Name), - Description: graphql.String(updatedFilterAlert.Description), - QueryString: graphql.String(updatedFilterAlert.QueryString), - ActionIdsOrNames: actionNames, - Labels: labels, - Enabled: graphql.Boolean(updatedFilterAlert.Enabled), - RunAsUserID: graphql.String(updatedFilterAlert.RunAsUserID), - QueryOwnershipType: humiographql.QueryOwnershipType(updatedFilterAlert.QueryOwnershipType), + ViewName: humiographql.RepoOrViewName(viewName), + ID: graphql.String(updatedFilterAlert.ID), + Name: graphql.String(updatedFilterAlert.Name), + Description: graphql.String(updatedFilterAlert.Description), + QueryString: graphql.String(updatedFilterAlert.QueryString), + ActionIdsOrNames: actionNames, + Labels: labels, + Enabled: graphql.Boolean(updatedFilterAlert.Enabled), + RunAsUserID: graphql.String(updatedFilterAlert.RunAsUserID), + ThrottleTimeSeconds: humiographql.Long(updatedFilterAlert.ThrottleTimeSeconds), + ThrottleField: graphql.String(updatedFilterAlert.ThrottleField), + QueryOwnershipType: humiographql.QueryOwnershipType(updatedFilterAlert.QueryOwnershipType), } variables := map[string]any{ @@ -118,15 +122,17 @@ func (fa *FilterAlerts) Create(viewName string, newFilterAlert *FilterAlert) (*F } createFilterAlert := humiographql.CreateFilterAlert{ - ViewName: humiographql.RepoOrViewName(viewName), - Name: graphql.String(newFilterAlert.Name), - Description: graphql.String(newFilterAlert.Description), - QueryString: graphql.String(newFilterAlert.QueryString), - ActionIdsOrNames: actionNames, - Labels: labels, - Enabled: graphql.Boolean(newFilterAlert.Enabled), - RunAsUserID: graphql.String(newFilterAlert.RunAsUserID), - QueryOwnershipType: humiographql.QueryOwnershipType(newFilterAlert.QueryOwnershipType), + ViewName: humiographql.RepoOrViewName(viewName), + Name: graphql.String(newFilterAlert.Name), + Description: graphql.String(newFilterAlert.Description), + QueryString: graphql.String(newFilterAlert.QueryString), + ActionIdsOrNames: actionNames, + Labels: labels, + Enabled: graphql.Boolean(newFilterAlert.Enabled), + ThrottleTimeSeconds: humiographql.Long(newFilterAlert.ThrottleTimeSeconds), + ThrottleField: graphql.String(newFilterAlert.ThrottleField), + RunAsUserID: graphql.String(newFilterAlert.RunAsUserID), + QueryOwnershipType: humiographql.QueryOwnershipType(newFilterAlert.QueryOwnershipType), } variables := map[string]any{ @@ -209,14 +215,16 @@ func mapHumioGraphqlFilterAlertToFilterAlert(input humiographql.FilterAlert) Fil } return FilterAlert{ - ID: string(input.ID), - Name: string(input.Name), - Description: string(input.Description), - QueryString: string(input.QueryString), - ActionNames: actionNames, - Labels: labels, - Enabled: bool(input.Enabled), - QueryOwnershipType: queryOwnershipType, - RunAsUserID: runAsUserID, + ID: string(input.ID), + Name: string(input.Name), + Description: string(input.Description), + QueryString: string(input.QueryString), + ActionNames: actionNames, + Labels: labels, + Enabled: bool(input.Enabled), + ThrottleTimeSeconds: int(input.ThrottleTimeSeconds), + ThrottleField: string(input.ThrottleField), + QueryOwnershipType: queryOwnershipType, + RunAsUserID: runAsUserID, } } diff --git a/api/internal/humiographql/filter-alerts.go b/api/internal/humiographql/filter-alerts.go index 0407231a..b612dde2 100644 --- a/api/internal/humiographql/filter-alerts.go +++ b/api/internal/humiographql/filter-alerts.go @@ -5,14 +5,16 @@ import ( ) type FilterAlert struct { - ID graphql.String `graphql:"id"` - Name graphql.String `graphql:"name"` - Description graphql.String `graphql:"description"` - QueryString graphql.String `graphql:"queryString"` - Actions []Action `graphql:"actions"` - Labels []graphql.String `graphql:"labels"` - Enabled graphql.Boolean `graphql:"enabled"` - QueryOwnership QueryOwnership `graphql:"queryOwnership"` + ID graphql.String `graphql:"id"` + Name graphql.String `graphql:"name"` + Description graphql.String `graphql:"description"` + QueryString graphql.String `graphql:"queryString"` + Actions []Action `graphql:"actions"` + Labels []graphql.String `graphql:"labels"` + Enabled graphql.Boolean `graphql:"enabled"` + ThrottleTimeSeconds Long `graphql:"throttleTimeSeconds"` + ThrottleField graphql.String `graphql:"throttleField"` + QueryOwnership QueryOwnership `graphql:"queryOwnership"` } type Action struct { @@ -20,26 +22,30 @@ type Action struct { } type CreateFilterAlert struct { - ViewName RepoOrViewName `json:"viewName"` - Name graphql.String `json:"name"` - Description graphql.String `json:"description,omitempty"` - QueryString graphql.String `json:"queryString"` - ActionIdsOrNames []graphql.String `json:"actionIdsOrNames"` - Labels []graphql.String `json:"labels"` - Enabled graphql.Boolean `json:"enabled"` - RunAsUserID graphql.String `json:"runAsUserId,omitempty"` - QueryOwnershipType QueryOwnershipType `json:"queryOwnershipType"` + ViewName RepoOrViewName `json:"viewName"` + Name graphql.String `json:"name"` + Description graphql.String `json:"description,omitempty"` + QueryString graphql.String `json:"queryString"` + ActionIdsOrNames []graphql.String `json:"actionIdsOrNames"` + Labels []graphql.String `json:"labels"` + Enabled graphql.Boolean `json:"enabled"` + ThrottleTimeSeconds Long `json:"throttleTimeSeconds"` + ThrottleField graphql.String `json:"throttleField"` + RunAsUserID graphql.String `json:"runAsUserId,omitempty"` + QueryOwnershipType QueryOwnershipType `json:"queryOwnershipType"` } type UpdateFilterAlert struct { - ViewName RepoOrViewName `json:"viewName"` - ID graphql.String `json:"id"` - Name graphql.String `json:"name"` - Description graphql.String `json:"description,omitempty"` - QueryString graphql.String `json:"queryString"` - ActionIdsOrNames []graphql.String `json:"actionIdsOrNames"` - Labels []graphql.String `json:"labels"` - Enabled graphql.Boolean `json:"enabled"` - RunAsUserID graphql.String `json:"runAsUserId,omitempty"` - QueryOwnershipType QueryOwnershipType `json:"queryOwnershipType"` + ViewName RepoOrViewName `json:"viewName"` + ID graphql.String `json:"id"` + Name graphql.String `json:"name"` + Description graphql.String `json:"description,omitempty"` + QueryString graphql.String `json:"queryString"` + ActionIdsOrNames []graphql.String `json:"actionIdsOrNames"` + Labels []graphql.String `json:"labels"` + Enabled graphql.Boolean `json:"enabled"` + ThrottleTimeSeconds Long `json:"throttleTimeSeconds"` + ThrottleField graphql.String `json:"throttleField"` + RunAsUserID graphql.String `json:"runAsUserId,omitempty"` + QueryOwnershipType QueryOwnershipType `json:"queryOwnershipType"` } diff --git a/cmd/humioctl/filter_alerts_list.go b/cmd/humioctl/filter_alerts_list.go index d5b6712f..b7f96e6b 100644 --- a/cmd/humioctl/filter_alerts_list.go +++ b/cmd/humioctl/filter_alerts_list.go @@ -43,12 +43,14 @@ func newFilterAlertsListCmd() *cobra.Command { format.String(filterAlert.Description), format.String(strings.Join(filterAlert.ActionNames, ", ")), format.String(strings.Join(filterAlert.Labels, ", ")), + format.Int(filterAlert.ThrottleTimeSeconds), + format.String(filterAlert.ThrottleField), format.String(filterAlert.RunAsUserID), format.String(filterAlert.QueryOwnershipType), } } - printOverviewTable(cmd, []string{"ID", "Name", "Enabled", "Description", "Actions", "Labels", "Run As User ID", "Query Ownership Type"}, rows) + printOverviewTable(cmd, []string{"ID", "Name", "Enabled", "Description", "Actions", "Labels", "Throttle Time Seconds", "Throttle Field", "Run As User ID", "Query Ownership Type"}, rows) }, } diff --git a/cmd/humioctl/filter_alerts_show.go b/cmd/humioctl/filter_alerts_show.go index aef51fa3..07f00bc4 100644 --- a/cmd/humioctl/filter_alerts_show.go +++ b/cmd/humioctl/filter_alerts_show.go @@ -54,6 +54,8 @@ func newFilterAlertsShowCmd() *cobra.Command { {format.String("Query String"), format.String(filterAlert.QueryString)}, {format.String("Labels"), format.String(strings.Join(filterAlert.Labels, ", "))}, {format.String("Actions"), format.String(strings.Join(filterAlert.ActionNames, ", "))}, + {format.String("Throttle Time Seconds"), format.Int(filterAlert.ThrottleTimeSeconds)}, + {format.String("Throttle Field"), format.String(filterAlert.ThrottleField)}, {format.String("Run As User ID"), format.String(filterAlert.RunAsUserID)}, {format.String("Query Ownership Type"), format.String(filterAlert.QueryOwnershipType)}, } From 5189483b862be15623d5699bc722b5f5f87e4bb1 Mon Sep 17 00:00:00 2001 From: Mike Rostermund Date: Thu, 4 Jul 2024 14:06:19 +0200 Subject: [PATCH 8/8] Fix spacing --- api/filter-alerts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/filter-alerts.go b/api/filter-alerts.go index 7bdcfdd2..9a459b3a 100644 --- a/api/filter-alerts.go +++ b/api/filter-alerts.go @@ -7,7 +7,7 @@ import ( ) type FilterAlert struct { - ID string `graphql:"id" yaml:"-" json:"id"` + ID string `graphql:"id" yaml:"-" json:"id"` Name string `graphql:"name" yaml:"name" json:"name"` Description string `graphql:"description" yaml:"description,omitempty" json:"description,omitempty"` QueryString string `graphql:"queryString" yaml:"queryString" json:"queryString"`