diff --git a/api/internal/humiographql/parsers.go b/api/internal/humiographql/parsers.go new file mode 100644 index 0000000..46ce25f --- /dev/null +++ b/api/internal/humiographql/parsers.go @@ -0,0 +1,25 @@ +package humiographql + +type ParserTestEventInput struct { + RawString string `graphql:"rawString"` +} + +type FieldHasValueInput struct { + FieldName string `graphql:"fieldName"` + ExpectedValue string `graphql:"expectedValue"` +} + +type ParserTestCaseOutputAssertionsInput struct { + FieldsNotPresent []string `graphql:"fieldsNotPresent"` + FieldsHaveValues []FieldHasValueInput `graphql:"fieldsHaveValues"` +} + +type ParserTestCaseAssertionsForOutputInput struct { + OutputEventIndex int `graphql:"outputEventIndex"` + Assertions ParserTestCaseOutputAssertionsInput `graphql:"assertions"` +} + +type ParserTestCaseInput struct { + Event ParserTestEventInput `graphql:"event"` + OutputAssertions []ParserTestCaseAssertionsForOutputInput `graphql:"outputAssertions"` +} diff --git a/api/parsers.go b/api/parsers.go index cf82a29..e3b1c38 100644 --- a/api/parsers.go +++ b/api/parsers.go @@ -1,19 +1,43 @@ package api -import graphql "github.com/cli/shurcooL-graphql" +import ( + graphql "github.com/cli/shurcooL-graphql" + "github.com/humio/cli/api/internal/humiographql" +) + +const LogScaleVersionWithParserAPIv2 = "1.129.0" + +type ParserTestEvent struct { + RawString string `graphql:"rawString"` +} + +type FieldHasValue struct { + FieldName string `graphql:"fieldName"` + ExpectedValue string `graphql:"expectedValue"` +} + +type ParserTestCaseOutputAssertions struct { + FieldsNotPresent []string `graphql:"fieldsNotPresent"` + FieldsHaveValues []FieldHasValue `graphql:"fieldsHaveValues"` +} + +type ParserTestCaseAssertionsForOutput struct { + OutputEventIndex int `graphql:"outputEventIndex"` + Assertions ParserTestCaseOutputAssertions `graphql:"assertions"` +} type ParserTestCase struct { - Input string - Output map[string]string + Event ParserTestEvent `graphql:"event"` + OutputAssertions []ParserTestCaseAssertionsForOutput `graphql:"outputAssertions"` } 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 `yaml:",flow"` + TestCases []ParserTestCase `yaml:",omitempty"` + FieldsToTag []string `yaml:",omitempty"` + FieldsToBeRemovedBeforeParsing []string `yaml:",omitempty"` } type Parsers struct { @@ -47,12 +71,39 @@ 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 !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) @@ -68,48 +119,178 @@ func (p *Parsers) Remove(repositoryName string, parserName string) error { return p.client.Mutate(&mutation, variables) } -func (p *Parsers) Add(repositoryName string, parser *Parser, force bool) error { +func (p *Parsers) Add(repositoryName string, parser *Parser, allowOverwritingExistingParser bool) error { + status, err := p.client.Status() + if err != nil { + return err + } + + atLeast, err := status.AtLeast(LogScaleVersionWithParserAPIv2) + if !atLeast { + 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})"` + } + + tagFieldsGQL := make([]graphql.String, len(parser.FieldsToTag)) + + for i, field := range parser.FieldsToTag { + tagFieldsGQL[i] = graphql.String(field) + } + + testsGQL := make([]graphql.String, len(parser.TestCases)) + + for i, field := range parser.TestCases { + testsGQL[i] = graphql.String(field.Event.RawString) + } + + 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(allowOverwritingExistingParser), + } + + return p.client.Mutate(&mutation, variables) + } 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})"` + } `graphql:"createParserV2(input: { name: $name, repositoryName: $repositoryName, testCases: $testCases, fieldsToTag: $fieldsToTag, fieldsToBeRemovedBeforeParsing: $fieldsToBeRemovedBeforeParsing, script: $script, allowOverwritingExistingParser: $allowOverwritingExistingParser})"` } - tagFieldsGQL := make([]graphql.String, len(parser.TagFields)) - - for i, field := range parser.TagFields { - tagFieldsGQL[i] = graphql.String(field) + fieldsToTagGQL := make([]graphql.String, len(parser.FieldsToTag)) + for i, field := range parser.FieldsToTag { + fieldsToTagGQL[i] = graphql.String(field) } - testsGQL := make([]graphql.String, len(parser.Tests)) + fieldsToBeRemovedBeforeParsingGQL := make([]graphql.String, len(parser.FieldsToBeRemovedBeforeParsing)) + for i, field := range parser.FieldsToBeRemovedBeforeParsing { + fieldsToBeRemovedBeforeParsingGQL[i] = graphql.String(field) + } - for i, field := range parser.Tests { - testsGQL[i] = graphql.String(field) + testCasesGQL := make([]humiographql.ParserTestCaseInput, len(parser.TestCases)) + for i := range parser.TestCases { + testCasesGQL[i] = mapParserTestCaseToInput(parser.TestCases[i]) } 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), + "name": graphql.String(parser.Name), + "script": graphql.String(parser.Script), + "repositoryName": humiographql.RepoOrViewName(repositoryName), + "testCases": testCasesGQL, + "fieldsToTag": fieldsToTagGQL, + "fieldsToBeRemovedBeforeParsing": fieldsToBeRemovedBeforeParsingGQL, + "allowOverwritingExistingParser": graphql.Boolean(allowOverwritingExistingParser), } return p.client.Mutate(&mutation, variables) } +// ParserTestEvent -> ParserTestEventInput +func mapParserTestEventToInput(p ParserTestEvent) humiographql.ParserTestEventInput { + return humiographql.ParserTestEventInput{RawString: p.RawString} +} + +// FieldHasValue -> FieldHasValueInput +func mapFieldHasValueToInput(p FieldHasValue) humiographql.FieldHasValueInput { + return humiographql.FieldHasValueInput{ + FieldName: p.FieldName, + ExpectedValue: p.ExpectedValue, + } +} + +// ParserTestCase -> ParserTestCaseInput, this is where add() should call +func mapParserTestCaseToInput(p ParserTestCase) humiographql.ParserTestCaseInput { + parserTestCaseAssertionsForOutputInput := make([]humiographql.ParserTestCaseAssertionsForOutputInput, len(p.OutputAssertions)) + for i := range p.OutputAssertions { + parserTestCaseAssertionsForOutputInput[i] = humiographql.ParserTestCaseAssertionsForOutputInput{ + OutputEventIndex: p.OutputAssertions[i].OutputEventIndex, + Assertions: mapParserTestCaseOutputAssertionsToInput(p.OutputAssertions[i].Assertions), + } + } + return humiographql.ParserTestCaseInput{ + Event: mapParserTestEventToInput(p.Event), + OutputAssertions: parserTestCaseAssertionsForOutputInput, + } +} + +func mapParserTestCaseOutputAssertionsToInput(p ParserTestCaseOutputAssertions) humiographql.ParserTestCaseOutputAssertionsInput { + fieldsHaveValuesInput := make([]humiographql.FieldHasValueInput, len(p.FieldsHaveValues)) + for i := range p.FieldsHaveValues { + fieldsHaveValuesInput[i] = mapFieldHasValueToInput(p.FieldsHaveValues[i]) + } + return humiographql.ParserTestCaseOutputAssertionsInput{ + FieldsNotPresent: p.FieldsNotPresent, + FieldsHaveValues: fieldsHaveValuesInput, + } +} + 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 !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 + } + var query struct { Repository struct { Parser *struct { - ID string - Name string - SourceCode string - TestData []string - TagFields []string + ID string + Name string + Script string + TestCases []ParserTestCase + FieldsToTag []string + FieldsToBeRemovedBeforeParsing []string } `graphql:"parser(name: $parserName)"` } `graphql:"repository(name: $repositoryName)"` } @@ -119,7 +300,7 @@ func (p *Parsers) Get(repositoryName string, parserName string) (*Parser, error) "repositoryName": graphql.String(repositoryName), } - err := p.client.Query(&query, variables) + err = p.client.Query(&query, variables) if err != nil { return nil, err } @@ -129,11 +310,12 @@ func (p *Parsers) Get(repositoryName string, parserName string) (*Parser, error) } 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, + ID: query.Repository.Parser.ID, + Name: query.Repository.Parser.Name, + TestCases: query.Repository.Parser.TestCases, + Script: query.Repository.Parser.Script, + FieldsToTag: query.Repository.Parser.FieldsToTag, + FieldsToBeRemovedBeforeParsing: query.Repository.Parser.FieldsToBeRemovedBeforeParsing, } return &parser, nil diff --git a/api/status.go b/api/status.go index 80cf0aa..3b1eb97 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 21eb1c2..827386d 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 0000000..e79a871 --- /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 654b08b..8ba49d4 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 4f137e7..66122ea 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 0967607..73edf16 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 a72ade7..edd5246 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=