diff --git a/cli/actions/run_test_action.go b/cli/actions/run_test_action.go index 1ae90eb24c..e267c17241 100644 --- a/cli/actions/run_test_action.go +++ b/cli/actions/run_test_action.go @@ -245,28 +245,8 @@ func (a runTestAction) applyTest(ctx context.Context, df defFile) (defFile, erro return df, fmt.Errorf("cannot inject local env vars: %w", err) } - var test openapi.TestResource - err = yaml.Unmarshal(df.Contents(), &test) - if err != nil { - a.logger.Error("error parsing test", zap.String("content", string(df.Contents())), zap.Error(err)) - return df, fmt.Errorf("could not unmarshal test yaml: %w", err) - } - - test, err = a.consolidateGRPCFile(df, test) - if err != nil { - return df, fmt.Errorf("could not consolidate grpc file: %w", err) - } - - marshalled, err := yaml.Marshal(test) - if err != nil { - return df, fmt.Errorf("could not marshal test yaml: %w", err) - } - df = defFile{fileutil.New(df.AbsPath(), marshalled)} - a.logger.Debug("applying test", zap.String("absolutePath", df.AbsPath()), - zap.String("id", test.Spec.GetId()), - zap.String("marshalled", string(marshalled)), ) updated, err := a.tests.Apply(ctx, df.File, a.yamlFormat) @@ -276,74 +256,17 @@ func (a runTestAction) applyTest(ctx context.Context, df defFile) (defFile, erro df = defFile{fileutil.New(df.AbsPath(), []byte(updated))} - err = yaml.Unmarshal(df.Contents(), &test) - if err != nil { - a.logger.Error("error parsing updated test", zap.String("content", string(df.Contents())), zap.Error(err)) - return df, fmt.Errorf("could not unmarshal test yaml: %w", err) - } - - a.logger.Debug("test applied", - zap.String("absolutePath", df.AbsPath()), - zap.String("id", test.Spec.GetId()), - ) - return df, nil } -func (a runTestAction) consolidateGRPCFile(df defFile, test openapi.TestResource) (openapi.TestResource, error) { - if test.Spec.Trigger.GetType() != "grpc" { - a.logger.Debug("test does not use grpc", zap.String("triggerType", test.Spec.Trigger.GetType())) - return test, nil - } - - definedPBFile := test.Spec.Trigger.Grpc.GetProtobufFile() - if !fileutil.LooksLikeFilePath(definedPBFile) { - a.logger.Debug("protobuf file is not a file path", zap.String("protobufFile", definedPBFile)) - return test, nil - } - - pbFilePath := df.RelativeFile(definedPBFile) - a.logger.Debug("protobuf file", zap.String("path", pbFilePath)) - - pbFile, err := fileutil.Read(pbFilePath) - if err != nil { - return test, fmt.Errorf(`cannot read protobuf file: %w`, err) - } - a.logger.Debug("protobuf file contents", zap.String("contents", string(pbFile.Contents()))) - - test.Spec.Trigger.Grpc.SetProtobufFile(string(pbFile.Contents())) - - return test, nil -} - func (a runTestAction) applyTransaction(ctx context.Context, df defFile) (defFile, error) { df, err := a.injectLocalEnvVars(ctx, df) if err != nil { return df, fmt.Errorf("cannot inject local env vars: %w", err) } - var tran openapi.TransactionResource - err = yaml.Unmarshal(df.Contents(), &tran) - if err != nil { - a.logger.Error("error parsing transaction", zap.String("content", string(df.Contents())), zap.Error(err)) - return df, fmt.Errorf("could not unmarshal transaction yaml: %w", err) - } - - tran, err = a.mapTransactionSteps(ctx, df, tran) - if err != nil { - return df, fmt.Errorf("could not map transaction steps: %w", err) - } - - marshalled, err := yaml.Marshal(tran) - if err != nil { - return df, fmt.Errorf("could not marshal test yaml: %w", err) - } - df = defFile{fileutil.New(df.AbsPath(), marshalled)} - a.logger.Debug("applying transaction", zap.String("absolutePath", df.AbsPath()), - zap.String("id", tran.Spec.GetId()), - zap.String("marshalled", string(marshalled)), ) updated, err := a.transactions.Apply(ctx, df.File, a.yamlFormat) @@ -352,63 +275,9 @@ func (a runTestAction) applyTransaction(ctx context.Context, df defFile) (defFil } df = defFile{fileutil.New(df.AbsPath(), []byte(updated))} - - err = yaml.Unmarshal(df.Contents(), &tran) - if err != nil { - a.logger.Error("error parsing updated transaction", zap.String("content", updated), zap.Error(err)) - return df, fmt.Errorf("could not unmarshal transaction yaml: %w", err) - } - - a.logger.Debug("transaction applied", - zap.String("absolutePath", df.AbsPath()), - zap.String("updated id", tran.Spec.GetId()), - ) - return df, nil } -func (a runTestAction) mapTransactionSteps(ctx context.Context, df defFile, tran openapi.TransactionResource) (openapi.TransactionResource, error) { - for i, step := range tran.Spec.GetSteps() { - a.logger.Debug("mapping transaction step", - zap.Int("index", i), - zap.String("step", step), - ) - if !fileutil.LooksLikeFilePath(step) { - a.logger.Debug("does not look like a file path", - zap.Int("index", i), - zap.String("step", step), - ) - continue - } - - f, err := fileutil.Read(df.RelativeFile(step)) - if err != nil { - return openapi.TransactionResource{}, fmt.Errorf("cannot read test file: %w", err) - } - - testDF, err := a.applyTest(ctx, defFile{f}) - if err != nil { - return openapi.TransactionResource{}, fmt.Errorf("cannot apply test '%s': %w", step, err) - } - - var test openapi.TestResource - err = yaml.Unmarshal(testDF.Contents(), &test) - if err != nil { - return openapi.TransactionResource{}, fmt.Errorf("cannot unmarshal updated test '%s': %w", step, err) - } - - a.logger.Debug("mapped transaction step", - zap.Int("index", i), - zap.String("step", step), - zap.String("mapped step", test.Spec.GetId()), - ) - - tran.Spec.Steps[i] = test.Spec.GetId() - } - - return tran, nil -} - func getTypeFromFile(df defFile) (string, error) { var raw map[string]any err := yaml.Unmarshal(df.Contents(), &raw) diff --git a/cli/cmd/config.go b/cli/cmd/config.go index ea769f7002..40b19ac295 100644 --- a/cli/cmd/config.go +++ b/cli/cmd/config.go @@ -16,8 +16,8 @@ import ( ) var ( + cliLogger = &zap.Logger{} cliConfig config.Config - cliLogger *zap.Logger versionText string isVersionMatch bool ) @@ -140,8 +140,7 @@ func setupLogger(cmd *cobra.Command, args []string) { zapcore.Lock(os.Stdout), atom, )) - - cliLogger = logger + *cliLogger = *logger } func teardownCommand(cmd *cobra.Command, args []string) { diff --git a/cli/cmd/resources.go b/cli/cmd/resources.go index 5540e1f8a5..7751eca078 100644 --- a/cli/cmd/resources.go +++ b/cli/cmd/resources.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "net/http" "strings" @@ -8,167 +9,187 @@ import ( "github.com/Jeffail/gabs/v2" "github.com/kubeshop/tracetest/cli/analytics" + "github.com/kubeshop/tracetest/cli/pkg/fileutil" "github.com/kubeshop/tracetest/cli/pkg/resourcemanager" + "github.com/kubeshop/tracetest/cli/preprocessor" ) var resourceParams = &resourceParameters{} -var httpClient = &resourcemanager.HTTPClient{} - -var resources = resourcemanager.NewRegistry(). - Register( - resourcemanager.NewClient( - httpClient, - "config", "configs", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "ANALYTICS ENABLED", Path: "spec.analyticsEnabled"}, - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "analyzer", "analyzers", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "ENABLED", Path: "spec.enabled"}, - {Header: "MINIMUM SCORE", Path: "spec.minimumScore"}, - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "pollingprofile", "pollingprofiles", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "STRATEGY", Path: "spec.strategy"}, - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "demo", "demos", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "TYPE", Path: "spec.type"}, - {Header: "ENABLED", Path: "spec.enabled"}, - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "datastore", "datastores", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "DEFAULT", Path: "spec.default"}, - }, - ItemModifier: func(item *gabs.Container) error { - isDefault := item.Path("spec.default").Data().(bool) - if !isDefault { - item.SetP("", "spec.default") - } else { - item.SetP("*", "spec.default") - } - return nil - }, - }), - resourcemanager.WithDeleteEnabled("DataStore removed. Defaulting back to no-tracing mode"), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "environment", "environments", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "DESCRIPTION", Path: "spec.description"}, - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "transaction", "transactions", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "VERSION", Path: "spec.version"}, - {Header: "STEPS", Path: "spec.summary.steps"}, - {Header: "RUNS", Path: "spec.summary.runs"}, - {Header: "LAST RUN TIME", Path: "spec.summary.lastRun.time"}, - {Header: "LAST RUN SUCCESSES", Path: "spec.summary.lastRun.passes"}, - {Header: "LAST RUN FAILURES", Path: "spec.summary.lastRun.fails"}, - }, - ItemModifier: func(item *gabs.Container) error { - // set spec.summary.steps to the number of steps in the transaction - item.SetP(len(item.Path("spec.steps").Children()), "spec.summary.steps") - - if err := formatItemDate(item, "spec.summary.lastRun.time"); err != nil { - return err - } - - return nil - }, - }), - ), - ). - Register( - resourcemanager.NewClient( - httpClient, - "test", "tests", - resourcemanager.WithTableConfig(resourcemanager.TableConfig{ - Cells: []resourcemanager.TableCellConfig{ - {Header: "ID", Path: "spec.id"}, - {Header: "NAME", Path: "spec.name"}, - {Header: "VERSION", Path: "spec.version"}, - {Header: "TRIGGER TYPE", Path: "spec.trigger.type"}, - {Header: "RUNS", Path: "spec.summary.runs"}, - {Header: "LAST RUN TIME", Path: "spec.summary.lastRun.time"}, - {Header: "LAST RUN SUCCESSES", Path: "spec.summary.lastRun.passes"}, - {Header: "LAST RUN FAILURES", Path: "spec.summary.lastRun.fails"}, - {Header: "URL", Path: "spec.url"}, - }, - ItemModifier: func(item *gabs.Container) error { - // set spec.summary.steps to the number of steps in the transaction - id, ok := item.Path("spec.id").Data().(string) - if !ok { - return fmt.Errorf("test id '%s' is not a string", id) - } - - url := cliConfig.URL() + "/test/" + id - item.SetP(url, "spec.url") - - if err := formatItemDate(item, "spec.summary.lastRun.time"); err != nil { - return err - } - - return nil - }, - }), - ), +var ( + httpClient = &resourcemanager.HTTPClient{} + + testPreprocessor = preprocessor.Test(cliLogger) + testClient = resourcemanager.NewClient( + httpClient, + "test", "tests", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "VERSION", Path: "spec.version"}, + {Header: "TRIGGER TYPE", Path: "spec.trigger.type"}, + {Header: "RUNS", Path: "spec.summary.runs"}, + {Header: "LAST RUN TIME", Path: "spec.summary.lastRun.time"}, + {Header: "LAST RUN SUCCESSES", Path: "spec.summary.lastRun.passes"}, + {Header: "LAST RUN FAILURES", Path: "spec.summary.lastRun.fails"}, + {Header: "URL", Path: "spec.url"}, + }, + ItemModifier: func(item *gabs.Container) error { + // set spec.summary.steps to the number of steps in the transaction + id, ok := item.Path("spec.id").Data().(string) + if !ok { + return fmt.Errorf("test id '%s' is not a string", id) + } + + url := cliConfig.URL() + "/test/" + id + item.SetP(url, "spec.url") + + if err := formatItemDate(item, "spec.summary.lastRun.time"); err != nil { + return err + } + + return nil + }, + }), + resourcemanager.WithApplyPreProcessor(testPreprocessor.Preprocess), ) + transactionPreprocessor = preprocessor.Transaction(cliLogger, func(ctx context.Context, input fileutil.File) (fileutil.File, error) { + formatYAML, err := resourcemanager.Formats.Get(resourcemanager.FormatYAML) + if err != nil { + return input, fmt.Errorf("cannot get yaml format: %w", err) + } + updated, err := testClient.Apply(ctx, input, formatYAML) + if err != nil { + return input, fmt.Errorf("cannot apply test: %w", err) + } + + return fileutil.New(input.AbsPath(), []byte(updated)), nil + }) + + resources = resourcemanager.NewRegistry(). + Register( + resourcemanager.NewClient( + httpClient, + "config", "configs", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "ANALYTICS ENABLED", Path: "spec.analyticsEnabled"}, + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "analyzer", "analyzers", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "ENABLED", Path: "spec.enabled"}, + {Header: "MINIMUM SCORE", Path: "spec.minimumScore"}, + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "pollingprofile", "pollingprofiles", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "STRATEGY", Path: "spec.strategy"}, + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "demo", "demos", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "TYPE", Path: "spec.type"}, + {Header: "ENABLED", Path: "spec.enabled"}, + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "datastore", "datastores", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "DEFAULT", Path: "spec.default"}, + }, + ItemModifier: func(item *gabs.Container) error { + isDefault := item.Path("spec.default").Data().(bool) + if !isDefault { + item.SetP("", "spec.default") + } else { + item.SetP("*", "spec.default") + } + return nil + }, + }), + resourcemanager.WithDeleteSuccessMessage("DataStore removed. Defaulting back to no-tracing mode"), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "environment", "environments", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "DESCRIPTION", Path: "spec.description"}, + }, + }), + ), + ). + Register( + resourcemanager.NewClient( + httpClient, + "transaction", "transactions", + resourcemanager.WithTableConfig(resourcemanager.TableConfig{ + Cells: []resourcemanager.TableCellConfig{ + {Header: "ID", Path: "spec.id"}, + {Header: "NAME", Path: "spec.name"}, + {Header: "VERSION", Path: "spec.version"}, + {Header: "STEPS", Path: "spec.summary.steps"}, + {Header: "RUNS", Path: "spec.summary.runs"}, + {Header: "LAST RUN TIME", Path: "spec.summary.lastRun.time"}, + {Header: "LAST RUN SUCCESSES", Path: "spec.summary.lastRun.passes"}, + {Header: "LAST RUN FAILURES", Path: "spec.summary.lastRun.fails"}, + }, + ItemModifier: func(item *gabs.Container) error { + // set spec.summary.steps to the number of steps in the transaction + item.SetP(len(item.Path("spec.steps").Children()), "spec.summary.steps") + + if err := formatItemDate(item, "spec.summary.lastRun.time"); err != nil { + return err + } + + return nil + }, + }), + resourcemanager.WithApplyPreProcessor(transactionPreprocessor.Preprocess), + ), + ). + Register(testClient) +) + func resourceList() string { return strings.Join(resources.List(), "|") } diff --git a/cli/pkg/resourcemanager/apply.go b/cli/pkg/resourcemanager/apply.go index e81147580e..5356e3ffa5 100644 --- a/cli/pkg/resourcemanager/apply.go +++ b/cli/pkg/resourcemanager/apply.go @@ -12,7 +12,18 @@ import ( const VerbApply Verb = "apply" +type applyPreProcessorFn func(context.Context, fileutil.File) (fileutil.File, error) + func (c Client) Apply(ctx context.Context, inputFile fileutil.File, requestedFormat Format) (string, error) { + + if c.options.applyPreProcessor != nil { + var err error + inputFile, err = c.options.applyPreProcessor(ctx, inputFile) + if err != nil { + return "", fmt.Errorf("cannot preprocess Apply request: %w", err) + } + } + url := c.client.url(c.resourceNamePlural) req, err := http.NewRequestWithContext(ctx, http.MethodPut, url.String(), inputFile.Reader()) if err != nil { @@ -90,5 +101,5 @@ func (c Client) Apply(ctx context.Context, inputFile fileutil.File, requestedFor } - return requestedFormat.Format(string(body), c.tableConfig) + return requestedFormat.Format(string(body), c.options.tableConfig) } diff --git a/cli/pkg/resourcemanager/client.go b/cli/pkg/resourcemanager/client.go index 0014735947..9f18cd8226 100644 --- a/cli/pkg/resourcemanager/client.go +++ b/cli/pkg/resourcemanager/client.go @@ -15,8 +15,7 @@ type Client struct { client *HTTPClient resourceName string resourceNamePlural string - deleteSuccessMsg string - tableConfig TableConfig + options options } type HTTPClient struct { @@ -56,27 +55,13 @@ func (c HTTPClient) do(req *http.Request) (*http.Response, error) { return c.client.Do(req) } -type options func(c *Client) - -func WithDeleteEnabled(deleteSuccessMssg string) options { - return func(c *Client) { - c.deleteSuccessMsg = deleteSuccessMssg - } -} - -func WithTableConfig(tableConfig TableConfig) options { - return func(c *Client) { - c.tableConfig = tableConfig - } -} - // NewClient creates a new client for a resource managed by the resourceamanger. // The tableConfig parameter configures how the table view should be rendered. // This configuration work both for a single resource from a Get, or a ResourceList from a List func NewClient( httpClient *HTTPClient, resourceName, resourceNamePlural string, - opts ...options) Client { + opts ...option) Client { c := Client{ client: httpClient, resourceName: resourceName, @@ -84,11 +69,10 @@ func NewClient( } for _, opt := range opts { - opt(&c) + opt(&c.options) } return c - } type requestError struct { diff --git a/cli/pkg/resourcemanager/delete.go b/cli/pkg/resourcemanager/delete.go index 71bf2786cf..cf4302378a 100644 --- a/cli/pkg/resourcemanager/delete.go +++ b/cli/pkg/resourcemanager/delete.go @@ -38,8 +38,8 @@ func (c Client) Delete(ctx context.Context, id string, format Format) (string, e } msg := "" - if c.deleteSuccessMsg != "" { - msg = c.deleteSuccessMsg + if c.options.deleteSuccessMsg != "" { + msg = c.options.deleteSuccessMsg } else { ucfirst := strings.ToUpper(string(c.resourceName[0])) + c.resourceName[1:] msg = fmt.Sprintf("%s successfully deleted", ucfirst) diff --git a/cli/pkg/resourcemanager/get.go b/cli/pkg/resourcemanager/get.go index 40d88d0c2c..89db55160b 100644 --- a/cli/pkg/resourcemanager/get.go +++ b/cli/pkg/resourcemanager/get.go @@ -42,5 +42,5 @@ func (c Client) Get(ctx context.Context, id string, format Format) (string, erro return "", fmt.Errorf("cannot read Get response: %w", err) } - return format.Format(string(body), c.tableConfig) + return format.Format(string(body), c.options.tableConfig) } diff --git a/cli/pkg/resourcemanager/list.go b/cli/pkg/resourcemanager/list.go index aeacab56dd..d1ebc7b7ca 100644 --- a/cli/pkg/resourcemanager/list.go +++ b/cli/pkg/resourcemanager/list.go @@ -55,5 +55,5 @@ func (c Client) List(ctx context.Context, opt ListOption, format Format) (string return "", fmt.Errorf("cannot read List response: %w", err) } - return format.Format(string(body), c.tableConfig) + return format.Format(string(body), c.options.tableConfig) } diff --git a/cli/pkg/resourcemanager/options.go b/cli/pkg/resourcemanager/options.go new file mode 100644 index 0000000000..c3200b64eb --- /dev/null +++ b/cli/pkg/resourcemanager/options.go @@ -0,0 +1,27 @@ +package resourcemanager + +type options struct { + applyPreProcessor applyPreProcessorFn + tableConfig TableConfig + deleteSuccessMsg string +} + +type option func(*options) + +func WithApplyPreProcessor(preProcessor applyPreProcessorFn) option { + return func(o *options) { + o.applyPreProcessor = preProcessor + } +} + +func WithDeleteSuccessMessage(deleteSuccessMssg string) option { + return func(o *options) { + o.deleteSuccessMsg = deleteSuccessMssg + } +} + +func WithTableConfig(tableConfig TableConfig) option { + return func(o *options) { + o.tableConfig = tableConfig + } +} diff --git a/cli/preprocessor/test.go b/cli/preprocessor/test.go new file mode 100644 index 0000000000..0c2176c27c --- /dev/null +++ b/cli/preprocessor/test.go @@ -0,0 +1,68 @@ +package preprocessor + +import ( + "context" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/kubeshop/tracetest/cli/openapi" + "github.com/kubeshop/tracetest/cli/pkg/fileutil" + "go.uber.org/zap" +) + +type test struct { + logger *zap.Logger +} + +func Test(logger *zap.Logger) test { + return test{ + logger: logger, + } +} + +func (t test) Preprocess(ctx context.Context, input fileutil.File) (fileutil.File, error) { + var test openapi.TestResource + err := yaml.Unmarshal(input.Contents(), &test) + if err != nil { + t.logger.Error("error parsing test", zap.String("content", string(input.Contents())), zap.Error(err)) + return input, fmt.Errorf("could not unmarshal test yaml: %w", err) + } + + test, err = t.consolidateGRPCFile(input, test) + if err != nil { + return input, fmt.Errorf("could not consolidate grpc file: %w", err) + } + + marshalled, err := yaml.Marshal(test) + if err != nil { + return input, fmt.Errorf("could not marshal test yaml: %w", err) + } + + return fileutil.New(input.AbsPath(), marshalled), nil +} + +func (t test) consolidateGRPCFile(input fileutil.File, test openapi.TestResource) (openapi.TestResource, error) { + if test.Spec.Trigger.GetType() != "grpc" { + t.logger.Debug("test does not use grpc", zap.String("triggerType", test.Spec.Trigger.GetType())) + return test, nil + } + + definedPBFile := test.Spec.Trigger.Grpc.GetProtobufFile() + if !fileutil.LooksLikeFilePath(definedPBFile) { + t.logger.Debug("protobuf file is not a file path", zap.String("protobufFile", definedPBFile)) + return test, nil + } + + pbFilePath := input.RelativeFile(definedPBFile) + t.logger.Debug("protobuf file", zap.String("path", pbFilePath)) + + pbFile, err := fileutil.Read(pbFilePath) + if err != nil { + return test, fmt.Errorf(`cannot read protobuf file: %w`, err) + } + t.logger.Debug("protobuf file contents", zap.String("contents", string(pbFile.Contents()))) + + test.Spec.Trigger.Grpc.SetProtobufFile(string(pbFile.Contents())) + + return test, nil +} diff --git a/cli/preprocessor/transaction.go b/cli/preprocessor/transaction.go new file mode 100644 index 0000000000..7c3015ae64 --- /dev/null +++ b/cli/preprocessor/transaction.go @@ -0,0 +1,87 @@ +package preprocessor + +import ( + "context" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/kubeshop/tracetest/cli/openapi" + "github.com/kubeshop/tracetest/cli/pkg/fileutil" + "go.uber.org/zap" +) + +type applyTestFunc func(context.Context, fileutil.File) (fileutil.File, error) + +type transaction struct { + logger *zap.Logger + applyTestFn applyTestFunc +} + +func Transaction(logger *zap.Logger, applyTestFn applyTestFunc) transaction { + return transaction{ + logger: logger, + applyTestFn: applyTestFn, + } +} + +func (t transaction) Preprocess(ctx context.Context, input fileutil.File) (fileutil.File, error) { + var tran openapi.TransactionResource + err := yaml.Unmarshal(input.Contents(), &tran) + if err != nil { + t.logger.Error("error parsing transaction", zap.String("content", string(input.Contents())), zap.Error(err)) + return input, fmt.Errorf("could not unmarshal transaction yaml: %w", err) + } + + tran, err = t.mapTransactionSteps(ctx, input, tran) + if err != nil { + return input, fmt.Errorf("could not map transaction steps: %w", err) + } + + marshalled, err := yaml.Marshal(tran) + if err != nil { + return input, fmt.Errorf("could not marshal test yaml: %w", err) + } + return fileutil.New(input.AbsPath(), marshalled), nil +} + +func (t transaction) mapTransactionSteps(ctx context.Context, input fileutil.File, tran openapi.TransactionResource) (openapi.TransactionResource, error) { + for i, step := range tran.Spec.GetSteps() { + t.logger.Debug("mapping transaction step", + zap.Int("index", i), + zap.String("step", step), + ) + if !fileutil.LooksLikeFilePath(step) { + t.logger.Debug("does not look like a file path", + zap.Int("index", i), + zap.String("step", step), + ) + continue + } + + f, err := fileutil.Read(input.RelativeFile(step)) + if err != nil { + return openapi.TransactionResource{}, fmt.Errorf("cannot read test file: %w", err) + } + + testFile, err := t.applyTestFn(ctx, f) + if err != nil { + return openapi.TransactionResource{}, fmt.Errorf("cannot apply test '%s': %w", step, err) + } + + var test openapi.TestResource + err = yaml.Unmarshal(testFile.Contents(), &test) + if err != nil { + return openapi.TransactionResource{}, fmt.Errorf("cannot unmarshal updated test '%s': %w", step, err) + } + + t.logger.Debug("mapped transaction step", + zap.Int("index", i), + zap.String("step", step), + zap.String("mapped step", test.Spec.GetId()), + ) + + tran.Spec.Steps[i] = test.Spec.GetId() + } + + return tran, nil +} diff --git a/testing/cli-e2etest/testscenarios/test/apply_test_test.go b/testing/cli-e2etest/testscenarios/test/apply_test_test.go index 4f196b1edd..9e954124ad 100644 --- a/testing/cli-e2etest/testscenarios/test/apply_test_test.go +++ b/testing/cli-e2etest/testscenarios/test/apply_test_test.go @@ -2,51 +2,91 @@ package test import ( "fmt" + "os" "testing" - "atomicgo.dev/assert" "github.com/kubeshop/tracetest/cli-e2etest/environment" "github.com/kubeshop/tracetest/cli-e2etest/helpers" "github.com/kubeshop/tracetest/cli-e2etest/testscenarios/types" "github.com/kubeshop/tracetest/cli-e2etest/tracetestcli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestApplyTest(t *testing.T) { - // instantiate require with testing helper - require := require.New(t) - - // setup isolated e2e environment - env := environment.CreateAndStart(t) - defer env.Close(t) - - cliConfig := env.GetCLIConfigPath(t) - - // Given I am a Tracetest CLI user - // And I have my server recently created - - // When I try to set up a new test - // Then it should be applied with success - testPath := env.GetTestResourcePath(t, "list") - - result := tracetestcli.Exec(t, fmt.Sprintf("apply test --file %s", testPath), tracetestcli.WithCLIConfig(cliConfig)) - helpers.RequireExitCodeEqual(t, result, 0) - - // When I try to get a test - // Then it should return the test applied on the last step - result = tracetestcli.Exec(t, "get test --id fH_8AulVR", tracetestcli.WithCLIConfig(cliConfig)) - helpers.RequireExitCodeEqual(t, result, 0) - - listTest := helpers.UnmarshalYAML[types.TestResource](t, result.StdOut) - assert.Equal("Test", listTest.Type) - assert.Equal("fH_8AulVR", listTest.Spec.ID) - assert.Equal("Pokeshop - List", listTest.Spec.Name) - assert.Equal("List Pokemon", listTest.Spec.Description) - assert.Equal("http", listTest.Spec.Trigger.Type) - assert.Equal("http://demo-api:8081/pokemon?take=20&skip=0", listTest.Spec.Trigger.HTTPRequest.URL) - assert.Equal("GET", listTest.Spec.Trigger.HTTPRequest.Method) - assert.Equal("", listTest.Spec.Trigger.HTTPRequest.Body) - require.Len(listTest.Spec.Trigger.HTTPRequest.Headers, 1) - assert.Equal("Content-Type", listTest.Spec.Trigger.HTTPRequest.Headers[0].Key) - assert.Equal("application/json", listTest.Spec.Trigger.HTTPRequest.Headers[0].Value) + t.Run("Basic", func(t *testing.T) { + // instantiate require with testing helper + require := require.New(t) + assert := assert.New(t) + + // setup isolated e2e environment + env := environment.CreateAndStart(t) + defer env.Close(t) + + cliConfig := env.GetCLIConfigPath(t) + + // Given I am a Tracetest CLI user + // And I have my server recently created + + // When I try to set up a new test + // Then it should be applied with success + testPath := env.GetTestResourcePath(t, "list") + + result := tracetestcli.Exec(t, fmt.Sprintf("apply test --file %s", testPath), tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + // When I try to get a test + // Then it should return the test applied on the last step + result = tracetestcli.Exec(t, "get test --id fH_8AulVR", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + listTest := helpers.UnmarshalYAML[types.TestResource](t, result.StdOut) + assert.Equal("Test", listTest.Type) + assert.Equal("fH_8AulVR", listTest.Spec.ID) + assert.Equal("Pokeshop - List", listTest.Spec.Name) + assert.Equal("List Pokemon", listTest.Spec.Description) + assert.Equal("http", listTest.Spec.Trigger.Type) + assert.Equal("http://demo-api:8081/pokemon?take=20&skip=0", listTest.Spec.Trigger.HTTPRequest.URL) + assert.Equal("GET", listTest.Spec.Trigger.HTTPRequest.Method) + assert.Equal("", listTest.Spec.Trigger.HTTPRequest.Body) + require.Len(listTest.Spec.Trigger.HTTPRequest.Headers, 1) + assert.Equal("Content-Type", listTest.Spec.Trigger.HTTPRequest.Headers[0].Key) + assert.Equal("application/json", listTest.Spec.Trigger.HTTPRequest.Headers[0].Value) + }) + + t.Run("EmbeddingProtobufFile", func(t *testing.T) { + // instantiate require with testing helper + require := require.New(t) + assert := assert.New(t) + + proto, err := os.ReadFile("./resources/api.proto") + require.NoError(err) + + // setup isolated e2e environment + env := environment.CreateAndStart(t) + defer env.Close(t) + + cliConfig := env.GetCLIConfigPath(t) + + // Given I am a Tracetest CLI user + // And I have my server recently created + + // When I try to set up a new test + // Then it should be applied with success + testPath := env.GetTestResourcePath(t, "grpc-trigger-reference-protobuf") + + result := tracetestcli.Exec(t, fmt.Sprintf("apply test --file %s", testPath), tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + // When I try to get a test + // Then it should return the test applied on the last step + result = tracetestcli.Exec(t, "get test --id create-pokemon", tracetestcli.WithCLIConfig(cliConfig)) + helpers.RequireExitCodeEqual(t, result, 0) + + listTest := helpers.UnmarshalYAML[types.TestResource](t, result.StdOut) + assert.Equal("Test", listTest.Type) + assert.Equal("create-pokemon", listTest.Spec.ID) + assert.Equal("grpc", listTest.Spec.Trigger.Type) + assert.Equal(string(proto), listTest.Spec.Trigger.GRPCRequest.ProtobufFile) + }) } diff --git a/testing/cli-e2etest/testscenarios/transaction/resources/another-transaction.yaml b/testing/cli-e2etest/testscenarios/transaction/resources/another-transaction.yaml index 3eb80bbbc9..749f7fc003 100644 --- a/testing/cli-e2etest/testscenarios/transaction/resources/another-transaction.yaml +++ b/testing/cli-e2etest/testscenarios/transaction/resources/another-transaction.yaml @@ -4,7 +4,7 @@ spec: name: Another Transaction description: another transaction steps: - - 9wtAH2_Vg - - 9wtAH2_Vg - - ajksdkasjbd - - ajksdkasjbd + - ./transaction-step-1.yaml + - ./transaction-step-1.yaml + - ./transaction-step-2.yaml + - ./transaction-step-2.yaml diff --git a/testing/cli-e2etest/testscenarios/transaction/resources/new-transaction.yaml b/testing/cli-e2etest/testscenarios/transaction/resources/new-transaction.yaml index 3caab5009f..f14118d545 100644 --- a/testing/cli-e2etest/testscenarios/transaction/resources/new-transaction.yaml +++ b/testing/cli-e2etest/testscenarios/transaction/resources/new-transaction.yaml @@ -4,5 +4,5 @@ spec: name: New Transaction description: a transaction steps: - - 9wtAH2_Vg - - ajksdkasjbd + - ./transaction-step-1.yaml + - ./transaction-step-2.yaml diff --git a/testing/cli-e2etest/testscenarios/transaction/resources/one-more-transaction.yaml b/testing/cli-e2etest/testscenarios/transaction/resources/one-more-transaction.yaml index 2efd96f648..a8a6f0e3bd 100644 --- a/testing/cli-e2etest/testscenarios/transaction/resources/one-more-transaction.yaml +++ b/testing/cli-e2etest/testscenarios/transaction/resources/one-more-transaction.yaml @@ -4,6 +4,6 @@ spec: name: One More Transaction description: one more transaction steps: - - 9wtAH2_Vg - - 9wtAH2_Vg - - ajksdkasjbd + - ./transaction-step-1.yaml + - ./transaction-step-1.yaml + - ./transaction-step-2.yaml diff --git a/testing/cli-e2etest/testscenarios/transaction/resources/updated-new-transaction.yaml b/testing/cli-e2etest/testscenarios/transaction/resources/updated-new-transaction.yaml index ffdad1491f..70e2c28f03 100644 --- a/testing/cli-e2etest/testscenarios/transaction/resources/updated-new-transaction.yaml +++ b/testing/cli-e2etest/testscenarios/transaction/resources/updated-new-transaction.yaml @@ -4,6 +4,6 @@ spec: name: Updated Transaction description: an updated transaction steps: - - 9wtAH2_Vg - - ajksdkasjbd - - ajksdkasjbd + - ./transaction-step-1.yaml + - ./transaction-step-2.yaml + - ./transaction-step-2.yaml diff --git a/testing/cli-e2etest/testscenarios/types/test.go b/testing/cli-e2etest/testscenarios/types/test.go index d892d61b8b..67d856e4e6 100644 --- a/testing/cli-e2etest/testscenarios/types/test.go +++ b/testing/cli-e2etest/testscenarios/types/test.go @@ -23,8 +23,13 @@ type Test struct { } type Trigger struct { - Type string `json:"type"` - HTTPRequest HTTPRequest `json:"httpRequest"` + Type string `json:"type"` + HTTPRequest *HTTPRequest `json:"httpRequest"` + GRPCRequest *GRPCRequest `json:"grpc"` +} + +type GRPCRequest struct { + ProtobufFile string `json:"protobufFile,omitempty"` } type HTTPRequest struct { @@ -45,16 +50,11 @@ type Output struct { } type TestSpec struct { - Selector Selector `json:"selector"` + Selector string `json:"selector"` Name string `json:"name,omitempty"` Assertions []string `json:"assertions"` } -type Selector struct { - Query string `json:"query"` - ParsedSelector SpanSelector `json:"parsedSelector"` -} - type SpanSelector struct { Filters []SelectorFilter `json:"filters"` PseudoClass *SelectorPseudoClass `json:"pseudoClass,omitempty"`