diff --git a/docs/services-architecture.md b/docs/services-architecture.md index 9b9af8fa..e91aecd8 100644 --- a/docs/services-architecture.md +++ b/docs/services-architecture.md @@ -9,12 +9,14 @@ It is divided into 2 parts: - `client`, part of cli app running on local - `server`, part of server app running on remote machine -Each part follows hexagonal architecture. It divides into following layers: +`server` should follows hexagonal architecture. It divides into following layers: - `application`, contains the application logic - `infrastructure`, contains the implementation of the ports and adapters - `input`, contains user interface/api adapters implementation, such as GRPC controllers, CLI command handlers and etc - `output`, contains adapters to external systems implementation, such as HTTP/GRPC client, repositories and etc +`client` should be simple wrapper over GRPC client, which is generated from protobuf definition. + Other: - `./.go` contains the service related commands - `./.proto` protobuf definition of client-server communication diff --git a/go.mod b/go.mod index f3f371e7..574997a3 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/google/subcommands v1.2.0 github.com/gowebpki/jcs v1.0.1 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0 + github.com/joho/godotenv v1.5.1 github.com/open-policy-agent/opa v0.67.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.20.2 @@ -24,12 +25,13 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 gocloud.dev v0.32.0 golang.org/x/sync v0.8.0 - golang.org/x/sys v0.24.0 + golang.org/x/sys v0.25.0 golang.org/x/term v0.22.0 google.golang.org/grpc v1.65.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 google.golang.org/protobuf v1.34.2 gopkg.in/ini.v1 v1.67.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -108,12 +110,11 @@ require ( golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.169.0 // indirect google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 282dcdfb..5a08c815 100644 --- a/go.sum +++ b/go.sum @@ -268,6 +268,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= @@ -503,8 +505,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -574,8 +576,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= diff --git a/services/localfile/README.md b/services/localfile/README.md new file mode 100644 index 00000000..74f1252c --- /dev/null +++ b/services/localfile/README.md @@ -0,0 +1,60 @@ +# Local file +Service to manipulate file on remote machines + +# Usage + +### sanssh file get-data +Get data from a file of specific format on a remote host by specified data key. + +```bash +sanssh file get-data [--format ] +``` +Where: +- `` common sanssh arguments +- `` is the path to the file on remote machine. If --format is not provided, format would be detected from file extension. +- `` is the key to read from the file. For different file formats it would require keys in different format + - for `yml`, key should be valid [YAMLPath](https://github.com/goccy/go-yaml/tree/master?tab=readme-ov-file#5-use-yamlpath) string + - for `dotenv`, key should be a name of variable +- `` is the format of the file, if specified it would override the format detected from file extension. Supported formats are: + - `yml` + - `dotenv` + +Examples: +```bash +# Get data from a yml file +sanssh --targets $TARGET file get-data /etc/config.yml "$.databases[0].host" +# Get data from a dotenv with explicitly specified format +sanssh --targets file get-data --format dotenv /etc/some-config "HOST" +``` + +### sanssh file set-data +Set data to a file of specific format on a remote host by specified data key. + +```bash +sanssh file set-data [--format ] [--value-type ] +``` +Where: +- `` common sanssh arguments +- `` is the path to the file on remote machine. If --format is not provided, format would be detected from file extension. +- `` is the key to set value in the file. For different file formats it would require keys in different format + - for `yml`, key should be valid [YAMLPath](https://github.com/goccy/go-yaml/tree/master?tab=readme-ov-file#5-use-yamlpath) string + - for `dotenv`, key should be a name of variable +- `` is the value to set in the file +- `` is the format of the file, if specified it would override the format detected from file extension. Supported formats are: + - `yml` + - `dotenv` +- `` is the type of value to set in the file. By default, `string`. Supported types are: + - `string` + - `int` + - `float` + - `bool` + +Examples: +```bash +# Set data to a yml file +sanssh --targets $TARGET file set-data /etc/config.yml "database.host" "localhost" +# Set data to a dotenv with explicitly specified format +sanssh --targets file set-data --format dotenv /etc/some-config "HOST" "localhost" +# Set data specified type +sanssh --targets file set-data --value-type int /etc/config.yml "database.port" 8080 +``` diff --git a/services/localfile/client/cli-controllers/file.utils.go b/services/localfile/client/cli-controllers/file.utils.go new file mode 100644 index 00000000..64f33b26 --- /dev/null +++ b/services/localfile/client/cli-controllers/file.utils.go @@ -0,0 +1,37 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 cli_controllers + +import ( + "errors" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + "path/filepath" +) + +func getFileTypeFromPath(filePath string) (pb.FileFormat, error) { + fileExt := filepath.Ext(filePath) + + switch fileExt { + case ".yml", ".yaml": + return pb.FileFormat_YML, nil + case ".env": + return pb.FileFormat_DOTENV, nil + default: + return pb.FileFormat_UNKNOWN, errors.New("file type is unsupported") + } +} diff --git a/services/localfile/client/cli-controllers/get-data.controller.go b/services/localfile/client/cli-controllers/get-data.controller.go new file mode 100644 index 00000000..3b7ccee0 --- /dev/null +++ b/services/localfile/client/cli-controllers/get-data.controller.go @@ -0,0 +1,151 @@ +/* Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 cli_controllers + +import ( + "context" + "errors" + "flag" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + "github.com/Snowflake-Labs/sansshell/services/util" + cliUtils "github.com/Snowflake-Labs/sansshell/services/util/cli" + "github.com/google/subcommands" + "google.golang.org/grpc/status" + "os" + "strings" +) + +// setDataCmd cli adapter for execution infrastructure implementation of [subcommands.Command] interface +type getDataCmd struct { + fileFormat pb.FileFormat + cliLogger cliUtils.StyledCliLogger +} + +func (*getDataCmd) Name() string { return "get-data" } +func (*getDataCmd) Synopsis() string { + return "Get data from file of specific format. Currently supported: yml, dotenv" +} +func (*getDataCmd) Usage() string { + return `get-data [--format=yml|dotenv] : + Get value by 'data-key' from file of file by 'file-path' of specific format. + Arguments: + - file-path - path to file with data + - data-key - key to read data from file. For different file format it should be: + - yml - YmlPath string + - dotenv - variable key + + Format could be detected from file extension or explicitly specified by --format flag. + + Flags: +` +} + +func (p *getDataCmd) SetFlags(f *flag.FlagSet) { + f.Func("format", "File format (Optional). Could be one of: yml, dotenv", func(s string) error { + lowerCased := strings.ToLower(s) + + switch lowerCased { + case "yml": + p.fileFormat = pb.FileFormat_YML + case "dotenv": + p.fileFormat = pb.FileFormat_DOTENV + default: + return errors.New("could be only yml or dotenv") + } + + return nil + }) +} + +// Execute is a method handle command execution. It adapter between cli and business logic +func (p *getDataCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + state := args[0].(*util.ExecuteState) + + if len(f.Args()) < 1 { + p.cliLogger.Errorc(cliUtils.RedText, "File path is missing.\n") + return subcommands.ExitUsageError + } + + if len(f.Args()) < 2 { + p.cliLogger.Errorc(cliUtils.RedText, "Property path is missing.\n") + return subcommands.ExitUsageError + } + + remoteFilePath := f.Arg(0) + dataKey := f.Arg(1) + + fileFormat := p.fileFormat + if fileFormat == pb.FileFormat_UNKNOWN { + fileFormatFromExt, err := getFileTypeFromPath(remoteFilePath) + if err != nil { + p.cliLogger.Errorfc(cliUtils.RedText, "Could not get file type from filepath: %s\n", err.Error()) + return subcommands.ExitUsageError + } + + fileFormat = fileFormatFromExt + } + + preloader := cliUtils.NewDotPreloader("Waiting for results from remote machines", util.IsStreamToTerminal(os.Stdout)) + client := pb.NewLocalFileClientProxy(state.Conn) + + preloader.Start() + responses, err := client.DataGetOneMany(ctx, &pb.DataGetRequest{ + Filename: remoteFilePath, + DataKey: dataKey, + FileFormat: fileFormat, + }) + + if err != nil { + preloader.Stop() + p.cliLogger.Errorfc(cliUtils.RedText, "Unexpected error: %s\n", err.Error()) + return subcommands.ExitFailure + } + + for resp := range responses { + preloader.Stop() + + targetLogger := cliUtils.NewStyledCliLogger(state.Out[resp.Index], state.Err[resp.Index], &cliUtils.CliLoggerOptions{ + ApplyStylingForErr: util.IsStreamToTerminal(state.Err[resp.Index]), + ApplyStylingForOut: util.IsStreamToTerminal(state.Out[resp.Index]), + }) + + if resp.Error != nil { + st, _ := status.FromError(resp.Error) + targetLogger.Errorfc(cliUtils.RedText, + "Failed to get value: %s\n", + st.Message(), + ) + continue + } + targetLogger.Infof("Value: %s\n", resp.Resp.Value) + + preloader.Start() + } + preloader.StopWith("Completed.\n") + + return subcommands.ExitSuccess +} + +func NewDataGetCmd() subcommands.Command { + return &getDataCmd{ + fileFormat: pb.FileFormat_UNKNOWN, + cliLogger: cliUtils.NewStyledCliLogger(os.Stdout, os.Stderr, &cliUtils.CliLoggerOptions{ + ApplyStylingForErr: util.IsStreamToTerminal(os.Stderr), + ApplyStylingForOut: util.IsStreamToTerminal(os.Stdout), + }), + } +} diff --git a/services/localfile/client/cli-controllers/set-data.controller.go b/services/localfile/client/cli-controllers/set-data.controller.go new file mode 100644 index 00000000..c4da6817 --- /dev/null +++ b/services/localfile/client/cli-controllers/set-data.controller.go @@ -0,0 +1,184 @@ +/* Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 cli_controllers + +import ( + "context" + "errors" + "flag" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + "github.com/Snowflake-Labs/sansshell/services/util" + cliUtils "github.com/Snowflake-Labs/sansshell/services/util/cli" + "github.com/google/subcommands" + "os" + "strings" +) + +// setDataCmd cli adapter for execution infrastructure implementation of [subcommands.Command] interface +type setDataCmd struct { + fileFormat pb.FileFormat + valueType pb.DataSetValueType + cliLogger cliUtils.StyledCliLogger +} + +func (*setDataCmd) Name() string { return "set-data" } +func (*setDataCmd) Synopsis() string { + return "Get data from file of specific format. Currently supported: yml, dotenv" +} +func (*setDataCmd) Usage() string { + return `sanssh file set-data [--format ] [--value-type ] + Where: + - common sanssh arguments + - is the path to the file on remote machine. If --format is not provided, format would be detected from file extension. + - is the key to set value in the file. For different file formats it would require keys in different format + - for "yml", key should be valid YAMLPath string + - for "dotenv", key should be a name of variable + - is the value to set in the file + + Flags: +` +} + +func (p *setDataCmd) SetFlags(f *flag.FlagSet) { + f.Func( + "value-type", + `type of value to set in the file. Supported types are: + - string (default) + - int + - float + - bool +`, + func(s string) error { + lowerCased := strings.ToLower(s) + switch lowerCased { + case "string": + p.valueType = pb.DataSetValueType_STRING_VAL + case "int": + p.valueType = pb.DataSetValueType_INT_VAL + case "float": + p.valueType = pb.DataSetValueType_FLOAT_VAL + case "bool": + p.valueType = pb.DataSetValueType_BOOL_VAL + default: + return errors.New("invalid value type") + } + + return nil + }) + + f.Func("format", "File format (Optional). Could be one of: yml, dotenv", func(s string) error { + lowerCased := strings.ToLower(s) + + switch lowerCased { + case "yml": + p.fileFormat = pb.FileFormat_YML + case "dotenv": + p.fileFormat = pb.FileFormat_DOTENV + default: + return errors.New("invalid file format") + } + + return nil + }) +} + +// Execute is a method handle command execution. It adapter between cli and business logic +func (p *setDataCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { + state := args[0].(*util.ExecuteState) + + if len(f.Args()) < 1 { + p.cliLogger.Errorc(cliUtils.RedText, "File path is missing.\n") + return subcommands.ExitUsageError + } + + if len(f.Args()) < 2 { + p.cliLogger.Errorc(cliUtils.RedText, "Data path is missing.\n") + return subcommands.ExitUsageError + } + + if len(f.Args()) < 3 { + p.cliLogger.Errorc(cliUtils.RedText, "Data value is missing.\n") + return subcommands.ExitUsageError + } + + remoteFilePath := f.Arg(0) + dataKey := f.Arg(1) + dataValue := f.Arg(2) + dataValueType := p.valueType + + fileFormat := p.fileFormat + if fileFormat == pb.FileFormat_UNKNOWN { + fileFormatFromExt, err := getFileTypeFromPath(remoteFilePath) + if err != nil { + p.cliLogger.Errorfc(cliUtils.RedText, "Could not set data in filepath: %s\n", err.Error()) + return subcommands.ExitUsageError + } + + fileFormat = fileFormatFromExt + } + + preloader := cliUtils.NewDotPreloader("Waiting for results from remote machines", util.IsStreamToTerminal(os.Stdout)) + client := pb.NewLocalFileClientProxy(state.Conn) + + p.cliLogger.Infof("Setting data in file %s\n", dataValueType) + preloader.Start() + + responses, err := client.DataSetOneMany(ctx, &pb.DataSetRequest{ + Filename: remoteFilePath, + DataKey: dataKey, + FileFormat: fileFormat, + Value: dataValue, + ValueType: dataValueType, + }) + + if err != nil { + preloader.Stop() + p.cliLogger.Errorfc(cliUtils.RedText, "Unexpected error: %s\n", err.Error()) + return subcommands.ExitFailure + } + + for resp := range responses { + preloader.Stop() + + targetLogger := cliUtils.NewStyledCliLogger(state.Out[resp.Index], state.Err[resp.Index], &cliUtils.CliLoggerOptions{ + ApplyStylingForErr: util.IsStreamToTerminal(state.Err[resp.Index]), + ApplyStylingForOut: util.IsStreamToTerminal(state.Out[resp.Index]), + }) + + status := cliUtils.CGreen("Ok") + if resp.Error != nil { + status = cliUtils.Colorizef(cliUtils.RedText, "Failed due to error - %s", resp.Error.Error()) + } + + targetLogger.Infof("Value set status: %s\n", status) + preloader.Start() + } + preloader.StopWith("Completed.\n") + + return subcommands.ExitSuccess +} + +func NewDataSetCmd() subcommands.Command { + return &setDataCmd{ + fileFormat: pb.FileFormat_UNKNOWN, + valueType: pb.DataSetValueType_STRING_VAL, + cliLogger: cliUtils.NewStyledCliLogger(os.Stdout, os.Stderr, &cliUtils.CliLoggerOptions{ + ApplyStylingForErr: util.IsStreamToTerminal(os.Stderr), + ApplyStylingForOut: util.IsStreamToTerminal(os.Stdout), + }), + } +} diff --git a/services/localfile/client/client.go b/services/localfile/client/client.go index cc9ae361..302858d8 100644 --- a/services/localfile/client/client.go +++ b/services/localfile/client/client.go @@ -21,6 +21,7 @@ import ( "context" "flag" "fmt" + cli_controllers "github.com/Snowflake-Labs/sansshell/services/localfile/client/cli-controllers" "io" "io/fs" "os" @@ -44,6 +45,9 @@ func init() { func (*fileCmd) GetSubpackage(f *flag.FlagSet) *subcommands.Commander { c := client.SetupSubpackage(subPackage, f) + + dataGetCmd := cli_controllers.NewDataGetCmd() + dataSetCmd := cli_controllers.NewDataSetCmd() c.Register(&chgrpCmd{}, "") c.Register(&chmodCmd{}, "") c.Register(&chownCmd{}, "") @@ -51,6 +55,8 @@ func (*fileCmd) GetSubpackage(f *flag.FlagSet) *subcommands.Commander { c.Register(&immutableCmd{}, "") c.Register(&lsCmd{}, "") c.Register(&readCmd{}, "") + c.Register(dataGetCmd, "") + c.Register(dataSetCmd, "") c.Register(&readlinkCmd{}, "") c.Register(&renameCmd{}, "") c.Register(&rmCmd{}, "") diff --git a/services/localfile/localfile.pb.go b/services/localfile/localfile.pb.go index 2e317e38..8248b21d 100644 --- a/services/localfile/localfile.pb.go +++ b/services/localfile/localfile.pb.go @@ -15,8 +15,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 -// protoc v5.27.1 +// protoc-gen-go v1.34.2 +// protoc v5.28.1 // source: localfile.proto package localfile @@ -94,6 +94,112 @@ func (SumType) EnumDescriptor() ([]byte, []int) { return file_localfile_proto_rawDescGZIP(), []int{0} } +// FileFormat enum what represents file formats +type FileFormat int32 + +const ( + FileFormat_UNKNOWN FileFormat = 0 + FileFormat_YML FileFormat = 1 + FileFormat_DOTENV FileFormat = 2 +) + +// Enum value maps for FileFormat. +var ( + FileFormat_name = map[int32]string{ + 0: "UNKNOWN", + 1: "YML", + 2: "DOTENV", + } + FileFormat_value = map[string]int32{ + "UNKNOWN": 0, + "YML": 1, + "DOTENV": 2, + } +) + +func (x FileFormat) Enum() *FileFormat { + p := new(FileFormat) + *p = x + return p +} + +func (x FileFormat) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (FileFormat) Descriptor() protoreflect.EnumDescriptor { + return file_localfile_proto_enumTypes[1].Descriptor() +} + +func (FileFormat) Type() protoreflect.EnumType { + return &file_localfile_proto_enumTypes[1] +} + +func (x FileFormat) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use FileFormat.Descriptor instead. +func (FileFormat) EnumDescriptor() ([]byte, []int) { + return file_localfile_proto_rawDescGZIP(), []int{1} +} + +// DataSetValueType enum what represents how represent value in a file +type DataSetValueType int32 + +const ( + DataSetValueType_UNKNOWN_VAL DataSetValueType = 0 + DataSetValueType_STRING_VAL DataSetValueType = 1 + DataSetValueType_INT_VAL DataSetValueType = 2 + DataSetValueType_FLOAT_VAL DataSetValueType = 3 + DataSetValueType_BOOL_VAL DataSetValueType = 4 +) + +// Enum value maps for DataSetValueType. +var ( + DataSetValueType_name = map[int32]string{ + 0: "UNKNOWN_VAL", + 1: "STRING_VAL", + 2: "INT_VAL", + 3: "FLOAT_VAL", + 4: "BOOL_VAL", + } + DataSetValueType_value = map[string]int32{ + "UNKNOWN_VAL": 0, + "STRING_VAL": 1, + "INT_VAL": 2, + "FLOAT_VAL": 3, + "BOOL_VAL": 4, + } +) + +func (x DataSetValueType) Enum() *DataSetValueType { + p := new(DataSetValueType) + *p = x + return p +} + +func (x DataSetValueType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DataSetValueType) Descriptor() protoreflect.EnumDescriptor { + return file_localfile_proto_enumTypes[2].Descriptor() +} + +func (DataSetValueType) Type() protoreflect.EnumType { + return &file_localfile_proto_enumTypes[2] +} + +func (x DataSetValueType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DataSetValueType.Descriptor instead. +func (DataSetValueType) EnumDescriptor() ([]byte, []int) { + return file_localfile_proto_rawDescGZIP(), []int{2} +} + // ReadActionRequest indicates the type of read we're performing. // Either a file read which then terminates or a tail based read that // continues forever (i.e. as tail -f on the command line would do). @@ -1570,6 +1676,198 @@ func (x *MkdirRequest) GetDirAttrs() *FileAttributes { return nil } +// DataGetRequest is request to read data from a specific file in specific format +type DataGetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` + FileFormat FileFormat `protobuf:"varint,2,opt,name=file_format,json=fileFormat,proto3,enum=LocalFile.FileFormat" json:"file_format,omitempty"` + DataKey string `protobuf:"bytes,3,opt,name=data_key,json=dataKey,proto3" json:"data_key,omitempty"` +} + +func (x *DataGetRequest) Reset() { + *x = DataGetRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_localfile_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataGetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataGetRequest) ProtoMessage() {} + +func (x *DataGetRequest) ProtoReflect() protoreflect.Message { + mi := &file_localfile_proto_msgTypes[23] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataGetRequest.ProtoReflect.Descriptor instead. +func (*DataGetRequest) Descriptor() ([]byte, []int) { + return file_localfile_proto_rawDescGZIP(), []int{23} +} + +func (x *DataGetRequest) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *DataGetRequest) GetFileFormat() FileFormat { + if x != nil { + return x.FileFormat + } + return FileFormat_UNKNOWN +} + +func (x *DataGetRequest) GetDataKey() string { + if x != nil { + return x.DataKey + } + return "" +} + +// DataGetReply contains the value of read from file +type DataGetReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *DataGetReply) Reset() { + *x = DataGetReply{} + if protoimpl.UnsafeEnabled { + mi := &file_localfile_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataGetReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataGetReply) ProtoMessage() {} + +func (x *DataGetReply) ProtoReflect() protoreflect.Message { + mi := &file_localfile_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataGetReply.ProtoReflect.Descriptor instead. +func (*DataGetReply) Descriptor() ([]byte, []int) { + return file_localfile_proto_rawDescGZIP(), []int{24} +} + +func (x *DataGetReply) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +// DataSetRequest is request to set property in a specific file in specific format +type DataSetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"` + FileFormat FileFormat `protobuf:"varint,2,opt,name=file_format,json=fileFormat,proto3,enum=LocalFile.FileFormat" json:"file_format,omitempty"` + DataKey string `protobuf:"bytes,3,opt,name=data_key,json=dataKey,proto3" json:"data_key,omitempty"` + Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` + ValueType DataSetValueType `protobuf:"varint,5,opt,name=value_type,json=valueType,proto3,enum=LocalFile.DataSetValueType" json:"value_type,omitempty"` +} + +func (x *DataSetRequest) Reset() { + *x = DataSetRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_localfile_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataSetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataSetRequest) ProtoMessage() {} + +func (x *DataSetRequest) ProtoReflect() protoreflect.Message { + mi := &file_localfile_proto_msgTypes[25] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataSetRequest.ProtoReflect.Descriptor instead. +func (*DataSetRequest) Descriptor() ([]byte, []int) { + return file_localfile_proto_rawDescGZIP(), []int{25} +} + +func (x *DataSetRequest) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *DataSetRequest) GetFileFormat() FileFormat { + if x != nil { + return x.FileFormat + } + return FileFormat_UNKNOWN +} + +func (x *DataSetRequest) GetDataKey() string { + if x != nil { + return x.DataKey + } + return "" +} + +func (x *DataSetRequest) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *DataSetRequest) GetValueType() DataSetValueType { + if x != nil { + return x.ValueType + } + return DataSetValueType_UNKNOWN_VAL +} + var File_localfile_proto protoreflect.FileDescriptor var file_localfile_proto_rawDesc = []byte{ @@ -1705,70 +2003,111 @@ var file_localfile_proto_rawDesc = []byte{ 0x09, 0x64, 0x69, 0x72, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x08, 0x64, 0x69, 0x72, - 0x41, 0x74, 0x74, 0x72, 0x73, 0x2a, 0x77, 0x0a, 0x07, 0x53, 0x75, 0x6d, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x14, 0x0a, 0x10, 0x53, 0x55, 0x4d, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x4b, - 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x55, 0x4d, 0x5f, 0x54, 0x59, - 0x50, 0x45, 0x5f, 0x43, 0x52, 0x43, 0x33, 0x32, 0x49, 0x45, 0x45, 0x45, 0x10, 0x01, 0x12, 0x10, - 0x0a, 0x0c, 0x53, 0x55, 0x4d, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x44, 0x35, 0x10, 0x02, - 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x55, 0x4d, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x48, 0x41, - 0x32, 0x35, 0x36, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x53, 0x55, 0x4d, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x53, 0x48, 0x41, 0x35, 0x31, 0x32, 0x5f, 0x32, 0x35, 0x36, 0x10, 0x04, 0x32, 0xb6, - 0x06, 0x0a, 0x09, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x3e, 0x0a, 0x04, - 0x52, 0x65, 0x61, 0x64, 0x12, 0x1c, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, - 0x2e, 0x52, 0x65, 0x61, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, - 0x65, 0x61, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3a, 0x0a, 0x04, - 0x53, 0x74, 0x61, 0x74, 0x12, 0x16, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, - 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x4c, - 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x65, 0x70, - 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x37, 0x0a, 0x03, 0x53, 0x75, 0x6d, 0x12, - 0x15, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x53, 0x75, 0x6d, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, - 0x6c, 0x65, 0x2e, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x30, - 0x01, 0x12, 0x3c, 0x0a, 0x05, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x4c, 0x6f, 0x63, - 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x28, 0x01, 0x12, - 0x38, 0x0a, 0x04, 0x43, 0x6f, 0x70, 0x79, 0x12, 0x16, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, - 0x69, 0x6c, 0x65, 0x2e, 0x43, 0x6f, 0x70, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x04, 0x4c, 0x69, 0x73, - 0x74, 0x12, 0x16, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x4c, 0x6f, 0x63, 0x61, - 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, - 0x00, 0x30, 0x01, 0x12, 0x52, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, - 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x53, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x02, 0x52, 0x6d, 0x12, 0x14, 0x2e, - 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x6d, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3a, 0x0a, - 0x05, 0x52, 0x6d, 0x64, 0x69, 0x72, 0x12, 0x17, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, - 0x6c, 0x65, 0x2e, 0x52, 0x6d, 0x64, 0x69, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x06, 0x52, 0x65, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x18, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, - 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x41, 0x74, 0x74, 0x72, 0x73, 0x22, 0x7f, 0x0a, 0x0e, 0x44, 0x61, 0x74, 0x61, 0x47, 0x65, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x0b, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, + 0x0a, 0x66, 0x69, 0x6c, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x64, + 0x61, 0x74, 0x61, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, + 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x22, 0x24, 0x0a, 0x0c, 0x44, 0x61, 0x74, 0x61, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xd1, 0x01, 0x0a, + 0x0e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x0b, 0x66, + 0x69, 0x6c, 0x65, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x15, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x46, 0x69, 0x6c, + 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x65, 0x46, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, + 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x79, 0x70, 0x65, + 0x2a, 0x77, 0x0a, 0x07, 0x53, 0x75, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x53, + 0x55, 0x4d, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x55, 0x4d, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, + 0x43, 0x33, 0x32, 0x49, 0x45, 0x45, 0x45, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x55, 0x4d, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x44, 0x35, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, + 0x55, 0x4d, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x10, 0x03, + 0x12, 0x17, 0x0a, 0x13, 0x53, 0x55, 0x4d, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x48, 0x41, + 0x35, 0x31, 0x32, 0x5f, 0x32, 0x35, 0x36, 0x10, 0x04, 0x2a, 0x2e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, + 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x59, 0x4d, 0x4c, 0x10, 0x01, 0x12, 0x0a, 0x0a, + 0x06, 0x44, 0x4f, 0x54, 0x45, 0x4e, 0x56, 0x10, 0x02, 0x2a, 0x5d, 0x0a, 0x10, 0x44, 0x61, 0x74, + 0x61, 0x53, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, + 0x0b, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x56, 0x41, 0x4c, 0x10, 0x00, 0x12, 0x0e, + 0x0a, 0x0a, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x5f, 0x56, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x0b, + 0x0a, 0x07, 0x49, 0x4e, 0x54, 0x5f, 0x56, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x46, + 0x4c, 0x4f, 0x41, 0x54, 0x5f, 0x56, 0x41, 0x4c, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, + 0x4f, 0x4c, 0x5f, 0x56, 0x41, 0x4c, 0x10, 0x04, 0x32, 0xb7, 0x07, 0x0a, 0x09, 0x4c, 0x6f, 0x63, + 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x3e, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1c, + 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x4c, + 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x70, + 0x6c, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3a, 0x0a, 0x04, 0x53, 0x74, 0x61, 0x74, 0x12, 0x16, + 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, + 0x6c, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, + 0x30, 0x01, 0x12, 0x37, 0x0a, 0x03, 0x53, 0x75, 0x6d, 0x12, 0x15, 0x2e, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x13, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x53, 0x75, 0x6d, + 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x3c, 0x0a, 0x05, 0x57, + 0x72, 0x69, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, + 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x08, 0x52, 0x65, 0x61, 0x64, 0x6c, - 0x69, 0x6e, 0x6b, 0x12, 0x1a, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, - 0x52, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x18, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, - 0x6c, 0x69, 0x6e, 0x6b, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x07, 0x53, - 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x19, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, - 0x6c, 0x65, 0x2e, 0x53, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x05, 0x4d, - 0x6b, 0x64, 0x69, 0x72, 0x12, 0x17, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, - 0x2e, 0x4d, 0x6b, 0x64, 0x69, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x28, 0x01, 0x12, 0x38, 0x0a, 0x04, 0x43, 0x6f, 0x70, + 0x79, 0x12, 0x16, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x43, 0x6f, + 0x70, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x00, 0x12, 0x38, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x4c, 0x6f, + 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x52, 0x0a, + 0x11, 0x53, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x12, 0x23, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x53, + 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0x00, 0x12, 0x34, 0x0a, 0x02, 0x52, 0x6d, 0x12, 0x14, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, + 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x53, 0x6e, 0x6f, 0x77, 0x66, 0x6c, 0x61, 0x6b, 0x65, 0x2d, - 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x73, 0x61, 0x6e, 0x73, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x2f, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x66, 0x69, 0x6c, - 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x05, 0x52, 0x6d, 0x64, 0x69, 0x72, + 0x12, 0x17, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x6d, 0x64, + 0x69, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x06, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x2e, + 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0x00, 0x12, 0x42, 0x0a, 0x08, 0x52, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x1a, 0x2e, + 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x6c, 0x69, + 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x6b, 0x52, 0x65, + 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x07, 0x53, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, + 0x12, 0x19, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x53, 0x79, 0x6d, + 0x6c, 0x69, 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3a, 0x0a, 0x05, 0x4d, 0x6b, 0x64, 0x69, 0x72, 0x12, 0x17, + 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x4d, 0x6b, 0x64, 0x69, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0x00, 0x12, 0x3f, 0x0a, 0x07, 0x44, 0x61, 0x74, 0x61, 0x47, 0x65, 0x74, 0x12, 0x19, 0x2e, 0x4c, + 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x47, 0x65, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, + 0x69, 0x6c, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x07, 0x44, 0x61, 0x74, 0x61, 0x53, 0x65, 0x74, 0x12, 0x19, 0x2e, + 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x65, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x22, 0x00, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x53, 0x6e, 0x6f, 0x77, 0x66, 0x6c, 0x61, 0x6b, 0x65, 0x2d, 0x4c, 0x61, 0x62, 0x73, 0x2f, + 0x73, 0x61, 0x6e, 0x73, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x66, 0x69, 0x6c, 0x65, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1783,80 +2122,92 @@ func file_localfile_proto_rawDescGZIP() []byte { return file_localfile_proto_rawDescData } -var file_localfile_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_localfile_proto_msgTypes = make([]protoimpl.MessageInfo, 23) -var file_localfile_proto_goTypes = []interface{}{ +var file_localfile_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_localfile_proto_msgTypes = make([]protoimpl.MessageInfo, 26) +var file_localfile_proto_goTypes = []any{ (SumType)(0), // 0: LocalFile.SumType - (*ReadActionRequest)(nil), // 1: LocalFile.ReadActionRequest - (*ReadRequest)(nil), // 2: LocalFile.ReadRequest - (*TailRequest)(nil), // 3: LocalFile.TailRequest - (*ReadReply)(nil), // 4: LocalFile.ReadReply - (*StatRequest)(nil), // 5: LocalFile.StatRequest - (*StatReply)(nil), // 6: LocalFile.StatReply - (*SumRequest)(nil), // 7: LocalFile.SumRequest - (*SumReply)(nil), // 8: LocalFile.SumReply - (*FileAttribute)(nil), // 9: LocalFile.FileAttribute - (*FileAttributes)(nil), // 10: LocalFile.FileAttributes - (*FileWrite)(nil), // 11: LocalFile.FileWrite - (*WriteRequest)(nil), // 12: LocalFile.WriteRequest - (*CopyRequest)(nil), // 13: LocalFile.CopyRequest - (*ListRequest)(nil), // 14: LocalFile.ListRequest - (*ListReply)(nil), // 15: LocalFile.ListReply - (*SetFileAttributesRequest)(nil), // 16: LocalFile.SetFileAttributesRequest - (*RmRequest)(nil), // 17: LocalFile.RmRequest - (*RmdirRequest)(nil), // 18: LocalFile.RmdirRequest - (*RenameRequest)(nil), // 19: LocalFile.RenameRequest - (*ReadlinkRequest)(nil), // 20: LocalFile.ReadlinkRequest - (*ReadlinkReply)(nil), // 21: LocalFile.ReadlinkReply - (*SymlinkRequest)(nil), // 22: LocalFile.SymlinkRequest - (*MkdirRequest)(nil), // 23: LocalFile.MkdirRequest - (*timestamppb.Timestamp)(nil), // 24: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 25: google.protobuf.Empty + (FileFormat)(0), // 1: LocalFile.FileFormat + (DataSetValueType)(0), // 2: LocalFile.DataSetValueType + (*ReadActionRequest)(nil), // 3: LocalFile.ReadActionRequest + (*ReadRequest)(nil), // 4: LocalFile.ReadRequest + (*TailRequest)(nil), // 5: LocalFile.TailRequest + (*ReadReply)(nil), // 6: LocalFile.ReadReply + (*StatRequest)(nil), // 7: LocalFile.StatRequest + (*StatReply)(nil), // 8: LocalFile.StatReply + (*SumRequest)(nil), // 9: LocalFile.SumRequest + (*SumReply)(nil), // 10: LocalFile.SumReply + (*FileAttribute)(nil), // 11: LocalFile.FileAttribute + (*FileAttributes)(nil), // 12: LocalFile.FileAttributes + (*FileWrite)(nil), // 13: LocalFile.FileWrite + (*WriteRequest)(nil), // 14: LocalFile.WriteRequest + (*CopyRequest)(nil), // 15: LocalFile.CopyRequest + (*ListRequest)(nil), // 16: LocalFile.ListRequest + (*ListReply)(nil), // 17: LocalFile.ListReply + (*SetFileAttributesRequest)(nil), // 18: LocalFile.SetFileAttributesRequest + (*RmRequest)(nil), // 19: LocalFile.RmRequest + (*RmdirRequest)(nil), // 20: LocalFile.RmdirRequest + (*RenameRequest)(nil), // 21: LocalFile.RenameRequest + (*ReadlinkRequest)(nil), // 22: LocalFile.ReadlinkRequest + (*ReadlinkReply)(nil), // 23: LocalFile.ReadlinkReply + (*SymlinkRequest)(nil), // 24: LocalFile.SymlinkRequest + (*MkdirRequest)(nil), // 25: LocalFile.MkdirRequest + (*DataGetRequest)(nil), // 26: LocalFile.DataGetRequest + (*DataGetReply)(nil), // 27: LocalFile.DataGetReply + (*DataSetRequest)(nil), // 28: LocalFile.DataSetRequest + (*timestamppb.Timestamp)(nil), // 29: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 30: google.protobuf.Empty } var file_localfile_proto_depIdxs = []int32{ - 2, // 0: LocalFile.ReadActionRequest.file:type_name -> LocalFile.ReadRequest - 3, // 1: LocalFile.ReadActionRequest.tail:type_name -> LocalFile.TailRequest - 24, // 2: LocalFile.StatReply.modtime:type_name -> google.protobuf.Timestamp + 4, // 0: LocalFile.ReadActionRequest.file:type_name -> LocalFile.ReadRequest + 5, // 1: LocalFile.ReadActionRequest.tail:type_name -> LocalFile.TailRequest + 29, // 2: LocalFile.StatReply.modtime:type_name -> google.protobuf.Timestamp 0, // 3: LocalFile.SumRequest.sum_type:type_name -> LocalFile.SumType 0, // 4: LocalFile.SumReply.sum_type:type_name -> LocalFile.SumType - 9, // 5: LocalFile.FileAttributes.attributes:type_name -> LocalFile.FileAttribute - 10, // 6: LocalFile.FileWrite.attrs:type_name -> LocalFile.FileAttributes - 11, // 7: LocalFile.WriteRequest.description:type_name -> LocalFile.FileWrite - 11, // 8: LocalFile.CopyRequest.destination:type_name -> LocalFile.FileWrite - 6, // 9: LocalFile.ListReply.entry:type_name -> LocalFile.StatReply - 10, // 10: LocalFile.SetFileAttributesRequest.attrs:type_name -> LocalFile.FileAttributes - 10, // 11: LocalFile.MkdirRequest.dir_attrs:type_name -> LocalFile.FileAttributes - 1, // 12: LocalFile.LocalFile.Read:input_type -> LocalFile.ReadActionRequest - 5, // 13: LocalFile.LocalFile.Stat:input_type -> LocalFile.StatRequest - 7, // 14: LocalFile.LocalFile.Sum:input_type -> LocalFile.SumRequest - 12, // 15: LocalFile.LocalFile.Write:input_type -> LocalFile.WriteRequest - 13, // 16: LocalFile.LocalFile.Copy:input_type -> LocalFile.CopyRequest - 14, // 17: LocalFile.LocalFile.List:input_type -> LocalFile.ListRequest - 16, // 18: LocalFile.LocalFile.SetFileAttributes:input_type -> LocalFile.SetFileAttributesRequest - 17, // 19: LocalFile.LocalFile.Rm:input_type -> LocalFile.RmRequest - 18, // 20: LocalFile.LocalFile.Rmdir:input_type -> LocalFile.RmdirRequest - 19, // 21: LocalFile.LocalFile.Rename:input_type -> LocalFile.RenameRequest - 20, // 22: LocalFile.LocalFile.Readlink:input_type -> LocalFile.ReadlinkRequest - 22, // 23: LocalFile.LocalFile.Symlink:input_type -> LocalFile.SymlinkRequest - 23, // 24: LocalFile.LocalFile.Mkdir:input_type -> LocalFile.MkdirRequest - 4, // 25: LocalFile.LocalFile.Read:output_type -> LocalFile.ReadReply - 6, // 26: LocalFile.LocalFile.Stat:output_type -> LocalFile.StatReply - 8, // 27: LocalFile.LocalFile.Sum:output_type -> LocalFile.SumReply - 25, // 28: LocalFile.LocalFile.Write:output_type -> google.protobuf.Empty - 25, // 29: LocalFile.LocalFile.Copy:output_type -> google.protobuf.Empty - 15, // 30: LocalFile.LocalFile.List:output_type -> LocalFile.ListReply - 25, // 31: LocalFile.LocalFile.SetFileAttributes:output_type -> google.protobuf.Empty - 25, // 32: LocalFile.LocalFile.Rm:output_type -> google.protobuf.Empty - 25, // 33: LocalFile.LocalFile.Rmdir:output_type -> google.protobuf.Empty - 25, // 34: LocalFile.LocalFile.Rename:output_type -> google.protobuf.Empty - 21, // 35: LocalFile.LocalFile.Readlink:output_type -> LocalFile.ReadlinkReply - 25, // 36: LocalFile.LocalFile.Symlink:output_type -> google.protobuf.Empty - 25, // 37: LocalFile.LocalFile.Mkdir:output_type -> google.protobuf.Empty - 25, // [25:38] is the sub-list for method output_type - 12, // [12:25] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 11, // 5: LocalFile.FileAttributes.attributes:type_name -> LocalFile.FileAttribute + 12, // 6: LocalFile.FileWrite.attrs:type_name -> LocalFile.FileAttributes + 13, // 7: LocalFile.WriteRequest.description:type_name -> LocalFile.FileWrite + 13, // 8: LocalFile.CopyRequest.destination:type_name -> LocalFile.FileWrite + 8, // 9: LocalFile.ListReply.entry:type_name -> LocalFile.StatReply + 12, // 10: LocalFile.SetFileAttributesRequest.attrs:type_name -> LocalFile.FileAttributes + 12, // 11: LocalFile.MkdirRequest.dir_attrs:type_name -> LocalFile.FileAttributes + 1, // 12: LocalFile.DataGetRequest.file_format:type_name -> LocalFile.FileFormat + 1, // 13: LocalFile.DataSetRequest.file_format:type_name -> LocalFile.FileFormat + 2, // 14: LocalFile.DataSetRequest.value_type:type_name -> LocalFile.DataSetValueType + 3, // 15: LocalFile.LocalFile.Read:input_type -> LocalFile.ReadActionRequest + 7, // 16: LocalFile.LocalFile.Stat:input_type -> LocalFile.StatRequest + 9, // 17: LocalFile.LocalFile.Sum:input_type -> LocalFile.SumRequest + 14, // 18: LocalFile.LocalFile.Write:input_type -> LocalFile.WriteRequest + 15, // 19: LocalFile.LocalFile.Copy:input_type -> LocalFile.CopyRequest + 16, // 20: LocalFile.LocalFile.List:input_type -> LocalFile.ListRequest + 18, // 21: LocalFile.LocalFile.SetFileAttributes:input_type -> LocalFile.SetFileAttributesRequest + 19, // 22: LocalFile.LocalFile.Rm:input_type -> LocalFile.RmRequest + 20, // 23: LocalFile.LocalFile.Rmdir:input_type -> LocalFile.RmdirRequest + 21, // 24: LocalFile.LocalFile.Rename:input_type -> LocalFile.RenameRequest + 22, // 25: LocalFile.LocalFile.Readlink:input_type -> LocalFile.ReadlinkRequest + 24, // 26: LocalFile.LocalFile.Symlink:input_type -> LocalFile.SymlinkRequest + 25, // 27: LocalFile.LocalFile.Mkdir:input_type -> LocalFile.MkdirRequest + 26, // 28: LocalFile.LocalFile.DataGet:input_type -> LocalFile.DataGetRequest + 28, // 29: LocalFile.LocalFile.DataSet:input_type -> LocalFile.DataSetRequest + 6, // 30: LocalFile.LocalFile.Read:output_type -> LocalFile.ReadReply + 8, // 31: LocalFile.LocalFile.Stat:output_type -> LocalFile.StatReply + 10, // 32: LocalFile.LocalFile.Sum:output_type -> LocalFile.SumReply + 30, // 33: LocalFile.LocalFile.Write:output_type -> google.protobuf.Empty + 30, // 34: LocalFile.LocalFile.Copy:output_type -> google.protobuf.Empty + 17, // 35: LocalFile.LocalFile.List:output_type -> LocalFile.ListReply + 30, // 36: LocalFile.LocalFile.SetFileAttributes:output_type -> google.protobuf.Empty + 30, // 37: LocalFile.LocalFile.Rm:output_type -> google.protobuf.Empty + 30, // 38: LocalFile.LocalFile.Rmdir:output_type -> google.protobuf.Empty + 30, // 39: LocalFile.LocalFile.Rename:output_type -> google.protobuf.Empty + 23, // 40: LocalFile.LocalFile.Readlink:output_type -> LocalFile.ReadlinkReply + 30, // 41: LocalFile.LocalFile.Symlink:output_type -> google.protobuf.Empty + 30, // 42: LocalFile.LocalFile.Mkdir:output_type -> google.protobuf.Empty + 27, // 43: LocalFile.LocalFile.DataGet:output_type -> LocalFile.DataGetReply + 30, // 44: LocalFile.LocalFile.DataSet:output_type -> google.protobuf.Empty + 30, // [30:45] is the sub-list for method output_type + 15, // [15:30] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_localfile_proto_init() } @@ -1865,7 +2216,7 @@ func file_localfile_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_localfile_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*ReadActionRequest); i { case 0: return &v.state @@ -1877,7 +2228,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*ReadRequest); i { case 0: return &v.state @@ -1889,7 +2240,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*TailRequest); i { case 0: return &v.state @@ -1901,7 +2252,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*ReadReply); i { case 0: return &v.state @@ -1913,7 +2264,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*StatRequest); i { case 0: return &v.state @@ -1925,7 +2276,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*StatReply); i { case 0: return &v.state @@ -1937,7 +2288,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*SumRequest); i { case 0: return &v.state @@ -1949,7 +2300,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*SumReply); i { case 0: return &v.state @@ -1961,7 +2312,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*FileAttribute); i { case 0: return &v.state @@ -1973,7 +2324,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*FileAttributes); i { case 0: return &v.state @@ -1985,7 +2336,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*FileWrite); i { case 0: return &v.state @@ -1997,7 +2348,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[11].Exporter = func(v any, i int) any { switch v := v.(*WriteRequest); i { case 0: return &v.state @@ -2009,7 +2360,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[12].Exporter = func(v any, i int) any { switch v := v.(*CopyRequest); i { case 0: return &v.state @@ -2021,7 +2372,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[13].Exporter = func(v any, i int) any { switch v := v.(*ListRequest); i { case 0: return &v.state @@ -2033,7 +2384,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[14].Exporter = func(v any, i int) any { switch v := v.(*ListReply); i { case 0: return &v.state @@ -2045,7 +2396,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[15].Exporter = func(v any, i int) any { switch v := v.(*SetFileAttributesRequest); i { case 0: return &v.state @@ -2057,7 +2408,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[16].Exporter = func(v any, i int) any { switch v := v.(*RmRequest); i { case 0: return &v.state @@ -2069,7 +2420,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[17].Exporter = func(v any, i int) any { switch v := v.(*RmdirRequest); i { case 0: return &v.state @@ -2081,7 +2432,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[18].Exporter = func(v any, i int) any { switch v := v.(*RenameRequest); i { case 0: return &v.state @@ -2093,7 +2444,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[19].Exporter = func(v any, i int) any { switch v := v.(*ReadlinkRequest); i { case 0: return &v.state @@ -2105,7 +2456,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[20].Exporter = func(v any, i int) any { switch v := v.(*ReadlinkReply); i { case 0: return &v.state @@ -2117,7 +2468,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[21].Exporter = func(v any, i int) any { switch v := v.(*SymlinkRequest); i { case 0: return &v.state @@ -2129,7 +2480,7 @@ func file_localfile_proto_init() { return nil } } - file_localfile_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + file_localfile_proto_msgTypes[22].Exporter = func(v any, i int) any { switch v := v.(*MkdirRequest); i { case 0: return &v.state @@ -2141,12 +2492,48 @@ func file_localfile_proto_init() { return nil } } + file_localfile_proto_msgTypes[23].Exporter = func(v any, i int) any { + switch v := v.(*DataGetRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_localfile_proto_msgTypes[24].Exporter = func(v any, i int) any { + switch v := v.(*DataGetReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_localfile_proto_msgTypes[25].Exporter = func(v any, i int) any { + switch v := v.(*DataSetRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } - file_localfile_proto_msgTypes[0].OneofWrappers = []interface{}{ + file_localfile_proto_msgTypes[0].OneofWrappers = []any{ (*ReadActionRequest_File)(nil), (*ReadActionRequest_Tail)(nil), } - file_localfile_proto_msgTypes[8].OneofWrappers = []interface{}{ + file_localfile_proto_msgTypes[8].OneofWrappers = []any{ (*FileAttribute_Uid)(nil), (*FileAttribute_Username)(nil), (*FileAttribute_Gid)(nil), @@ -2154,7 +2541,7 @@ func file_localfile_proto_init() { (*FileAttribute_Mode)(nil), (*FileAttribute_Immutable)(nil), } - file_localfile_proto_msgTypes[11].OneofWrappers = []interface{}{ + file_localfile_proto_msgTypes[11].OneofWrappers = []any{ (*WriteRequest_Description)(nil), (*WriteRequest_Contents)(nil), } @@ -2163,8 +2550,8 @@ func file_localfile_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_localfile_proto_rawDesc, - NumEnums: 1, - NumMessages: 23, + NumEnums: 3, + NumMessages: 26, NumExtensions: 0, NumServices: 1, }, diff --git a/services/localfile/localfile.proto b/services/localfile/localfile.proto index bdd5e430..84121fc5 100644 --- a/services/localfile/localfile.proto +++ b/services/localfile/localfile.proto @@ -68,6 +68,12 @@ service LocalFile { // Mkdir create a new directory. rpc Mkdir(MkdirRequest) returns (google.protobuf.Empty) {} + + // Get data from a file of specified type by provided key + rpc DataGet(DataGetRequest) returns (DataGetReply) {} + + // Set data value to a file of specified type by provided key + rpc DataSet(DataSetRequest) returns (google.protobuf.Empty) {} } // ReadActionRequest indicates the type of read we're performing. @@ -275,3 +281,38 @@ message SymlinkRequest { message MkdirRequest { FileAttributes dir_attrs = 1; } + +// FileFormat enum what represents file formats +enum FileFormat { + UNKNOWN = 0; + YML = 1; + DOTENV = 2; +} + +// DataGetRequest is request to read data from a specific file in specific format +message DataGetRequest { + string filename = 1; + FileFormat file_format = 2; + string data_key = 3; +} + +// DataGetReply contains the value of read from file +message DataGetReply { string value = 1; } + +// DataSetValueType enum what represents how represent value in a file +enum DataSetValueType { + UNKNOWN_VAL = 0; + STRING_VAL = 1; + INT_VAL = 2; + FLOAT_VAL = 3; + BOOL_VAL = 4; +} + +// DataSetRequest is request to set property in a specific file in specific format +message DataSetRequest { + string filename = 1; + FileFormat file_format = 2; + string data_key = 3; + string value = 4; + DataSetValueType value_type = 5; +} diff --git a/services/localfile/localfile_grpc.pb.go b/services/localfile/localfile_grpc.pb.go index c83f64d2..09ec1589 100644 --- a/services/localfile/localfile_grpc.pb.go +++ b/services/localfile/localfile_grpc.pb.go @@ -15,8 +15,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.3.0 -// - protoc v5.27.1 +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.28.1 // source: localfile.proto package localfile @@ -31,8 +31,8 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 const ( LocalFile_Read_FullMethodName = "/LocalFile.LocalFile/Read" @@ -48,25 +48,29 @@ const ( LocalFile_Readlink_FullMethodName = "/LocalFile.LocalFile/Readlink" LocalFile_Symlink_FullMethodName = "/LocalFile.LocalFile/Symlink" LocalFile_Mkdir_FullMethodName = "/LocalFile.LocalFile/Mkdir" + LocalFile_DataGet_FullMethodName = "/LocalFile.LocalFile/DataGet" + LocalFile_DataSet_FullMethodName = "/LocalFile.LocalFile/DataSet" ) // LocalFileClient is the client API for LocalFile service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The LocalFile service definition. type LocalFileClient interface { // Read reads a file from the disk and returns it contents. - Read(ctx context.Context, in *ReadActionRequest, opts ...grpc.CallOption) (LocalFile_ReadClient, error) + Read(ctx context.Context, in *ReadActionRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ReadReply], error) // Stat returns metadata about a filesystem path. - Stat(ctx context.Context, opts ...grpc.CallOption) (LocalFile_StatClient, error) + Stat(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StatRequest, StatReply], error) // Sum calculates a sum over the data in a single file. - Sum(ctx context.Context, opts ...grpc.CallOption) (LocalFile_SumClient, error) + Sum(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SumRequest, SumReply], error) // Write writes a file from the incoming RPC to a local file. - Write(ctx context.Context, opts ...grpc.CallOption) (LocalFile_WriteClient, error) + Write(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[WriteRequest, emptypb.Empty], error) // Copy retrieves a file from the given blob URL and writes it to a local // file. Copy(ctx context.Context, in *CopyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // List returns StatReply entries for the entities contained at a given path. - List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (LocalFile_ListClient, error) + List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ListReply], error) // SetFileAttributes takes a given filename and sets the given attributes. SetFileAttributes(ctx context.Context, in *SetFileAttributesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // Rm removes the given file. @@ -84,6 +88,10 @@ type LocalFileClient interface { Symlink(ctx context.Context, in *SymlinkRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // Mkdir create a new directory. Mkdir(ctx context.Context, in *MkdirRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // Get data from a file of specified type by provided key + DataGet(ctx context.Context, in *DataGetRequest, opts ...grpc.CallOption) (*DataGetReply, error) + // Set property from a file of specified type by provided key + DataSet(ctx context.Context, in *DataSetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } type localFileClient struct { @@ -94,12 +102,13 @@ func NewLocalFileClient(cc grpc.ClientConnInterface) LocalFileClient { return &localFileClient{cc} } -func (c *localFileClient) Read(ctx context.Context, in *ReadActionRequest, opts ...grpc.CallOption) (LocalFile_ReadClient, error) { - stream, err := c.cc.NewStream(ctx, &LocalFile_ServiceDesc.Streams[0], LocalFile_Read_FullMethodName, opts...) +func (c *localFileClient) Read(ctx context.Context, in *ReadActionRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ReadReply], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &LocalFile_ServiceDesc.Streams[0], LocalFile_Read_FullMethodName, cOpts...) if err != nil { return nil, err } - x := &localFileReadClient{stream} + x := &grpc.GenericClientStream[ReadActionRequest, ReadReply]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } @@ -109,134 +118,65 @@ func (c *localFileClient) Read(ctx context.Context, in *ReadActionRequest, opts return x, nil } -type LocalFile_ReadClient interface { - Recv() (*ReadReply, error) - grpc.ClientStream -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type LocalFile_ReadClient = grpc.ServerStreamingClient[ReadReply] -type localFileReadClient struct { - grpc.ClientStream -} - -func (x *localFileReadClient) Recv() (*ReadReply, error) { - m := new(ReadReply) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *localFileClient) Stat(ctx context.Context, opts ...grpc.CallOption) (LocalFile_StatClient, error) { - stream, err := c.cc.NewStream(ctx, &LocalFile_ServiceDesc.Streams[1], LocalFile_Stat_FullMethodName, opts...) +func (c *localFileClient) Stat(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[StatRequest, StatReply], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &LocalFile_ServiceDesc.Streams[1], LocalFile_Stat_FullMethodName, cOpts...) if err != nil { return nil, err } - x := &localFileStatClient{stream} + x := &grpc.GenericClientStream[StatRequest, StatReply]{ClientStream: stream} return x, nil } -type LocalFile_StatClient interface { - Send(*StatRequest) error - Recv() (*StatReply, error) - grpc.ClientStream -} - -type localFileStatClient struct { - grpc.ClientStream -} - -func (x *localFileStatClient) Send(m *StatRequest) error { - return x.ClientStream.SendMsg(m) -} - -func (x *localFileStatClient) Recv() (*StatReply, error) { - m := new(StatReply) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type LocalFile_StatClient = grpc.BidiStreamingClient[StatRequest, StatReply] -func (c *localFileClient) Sum(ctx context.Context, opts ...grpc.CallOption) (LocalFile_SumClient, error) { - stream, err := c.cc.NewStream(ctx, &LocalFile_ServiceDesc.Streams[2], LocalFile_Sum_FullMethodName, opts...) +func (c *localFileClient) Sum(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SumRequest, SumReply], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &LocalFile_ServiceDesc.Streams[2], LocalFile_Sum_FullMethodName, cOpts...) if err != nil { return nil, err } - x := &localFileSumClient{stream} + x := &grpc.GenericClientStream[SumRequest, SumReply]{ClientStream: stream} return x, nil } -type LocalFile_SumClient interface { - Send(*SumRequest) error - Recv() (*SumReply, error) - grpc.ClientStream -} - -type localFileSumClient struct { - grpc.ClientStream -} - -func (x *localFileSumClient) Send(m *SumRequest) error { - return x.ClientStream.SendMsg(m) -} - -func (x *localFileSumClient) Recv() (*SumReply, error) { - m := new(SumReply) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type LocalFile_SumClient = grpc.BidiStreamingClient[SumRequest, SumReply] -func (c *localFileClient) Write(ctx context.Context, opts ...grpc.CallOption) (LocalFile_WriteClient, error) { - stream, err := c.cc.NewStream(ctx, &LocalFile_ServiceDesc.Streams[3], LocalFile_Write_FullMethodName, opts...) +func (c *localFileClient) Write(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[WriteRequest, emptypb.Empty], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &LocalFile_ServiceDesc.Streams[3], LocalFile_Write_FullMethodName, cOpts...) if err != nil { return nil, err } - x := &localFileWriteClient{stream} + x := &grpc.GenericClientStream[WriteRequest, emptypb.Empty]{ClientStream: stream} return x, nil } -type LocalFile_WriteClient interface { - Send(*WriteRequest) error - CloseAndRecv() (*emptypb.Empty, error) - grpc.ClientStream -} - -type localFileWriteClient struct { - grpc.ClientStream -} - -func (x *localFileWriteClient) Send(m *WriteRequest) error { - return x.ClientStream.SendMsg(m) -} - -func (x *localFileWriteClient) CloseAndRecv() (*emptypb.Empty, error) { - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - m := new(emptypb.Empty) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type LocalFile_WriteClient = grpc.ClientStreamingClient[WriteRequest, emptypb.Empty] func (c *localFileClient) Copy(ctx context.Context, in *CopyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, LocalFile_Copy_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, LocalFile_Copy_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *localFileClient) List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (LocalFile_ListClient, error) { - stream, err := c.cc.NewStream(ctx, &LocalFile_ServiceDesc.Streams[4], LocalFile_List_FullMethodName, opts...) +func (c *localFileClient) List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ListReply], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &LocalFile_ServiceDesc.Streams[4], LocalFile_List_FullMethodName, cOpts...) if err != nil { return nil, err } - x := &localFileListClient{stream} + x := &grpc.GenericClientStream[ListRequest, ListReply]{ClientStream: stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } @@ -246,26 +186,13 @@ func (c *localFileClient) List(ctx context.Context, in *ListRequest, opts ...grp return x, nil } -type LocalFile_ListClient interface { - Recv() (*ListReply, error) - grpc.ClientStream -} - -type localFileListClient struct { - grpc.ClientStream -} - -func (x *localFileListClient) Recv() (*ListReply, error) { - m := new(ListReply) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type LocalFile_ListClient = grpc.ServerStreamingClient[ListReply] func (c *localFileClient) SetFileAttributes(ctx context.Context, in *SetFileAttributesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, LocalFile_SetFileAttributes_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, LocalFile_SetFileAttributes_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -273,8 +200,9 @@ func (c *localFileClient) SetFileAttributes(ctx context.Context, in *SetFileAttr } func (c *localFileClient) Rm(ctx context.Context, in *RmRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, LocalFile_Rm_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, LocalFile_Rm_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -282,8 +210,9 @@ func (c *localFileClient) Rm(ctx context.Context, in *RmRequest, opts ...grpc.Ca } func (c *localFileClient) Rmdir(ctx context.Context, in *RmdirRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, LocalFile_Rmdir_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, LocalFile_Rmdir_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -291,8 +220,9 @@ func (c *localFileClient) Rmdir(ctx context.Context, in *RmdirRequest, opts ...g } func (c *localFileClient) Rename(ctx context.Context, in *RenameRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, LocalFile_Rename_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, LocalFile_Rename_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -300,8 +230,9 @@ func (c *localFileClient) Rename(ctx context.Context, in *RenameRequest, opts .. } func (c *localFileClient) Readlink(ctx context.Context, in *ReadlinkRequest, opts ...grpc.CallOption) (*ReadlinkReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ReadlinkReply) - err := c.cc.Invoke(ctx, LocalFile_Readlink_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, LocalFile_Readlink_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -309,8 +240,9 @@ func (c *localFileClient) Readlink(ctx context.Context, in *ReadlinkRequest, opt } func (c *localFileClient) Symlink(ctx context.Context, in *SymlinkRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, LocalFile_Symlink_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, LocalFile_Symlink_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -318,8 +250,29 @@ func (c *localFileClient) Symlink(ctx context.Context, in *SymlinkRequest, opts } func (c *localFileClient) Mkdir(ctx context.Context, in *MkdirRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, LocalFile_Mkdir_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, LocalFile_Mkdir_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *localFileClient) DataGet(ctx context.Context, in *DataGetRequest, opts ...grpc.CallOption) (*DataGetReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DataGetReply) + err := c.cc.Invoke(ctx, LocalFile_DataGet_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *localFileClient) DataSet(ctx context.Context, in *DataSetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, LocalFile_DataSet_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -328,21 +281,23 @@ func (c *localFileClient) Mkdir(ctx context.Context, in *MkdirRequest, opts ...g // LocalFileServer is the server API for LocalFile service. // All implementations should embed UnimplementedLocalFileServer -// for forward compatibility +// for forward compatibility. +// +// The LocalFile service definition. type LocalFileServer interface { // Read reads a file from the disk and returns it contents. - Read(*ReadActionRequest, LocalFile_ReadServer) error + Read(*ReadActionRequest, grpc.ServerStreamingServer[ReadReply]) error // Stat returns metadata about a filesystem path. - Stat(LocalFile_StatServer) error + Stat(grpc.BidiStreamingServer[StatRequest, StatReply]) error // Sum calculates a sum over the data in a single file. - Sum(LocalFile_SumServer) error + Sum(grpc.BidiStreamingServer[SumRequest, SumReply]) error // Write writes a file from the incoming RPC to a local file. - Write(LocalFile_WriteServer) error + Write(grpc.ClientStreamingServer[WriteRequest, emptypb.Empty]) error // Copy retrieves a file from the given blob URL and writes it to a local // file. Copy(context.Context, *CopyRequest) (*emptypb.Empty, error) // List returns StatReply entries for the entities contained at a given path. - List(*ListRequest, LocalFile_ListServer) error + List(*ListRequest, grpc.ServerStreamingServer[ListReply]) error // SetFileAttributes takes a given filename and sets the given attributes. SetFileAttributes(context.Context, *SetFileAttributesRequest) (*emptypb.Empty, error) // Rm removes the given file. @@ -360,28 +315,35 @@ type LocalFileServer interface { Symlink(context.Context, *SymlinkRequest) (*emptypb.Empty, error) // Mkdir create a new directory. Mkdir(context.Context, *MkdirRequest) (*emptypb.Empty, error) + // Get property from a file of specified type by provided key + DataGet(context.Context, *DataGetRequest) (*DataGetReply, error) + // Set data value to a file of specified type by provided key + DataSet(context.Context, *DataSetRequest) (*emptypb.Empty, error) } -// UnimplementedLocalFileServer should be embedded to have forward compatible implementations. -type UnimplementedLocalFileServer struct { -} +// UnimplementedLocalFileServer should be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedLocalFileServer struct{} -func (UnimplementedLocalFileServer) Read(*ReadActionRequest, LocalFile_ReadServer) error { +func (UnimplementedLocalFileServer) Read(*ReadActionRequest, grpc.ServerStreamingServer[ReadReply]) error { return status.Errorf(codes.Unimplemented, "method Read not implemented") } -func (UnimplementedLocalFileServer) Stat(LocalFile_StatServer) error { +func (UnimplementedLocalFileServer) Stat(grpc.BidiStreamingServer[StatRequest, StatReply]) error { return status.Errorf(codes.Unimplemented, "method Stat not implemented") } -func (UnimplementedLocalFileServer) Sum(LocalFile_SumServer) error { +func (UnimplementedLocalFileServer) Sum(grpc.BidiStreamingServer[SumRequest, SumReply]) error { return status.Errorf(codes.Unimplemented, "method Sum not implemented") } -func (UnimplementedLocalFileServer) Write(LocalFile_WriteServer) error { +func (UnimplementedLocalFileServer) Write(grpc.ClientStreamingServer[WriteRequest, emptypb.Empty]) error { return status.Errorf(codes.Unimplemented, "method Write not implemented") } func (UnimplementedLocalFileServer) Copy(context.Context, *CopyRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Copy not implemented") } -func (UnimplementedLocalFileServer) List(*ListRequest, LocalFile_ListServer) error { +func (UnimplementedLocalFileServer) List(*ListRequest, grpc.ServerStreamingServer[ListReply]) error { return status.Errorf(codes.Unimplemented, "method List not implemented") } func (UnimplementedLocalFileServer) SetFileAttributes(context.Context, *SetFileAttributesRequest) (*emptypb.Empty, error) { @@ -405,6 +367,13 @@ func (UnimplementedLocalFileServer) Symlink(context.Context, *SymlinkRequest) (* func (UnimplementedLocalFileServer) Mkdir(context.Context, *MkdirRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method Mkdir not implemented") } +func (UnimplementedLocalFileServer) DataGet(context.Context, *DataGetRequest) (*DataGetReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method DataGet not implemented") +} +func (UnimplementedLocalFileServer) DataSet(context.Context, *DataSetRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DataSet not implemented") +} +func (UnimplementedLocalFileServer) testEmbeddedByValue() {} // UnsafeLocalFileServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to LocalFileServer will @@ -414,6 +383,13 @@ type UnsafeLocalFileServer interface { } func RegisterLocalFileServer(s grpc.ServiceRegistrar, srv LocalFileServer) { + // If the following call pancis, it indicates UnimplementedLocalFileServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&LocalFile_ServiceDesc, srv) } @@ -422,99 +398,32 @@ func _LocalFile_Read_Handler(srv interface{}, stream grpc.ServerStream) error { if err := stream.RecvMsg(m); err != nil { return err } - return srv.(LocalFileServer).Read(m, &localFileReadServer{stream}) + return srv.(LocalFileServer).Read(m, &grpc.GenericServerStream[ReadActionRequest, ReadReply]{ServerStream: stream}) } -type LocalFile_ReadServer interface { - Send(*ReadReply) error - grpc.ServerStream -} - -type localFileReadServer struct { - grpc.ServerStream -} - -func (x *localFileReadServer) Send(m *ReadReply) error { - return x.ServerStream.SendMsg(m) -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type LocalFile_ReadServer = grpc.ServerStreamingServer[ReadReply] func _LocalFile_Stat_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(LocalFileServer).Stat(&localFileStatServer{stream}) + return srv.(LocalFileServer).Stat(&grpc.GenericServerStream[StatRequest, StatReply]{ServerStream: stream}) } -type LocalFile_StatServer interface { - Send(*StatReply) error - Recv() (*StatRequest, error) - grpc.ServerStream -} - -type localFileStatServer struct { - grpc.ServerStream -} - -func (x *localFileStatServer) Send(m *StatReply) error { - return x.ServerStream.SendMsg(m) -} - -func (x *localFileStatServer) Recv() (*StatRequest, error) { - m := new(StatRequest) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type LocalFile_StatServer = grpc.BidiStreamingServer[StatRequest, StatReply] func _LocalFile_Sum_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(LocalFileServer).Sum(&localFileSumServer{stream}) -} - -type LocalFile_SumServer interface { - Send(*SumReply) error - Recv() (*SumRequest, error) - grpc.ServerStream -} - -type localFileSumServer struct { - grpc.ServerStream + return srv.(LocalFileServer).Sum(&grpc.GenericServerStream[SumRequest, SumReply]{ServerStream: stream}) } -func (x *localFileSumServer) Send(m *SumReply) error { - return x.ServerStream.SendMsg(m) -} - -func (x *localFileSumServer) Recv() (*SumRequest, error) { - m := new(SumRequest) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type LocalFile_SumServer = grpc.BidiStreamingServer[SumRequest, SumReply] func _LocalFile_Write_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(LocalFileServer).Write(&localFileWriteServer{stream}) -} - -type LocalFile_WriteServer interface { - SendAndClose(*emptypb.Empty) error - Recv() (*WriteRequest, error) - grpc.ServerStream -} - -type localFileWriteServer struct { - grpc.ServerStream + return srv.(LocalFileServer).Write(&grpc.GenericServerStream[WriteRequest, emptypb.Empty]{ServerStream: stream}) } -func (x *localFileWriteServer) SendAndClose(m *emptypb.Empty) error { - return x.ServerStream.SendMsg(m) -} - -func (x *localFileWriteServer) Recv() (*WriteRequest, error) { - m := new(WriteRequest) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type LocalFile_WriteServer = grpc.ClientStreamingServer[WriteRequest, emptypb.Empty] func _LocalFile_Copy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CopyRequest) @@ -539,21 +448,11 @@ func _LocalFile_List_Handler(srv interface{}, stream grpc.ServerStream) error { if err := stream.RecvMsg(m); err != nil { return err } - return srv.(LocalFileServer).List(m, &localFileListServer{stream}) -} - -type LocalFile_ListServer interface { - Send(*ListReply) error - grpc.ServerStream + return srv.(LocalFileServer).List(m, &grpc.GenericServerStream[ListRequest, ListReply]{ServerStream: stream}) } -type localFileListServer struct { - grpc.ServerStream -} - -func (x *localFileListServer) Send(m *ListReply) error { - return x.ServerStream.SendMsg(m) -} +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type LocalFile_ListServer = grpc.ServerStreamingServer[ListReply] func _LocalFile_SetFileAttributes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SetFileAttributesRequest) @@ -681,6 +580,42 @@ func _LocalFile_Mkdir_Handler(srv interface{}, ctx context.Context, dec func(int return interceptor(ctx, in, info, handler) } +func _LocalFile_DataGet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DataGetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LocalFileServer).DataGet(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: LocalFile_DataGet_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LocalFileServer).DataGet(ctx, req.(*DataGetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _LocalFile_DataSet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DataSetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LocalFileServer).DataSet(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: LocalFile_DataSet_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LocalFileServer).DataSet(ctx, req.(*DataSetRequest)) + } + return interceptor(ctx, in, info, handler) +} + // LocalFile_ServiceDesc is the grpc.ServiceDesc for LocalFile service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -720,6 +655,14 @@ var LocalFile_ServiceDesc = grpc.ServiceDesc{ MethodName: "Mkdir", Handler: _LocalFile_Mkdir_Handler, }, + { + MethodName: "DataGet", + Handler: _LocalFile_DataGet_Handler, + }, + { + MethodName: "DataSet", + Handler: _LocalFile_DataSet_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/services/localfile/localfile_grpcproxy.pb.go b/services/localfile/localfile_grpcproxy.pb.go index 945cbb43..565957e3 100644 --- a/services/localfile/localfile_grpcproxy.pb.go +++ b/services/localfile/localfile_grpcproxy.pb.go @@ -33,6 +33,8 @@ type LocalFileClientProxy interface { ReadlinkOneMany(ctx context.Context, in *ReadlinkRequest, opts ...grpc.CallOption) (<-chan *ReadlinkManyResponse, error) SymlinkOneMany(ctx context.Context, in *SymlinkRequest, opts ...grpc.CallOption) (<-chan *SymlinkManyResponse, error) MkdirOneMany(ctx context.Context, in *MkdirRequest, opts ...grpc.CallOption) (<-chan *MkdirManyResponse, error) + DataGetOneMany(ctx context.Context, in *DataGetRequest, opts ...grpc.CallOption) (<-chan *DataGetManyResponse, error) + DataSetOneMany(ctx context.Context, in *DataSetRequest, opts ...grpc.CallOption) (<-chan *DataSetManyResponse, error) } // Embed the original client inside of this so we get the other generated methods automatically. @@ -1039,3 +1041,137 @@ func (c *localFileClientProxy) MkdirOneMany(ctx context.Context, in *MkdirReques return ret, nil } + +// DataGetManyResponse encapsulates a proxy data packet. +// It includes the target, index, response and possible error returned. +type DataGetManyResponse struct { + Target string + // As targets can be duplicated this is the index into the slice passed to proxy.Conn. + Index int + Resp *DataGetReply + Error error +} + +// DataGetOneMany provides the same API as DataGet but sends the same request to N destinations at once. +// N can be a single destination. +// +// NOTE: The returned channel must be read until it closes in order to avoid leaking goroutines. +func (c *localFileClientProxy) DataGetOneMany(ctx context.Context, in *DataGetRequest, opts ...grpc.CallOption) (<-chan *DataGetManyResponse, error) { + conn := c.cc.(*proxy.Conn) + ret := make(chan *DataGetManyResponse) + // If this is a single case we can just use Invoke and marshal it onto the channel once and be done. + if len(conn.Targets) == 1 { + go func() { + out := &DataGetManyResponse{ + Target: conn.Targets[0], + Index: 0, + Resp: &DataGetReply{}, + } + err := conn.Invoke(ctx, "/LocalFile.LocalFile/DataGet", in, out.Resp, opts...) + if err != nil { + out.Error = err + } + // Send and close. + ret <- out + close(ret) + }() + return ret, nil + } + manyRet, err := conn.InvokeOneMany(ctx, "/LocalFile.LocalFile/DataGet", in, opts...) + if err != nil { + return nil, err + } + // A goroutine to retrive untyped responses and convert them to typed ones. + go func() { + for { + typedResp := &DataGetManyResponse{ + Resp: &DataGetReply{}, + } + + resp, ok := <-manyRet + if !ok { + // All done so we can shut down. + close(ret) + return + } + typedResp.Target = resp.Target + typedResp.Index = resp.Index + typedResp.Error = resp.Error + if resp.Error == nil { + if err := resp.Resp.UnmarshalTo(typedResp.Resp); err != nil { + typedResp.Error = fmt.Errorf("can't decode any response - %v. Original Error - %v", err, resp.Error) + } + } + ret <- typedResp + } + }() + + return ret, nil +} + +// DataSetManyResponse encapsulates a proxy data packet. +// It includes the target, index, response and possible error returned. +type DataSetManyResponse struct { + Target string + // As targets can be duplicated this is the index into the slice passed to proxy.Conn. + Index int + Resp *emptypb.Empty + Error error +} + +// DataSetOneMany provides the same API as DataSet but sends the same request to N destinations at once. +// N can be a single destination. +// +// NOTE: The returned channel must be read until it closes in order to avoid leaking goroutines. +func (c *localFileClientProxy) DataSetOneMany(ctx context.Context, in *DataSetRequest, opts ...grpc.CallOption) (<-chan *DataSetManyResponse, error) { + conn := c.cc.(*proxy.Conn) + ret := make(chan *DataSetManyResponse) + // If this is a single case we can just use Invoke and marshal it onto the channel once and be done. + if len(conn.Targets) == 1 { + go func() { + out := &DataSetManyResponse{ + Target: conn.Targets[0], + Index: 0, + Resp: &emptypb.Empty{}, + } + err := conn.Invoke(ctx, "/LocalFile.LocalFile/DataSet", in, out.Resp, opts...) + if err != nil { + out.Error = err + } + // Send and close. + ret <- out + close(ret) + }() + return ret, nil + } + manyRet, err := conn.InvokeOneMany(ctx, "/LocalFile.LocalFile/DataSet", in, opts...) + if err != nil { + return nil, err + } + // A goroutine to retrive untyped responses and convert them to typed ones. + go func() { + for { + typedResp := &DataSetManyResponse{ + Resp: &emptypb.Empty{}, + } + + resp, ok := <-manyRet + if !ok { + // All done so we can shut down. + close(ret) + return + } + typedResp.Target = resp.Target + typedResp.Index = resp.Index + typedResp.Error = resp.Error + if resp.Error == nil { + if err := resp.Resp.UnmarshalTo(typedResp.Resp); err != nil { + typedResp.Error = fmt.Errorf("can't decode any response - %v. Original Error - %v", err, resp.Error) + } + } + ret <- typedResp + } + }() + + return ret, nil +} diff --git a/services/localfile/server/application/data-get.usecase.go b/services/localfile/server/application/data-get.usecase.go new file mode 100644 index 00000000..58900198 --- /dev/null +++ b/services/localfile/server/application/data-get.usecase.go @@ -0,0 +1,80 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 application + +import ( + "context" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + "github.com/Snowflake-Labs/sansshell/services/localfile/server/infrastructure/output/file-data" + "github.com/Snowflake-Labs/sansshell/services/util" + error_utils "github.com/Snowflake-Labs/sansshell/services/util/error-utils" + "github.com/go-logr/logr" +) + +type DataGetErrorCodes int + +var ( + // DataGetErrorCodes_FilePathInvalid error code for invalid file path was provided + DataGetErrorCodes_FilePathInvalid DataGetErrorCodes = 1 + // DataGetErrorCodes_FileFormatNotSupported error code for file format not supported + DataGetErrorCodes_FileFormatNotSupported DataGetErrorCodes = 2 + // DataGetErrorCodes_CouldNotGetData error code for could not get data by provided key + DataGetErrorCodes_CouldNotGetData DataGetErrorCodes = 3 +) + +// DataGetUsecase usecase interface for getting data from file of specific format by provided data key +type DataGetUsecase interface { + // Run gets data from file by provided data key + // Returns data value or [error_utils.ErrorWithCode[DataGetErrorCodes]] if error occurred + Run(ctx context.Context, filePath string, dataKey string, fileFormat pb.FileFormat) (string, error_utils.ErrorWithCode[DataGetErrorCodes]) +} + +// NewDataGetUsecase creates new instance of [DataSetUsecase] +func NewDataGetUsecase(fileDataReaderFactory file_data.FileDataRepositoryFactory) DataGetUsecase { + return &dataGetUsecase{ + fileDataRepoFactory: fileDataReaderFactory, + } +} + +// dataGetUsecase implementation of [DataSetUsecase] interface +type dataGetUsecase struct { + fileDataRepoFactory file_data.FileDataRepositoryFactory +} + +// Run implementation of [DataSetUsecase.Run] interface +func (u *dataGetUsecase) Run(ctx context.Context, filePath string, dataKey string, fileFormat pb.FileFormat) (string, error_utils.ErrorWithCode[DataGetErrorCodes]) { + logger := logr.FromContextOrDiscard(ctx) + err := util.ValidPath(filePath) + if err != nil { + logger.Error(err, "invalid file path") + return "", error_utils.NewErrorWithCodef(DataGetErrorCodes_FilePathInvalid, "invalid file path: %s", err.Error()) + } + + fileDataRepo, err := u.fileDataRepoFactory.GetRepository(ctx, fileFormat) + if err != nil { + logger.Error(err, "unsupported file format", "format name", fileFormat.String(), "file path", filePath) + return "", error_utils.NewErrorWithCodef(DataGetErrorCodes_FileFormatNotSupported, "file fromat is not supported \"%s\" format", fileFormat.String()) + } + + value, err := fileDataRepo.GetDataByKey(filePath, dataKey) + if err != nil { + return "", error_utils.NewErrorWithCodef(DataGetErrorCodes_CouldNotGetData, "could not get data by \"%s\" key: %s", dataKey, err.Error()) + } + + return value, nil +} diff --git a/services/localfile/server/application/data-get.usecase_test.go b/services/localfile/server/application/data-get.usecase_test.go new file mode 100644 index 00000000..3d1936ae --- /dev/null +++ b/services/localfile/server/application/data-get.usecase_test.go @@ -0,0 +1,175 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 application + +import ( + "context" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + "github.com/Snowflake-Labs/sansshell/services/localfile/server/infrastructure/output/file-data" + "testing" +) + +func Test__dataGetUsecase__Run(t *testing.T) { + invalidFilepathTests := []struct { + name string + invalidFilePath string + expectedError string + }{ + { + name: "It should fail if open file path provided", + invalidFilePath: "/not-clear/file/path/", + expectedError: "[1]: invalid file path: rpc error: code = InvalidArgument desc = /not-clear/file/path/ must be a clean path", + }, + { + name: "It should fail if relative file path provided", + invalidFilePath: "./relative/path", + expectedError: "[1]: invalid file path: rpc error: code = InvalidArgument desc = ./relative/path must be an absolute path", + }, + { + name: "It should fail if parent directory relative file path provided", + invalidFilePath: "../parent-dir/relative/path", + expectedError: "[1]: invalid file path: rpc error: code = InvalidArgument desc = ../parent-dir/relative/path must be an absolute path", + }, + } + + for _, test := range invalidFilepathTests { + t.Run(test.name, func(t *testing.T) { + // ARRANGE + usecase := &dataGetUsecase{} + ctx := context.Background() + + // ACT + _, err := usecase.Run(ctx, test.invalidFilePath, "dataKey", pb.FileFormat_YML) + + // ASSERT + if err == nil { + t.Errorf("Expected error, got nil") + } + + if err.Error() != test.expectedError { + t.Errorf("Expected error %s, got %s", test.expectedError, err.Error()) + } + }) + } + + t.Run("It should fail if unsupported file format provided", func(t *testing.T) { + // ARRANGE + instanceMap := make(map[pb.FileFormat]file_data.FileDataRepository) + repoFactoryMock := NewFileDataRepositoryFactoryMock(instanceMap) + usecase := &dataGetUsecase{ + fileDataRepoFactory: repoFactoryMock, + } + ctx := context.Background() + + // ACT + _, err := usecase.Run(ctx, "/some/file/path", "dataKey", pb.FileFormat_UNKNOWN) + + // ASSERT + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("It should fail if unsupported file format provided", func(t *testing.T) { + // ARRANGE + instanceMap := make(map[pb.FileFormat]file_data.FileDataRepository) + repoFactoryMock := NewFileDataRepositoryFactoryMock(instanceMap) + usecase := &dataGetUsecase{ + fileDataRepoFactory: repoFactoryMock, + } + ctx := context.Background() + + // ACT + _, err := usecase.Run(ctx, "/some/file/path", "dataKey", pb.FileFormat_UNKNOWN) + + // ASSERT + if err == nil { + t.Errorf("Expected error, got nil") + return + } + }) + + t.Run("It should fail if file repo return error", func(t *testing.T) { + // ARRANGE + dataMap := make(map[string]map[string]string) + keyError := "some error" + expectedError := "[3]: could not get data by \"dataKey\" key: some error" + filePath := "/some/file/path" + dataKey := "dataKey" + errorOnGetKey := make(map[string]map[string]string) + errorOnGetKey[filePath] = make(map[string]string) + errorOnGetKey[filePath][dataKey] = keyError + repoMock := NewFileDataRepositoryMock(dataMap, nil, errorOnGetKey) + + instanceMap := make(map[pb.FileFormat]file_data.FileDataRepository) + instanceMap[pb.FileFormat_YML] = repoMock + repoFactoryMock := NewFileDataRepositoryFactoryMock(instanceMap) + usecase := &dataGetUsecase{ + fileDataRepoFactory: repoFactoryMock, + } + ctx := context.Background() + + // ACT + _, err := usecase.Run(ctx, filePath, dataKey, pb.FileFormat_YML) + + // ASSERT + if err == nil { + t.Errorf("Expected error, got nil") + return + } + + if err.Error() != expectedError { + t.Errorf("Expected error \"%s\", got \"%s\"", expectedError, err.Error()) + return + } + }) + + t.Run("It should return data by key, if repo return data", func(t *testing.T) { + // ARRANGE + filePath := "/some/file/path" + dataKey := "dataKey" + expectedData := "some data" + dataMap := make(map[string]map[string]string) + dataMap[filePath] = make(map[string]string) + dataMap[filePath][dataKey] = expectedData + repoMock := NewFileDataRepositoryMock(dataMap, nil, nil) + + instanceMap := make(map[pb.FileFormat]file_data.FileDataRepository) + instanceMap[pb.FileFormat_YML] = repoMock + repoFactoryMock := NewFileDataRepositoryFactoryMock(instanceMap) + usecase := &dataGetUsecase{ + fileDataRepoFactory: repoFactoryMock, + } + ctx := context.Background() + + // ACT + data, err := usecase.Run(ctx, filePath, dataKey, pb.FileFormat_YML) + + // ASSERT + if err != nil { + t.Errorf("Unexpected error: %s", err.Error()) + return + } + + if data != expectedData { + t.Errorf("Expected data %s, got %s", expectedData, data) + return + } + }) + +} diff --git a/services/localfile/server/application/data-set.usecase.go b/services/localfile/server/application/data-set.usecase.go new file mode 100644 index 00000000..745cb886 --- /dev/null +++ b/services/localfile/server/application/data-set.usecase.go @@ -0,0 +1,79 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 application + +import ( + "context" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + "github.com/Snowflake-Labs/sansshell/services/localfile/server/infrastructure/output/file-data" + "github.com/Snowflake-Labs/sansshell/services/util" + error_utils "github.com/Snowflake-Labs/sansshell/services/util/error-utils" + "github.com/go-logr/logr" +) + +type DataSetErrorCodes int + +var ( + // DataSetErrorCodes_FilePathInvalid error code for invalid file path was provided + DataSetErrorCodes_FilePathInvalid DataSetErrorCodes = 1 + // DataGetErrorCodes_FileFormatNotSupported error code for file format not supported + DataSetErrorCodes_FileFormatNotSupported DataSetErrorCodes = 2 + // DataGetErrorCodes_CouldNotGetData error code for could not get data by provided key + DataSetErrorCodes_CouldNotSetData DataSetErrorCodes = 3 +) + +// DataSetUsecase usecase interface for set data to file of specific format by provided data key +type DataSetUsecase interface { + // Run sets data to file by provided data key + // Returns [error_utils.ErrorWithCode[DataSetErrorCodes]] if error occurred + Run(ctx context.Context, filePath string, dataKey string, fileFormat pb.FileFormat, value string, valueType pb.DataSetValueType) error_utils.ErrorWithCode[DataSetErrorCodes] +} + +// NewDataSetUsecase creates new instance of [DataSetUsecase] +func NewDataSetUsecase(fileDataRepoFactory file_data.FileDataRepositoryFactory) DataSetUsecase { + return &dataSetUsecase{ + fileDataRepoFactory: fileDataRepoFactory, + } +} + +// dataSetUsecase implementation of [DataSetUsecase] interface +type dataSetUsecase struct { + fileDataRepoFactory file_data.FileDataRepositoryFactory +} + +func (u *dataSetUsecase) Run(ctx context.Context, filePath string, dataKey string, fileFormat pb.FileFormat, value string, valueType pb.DataSetValueType) error_utils.ErrorWithCode[DataSetErrorCodes] { + logger := logr.FromContextOrDiscard(ctx) + err := util.ValidPath(filePath) + if err != nil { + logger.Error(err, "invalid file path") + return error_utils.NewErrorWithCodef(DataSetErrorCodes_FilePathInvalid, "invalid file path: %s", err.Error()) + } + + fileDataRepo, err := u.fileDataRepoFactory.GetRepository(ctx, fileFormat) + if err != nil { + logger.Error(err, "unsupported file format", "format name", fileFormat.String(), "file path", filePath) + return error_utils.NewErrorWithCodef(DataSetErrorCodes_FileFormatNotSupported, "file fromat is not supported \"%s\"", fileFormat.String()) + } + + err = fileDataRepo.SetDataByKey(filePath, dataKey, value, valueType) + if err != nil { + return error_utils.NewErrorWithCodef(DataSetErrorCodes_CouldNotSetData, "could not set data by \"%s\" key: %s", dataKey, err.Error()) + } + + return nil +} diff --git a/services/localfile/server/application/data-set.usecase_test.go b/services/localfile/server/application/data-set.usecase_test.go new file mode 100644 index 00000000..adef3b50 --- /dev/null +++ b/services/localfile/server/application/data-set.usecase_test.go @@ -0,0 +1,186 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 application + +import ( + "context" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + "github.com/Snowflake-Labs/sansshell/services/localfile/server/infrastructure/output/file-data" + "testing" +) + +func Test__dataSetUsecase__Run(t *testing.T) { + invalidFilepathTests := []struct { + name string + invalidFilePath string + expectedError string + }{ + { + name: "It should fail if open file path provided", + invalidFilePath: "/not-clear/file/path/", + expectedError: "[1]: invalid file path: rpc error: code = InvalidArgument desc = /not-clear/file/path/ must be a clean path", + }, + { + name: "It should fail if relative file path provided", + invalidFilePath: "./relative/path", + expectedError: "[1]: invalid file path: rpc error: code = InvalidArgument desc = ./relative/path must be an absolute path", + }, + { + name: "It should fail if parent directory relative file path provided", + invalidFilePath: "../parent-dir/relative/path", + expectedError: "[1]: invalid file path: rpc error: code = InvalidArgument desc = ../parent-dir/relative/path must be an absolute path", + }, + } + + for _, test := range invalidFilepathTests { + t.Run(test.name, func(t *testing.T) { + // ARRANGE + usecase := &dataSetUsecase{} + ctx := context.Background() + + // ACT + err := usecase.Run(ctx, test.invalidFilePath, "dataKey", pb.FileFormat_YML, "some data", pb.DataSetValueType_STRING_VAL) + + // ASSERT + if err == nil { + t.Errorf("Expected error, got nil") + } + + if err.Error() != test.expectedError { + t.Errorf("Expected error \"%s\", got \"%s\"", test.expectedError, err.Error()) + } + }) + } + + t.Run("It should fail if unsupported file format provided", func(t *testing.T) { + // ARRANGE + instanceMap := make(map[pb.FileFormat]file_data.FileDataRepository) + repoFactoryMock := NewFileDataRepositoryFactoryMock(instanceMap) + usecase := &dataSetUsecase{ + fileDataRepoFactory: repoFactoryMock, + } + ctx := context.Background() + + // ACT + err := usecase.Run(ctx, "/some/file/path", "dataKey", pb.FileFormat_YML, "some data", pb.DataSetValueType_STRING_VAL) + + // ASSERT + if err == nil { + t.Errorf("Expected error, got nil") + } + }) + + t.Run("It should fail if unsupported file format provided", func(t *testing.T) { + // ARRANGE + instanceMap := make(map[pb.FileFormat]file_data.FileDataRepository) + repoFactoryMock := NewFileDataRepositoryFactoryMock(instanceMap) + usecase := &dataSetUsecase{ + fileDataRepoFactory: repoFactoryMock, + } + ctx := context.Background() + + // ACT + err := usecase.Run(ctx, "/some/file/path", "dataKey", pb.FileFormat_YML, "some data", pb.DataSetValueType_STRING_VAL) + + // ASSERT + if err == nil { + t.Errorf("Expected error, got nil") + return + } + }) + + t.Run("It should fail if file repo return error", func(t *testing.T) { + // ARRANGE + // create error on set specific key + expectedDataError := "some error" + expectedError := "[3]: could not set data by \"dataKey\" key: some error" + filePath := "/some/file/path" + dataKey := "dataKey" + errorOnSetKey := make(map[string]map[string]string) + errorOnSetKey[filePath] = make(map[string]string) + errorOnSetKey[filePath][dataKey] = expectedDataError + + // create repo and factory mocks + dataMap := make(map[string]map[string]string) + repoMock := NewFileDataRepositoryMock(dataMap, errorOnSetKey, nil) + instanceMap := make(map[pb.FileFormat]file_data.FileDataRepository) + instanceMap[pb.FileFormat_YML] = repoMock + repoFactoryMock := NewFileDataRepositoryFactoryMock(instanceMap) + + // create usecase + usecase := &dataSetUsecase{ + fileDataRepoFactory: repoFactoryMock, + } + ctx := context.Background() + + // ACT + err := usecase.Run(ctx, filePath, dataKey, pb.FileFormat_YML, "some data", pb.DataSetValueType_STRING_VAL) + + // ASSERT + if err == nil { + t.Errorf("Expected error, got nil") + return + } + + if err.Error() != expectedError { + t.Errorf("Expected error \"%s\", got \"%s\"", expectedError, err.Error()) + return + } + }) + + t.Run("It should set data by key", func(t *testing.T) { + // ARRANGE + filePath := "/some/file/path" + dataKey := "dataKey" + expectedData := "some data" + dataMap := make(map[string]map[string]string) + dataMap[filePath] = make(map[string]string) + dataMap[filePath][dataKey] = expectedData + errorOnSetKey := make(map[string]map[string]string) + repoMock := NewFileDataRepositoryMock(dataMap, errorOnSetKey, nil) + + instanceMap := make(map[pb.FileFormat]file_data.FileDataRepository) + instanceMap[pb.FileFormat_YML] = repoMock + repoFactoryMock := NewFileDataRepositoryFactoryMock(instanceMap) + usecase := &dataSetUsecase{ + fileDataRepoFactory: repoFactoryMock, + } + ctx := context.Background() + + // ACT + err := usecase.Run(ctx, filePath, dataKey, pb.FileFormat_YML, expectedData, pb.DataSetValueType_STRING_VAL) + + // ASSERT + if err != nil { + t.Errorf("Unexpected error: %s", err.Error()) + return + } + + data, ok := dataMap[filePath][dataKey] + if !ok { + t.Errorf("Expected data by key %s, got nothing", dataKey) + return + } + + if data != expectedData { + t.Errorf("Expected data %s, got %s", expectedData, data) + return + } + }) + +} diff --git a/services/localfile/server/application/file-data.repository_mock_test.go b/services/localfile/server/application/file-data.repository_mock_test.go new file mode 100644 index 00000000..e1fe1d17 --- /dev/null +++ b/services/localfile/server/application/file-data.repository_mock_test.go @@ -0,0 +1,88 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 application + +import ( + "context" + "errors" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + "github.com/Snowflake-Labs/sansshell/services/localfile/server/infrastructure/output/file-data" +) + +// ----------------------------------------------------------------------------- +// FileDataRepositoryFactoryMock implementation +// ----------------------------------------------------------------------------- +func NewFileDataRepositoryFactoryMock(instanceMap map[pb.FileFormat]file_data.FileDataRepository) file_data.FileDataRepositoryFactory { + return &fileDataRepositoryFactoryMock{ + instanceMap: instanceMap, + } +} + +type fileDataRepositoryFactoryMock struct { + instanceMap map[pb.FileFormat]file_data.FileDataRepository +} + +func (f *fileDataRepositoryFactoryMock) GetRepository(ctx context.Context, fileFormat pb.FileFormat) (file_data.FileDataRepository, error) { + if instanceMap, ok := f.instanceMap[fileFormat]; ok { + return instanceMap, nil + } + + return nil, errors.New("unsupported file type") +} + +// ----------------------------------------------------------------------------- +// FileDataRepositoryMock implementation +// ----------------------------------------------------------------------------- +func NewFileDataRepositoryMock(dataByFileByKey map[string]map[string]string, errorOnSetKey map[string]map[string]string, errorOnGetKey map[string]map[string]string) file_data.FileDataRepository { + return &fileDataRepositoryMock{ + dataByKey: dataByFileByKey, + errorOnSetKey: errorOnSetKey, + errorOnGetKey: errorOnGetKey, + } +} + +type fileDataRepositoryMock struct { + dataByKey map[string]map[string]string + errorOnSetKey map[string]map[string]string + errorOnGetKey map[string]map[string]string +} + +func (f *fileDataRepositoryMock) GetDataByKey(filePath string, key string) (string, error) { + if val, ok := f.errorOnGetKey[filePath][key]; ok { + return "", errors.New(val) + } + + if value, ok := f.dataByKey[filePath][key]; ok { + return value, nil + } + + return "", errors.New("key not found") +} + +func (f *fileDataRepositoryMock) SetDataByKey(filePath string, key string, value string, valueType pb.DataSetValueType) error { + if val, ok := f.errorOnSetKey[filePath][key]; ok { + return errors.New(val) + } + + if _, ok := f.dataByKey[filePath][key]; !ok { + return errors.New("key not found") + } + + f.dataByKey[filePath][key] = value + return nil +} diff --git a/services/localfile/server/infrastructure/output/file-data/file-data-dotenv.repository.go b/services/localfile/server/infrastructure/output/file-data/file-data-dotenv.repository.go new file mode 100644 index 00000000..399cfc04 --- /dev/null +++ b/services/localfile/server/infrastructure/output/file-data/file-data-dotenv.repository.go @@ -0,0 +1,128 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 file_data + +import ( + "context" + "fmt" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + arrayUtils "github.com/Snowflake-Labs/sansshell/services/util/array-utils" + fileUtils "github.com/Snowflake-Labs/sansshell/services/util/file-utils" + "github.com/go-logr/logr" + "github.com/joho/godotenv" + "io" + "strings" +) + +func newDotEnvFileDataRepository(context context.Context) FileDataRepository { + return &fileDataDotEnvRepository{ + context: context, + } +} + +// fileDataDotEnvRepository implementation of [application.FileDataRepository] interface +type fileDataDotEnvRepository struct { + context context.Context +} + +// GetDataByKey implementation of [application.FileDataRepository.GetDataByKey] interface +func (y *fileDataDotEnvRepository) GetDataByKey(filePath string, key string) (string, error) { + envMap, err := godotenv.Read(filePath) + if err != nil { + return "", fmt.Errorf("failed to read file") + } + + val, exists := envMap[key] + if !exists { + return "", fmt.Errorf("key \"%s\" not found", key) + } + return val, nil +} + +// SetDataByKey implementation of [application.FileDataRepository.SetDataByKey] interface +func (y *fileDataDotEnvRepository) SetDataByKey(filePath string, key string, value string, valType pb.DataSetValueType) error { + if valType != pb.DataSetValueType_STRING_VAL { + return fmt.Errorf("unsupported value type: %s", valType) + } + + f, err := fileUtils.OpenForOverwrite(filePath) + if err != nil { + return fmt.Errorf("failed to open file: %s", err) + } + defer f.Close() // close file when done + + // Apply an exclusive lock + unlock, err := fileUtils.ExclusiveLockFile(f) + if err != nil { + return fmt.Errorf("failed to lock file: %s", err) + } + defer (func() { + logger := logr.FromContextOrDiscard(y.context) + err := unlock() // unlock the file when done + logger.Error(err, "failed to unlock file") + })() + + r := io.Reader(f) + rawContent, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("failed to read file content: %w", err) + } + + content := string(rawContent) + lines := strings.Split(content, "\n") + + varLinePrefix := key + "=" + varLineIndex := arrayUtils.FindIndexBy(lines, func(line string) bool { + return strings.HasPrefix(line, varLinePrefix) + }) + + if varLineIndex == -1 { + lines = append(lines, escapeDotEnvVarValue(key)+"="+escapeDotEnvVarValue(value)) + } else { + lines[varLineIndex] = key + "=" + escapeDotEnvVarValue(value) + } + + updatedContent := strings.Join(lines, "\n") + + if _, err := f.Seek(0, 0); err != nil { + return fmt.Errorf("failed to seek file: %s", err) + } + + if _, err := f.WriteString(updatedContent); err != nil { + return fmt.Errorf("failed to write file: %s", err) + } + + return nil +} + +var doubleQuoteSpecialChars = map[string]string{ + "\\": "\\\\", + "\n": "\\\n", + "\r": "\\\r", + "\"": "\\\"", + "!": "\\!", + "$": "\\$", + "`": "\\`", +} + +func escapeDotEnvVarValue(value string) string { + for source, toReplace := range doubleQuoteSpecialChars { + value = strings.Replace(value, source, toReplace, -1) + } + return value +} diff --git a/services/localfile/server/infrastructure/output/file-data/file-data-dotenv.repository_integration_test.go b/services/localfile/server/infrastructure/output/file-data/file-data-dotenv.repository_integration_test.go new file mode 100644 index 00000000..1b2863b5 --- /dev/null +++ b/services/localfile/server/infrastructure/output/file-data/file-data-dotenv.repository_integration_test.go @@ -0,0 +1,333 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 file_data + +import ( + "errors" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + "os" + "testing" +) + +func Test_FileDataDonEnvRepository_GetDataByKey(t *testing.T) { + if os.Getenv("INTEGRATION_TEST") == "" { + t.Skip("skipping integration test") + } + + validDotEnv := `SOME=VAR +SOME_OTHER=VAR_VAL +` + + validYmlTests := []struct { + name string + key string + expectedResult string + expectedErr error + }{ + { + name: "It should get value by key", + key: "SOME_OTHER", + expectedResult: "VAR_VAL", + expectedErr: nil, + }, + { + name: "It should get error if key not found", + key: "NOT_EXISTED_KEY", + expectedErr: errors.New("key \"NOT_EXISTED_KEY\" not found"), + }, + } + + for _, test := range validYmlTests { + t.Run(test.name, func(t *testing.T) { + // ARRANGE + repo := &fileDataDotEnvRepository{} + release, filePath, err := writeStringToTmpFile(t, "test.yml", validDotEnv) + if err != nil { + t.Errorf("Unexpected tmp file creation error: %s", err.Error()) + return + } + defer (func() { + _ = release() + })() + + // ACT + result, err := repo.GetDataByKey(filePath, test.key) + + // ASSERT + if test.expectedErr != nil && err == nil { + t.Errorf("Expected error \"%s\", but got nil", test.expectedErr) + return + } + + if test.expectedErr != nil && err != nil && test.expectedErr.Error() != err.Error() { + t.Errorf("Expected \"%s\", but got \"%s\"", test.expectedErr, err) + return + } + + if result != test.expectedResult { + t.Errorf("Expected \"%s\", but got \"%s\"", test.expectedResult, result) + return + } + }) + } + + t.Run("It should fail if file no exists", func(t *testing.T) { + // ARRANGE + repo := &fileDataDotEnvRepository{} + expectedError := "failed to read file" + + // ACT + _, err := repo.GetDataByKey("not_existed_file.env", "SOME_KEY") + + // ASSERT + if err == nil { + t.Errorf("Expected error, but got nil") + return + } + + if err.Error() != expectedError { + t.Errorf("Expected \"%s\", but got \"%s\"", expectedError, err.Error()) + return + } + }) + + t.Run("It should fail if file contains not valid dotenv", func(t *testing.T) { + // ARRANGE + repo := &fileDataDotEnvRepository{} + yml := "^INVALID VAR=VALUE" + release, filePath, err := writeStringToTmpFile(t, "test.env", yml) + if err != nil { + t.Errorf("Unexpected tmp file creation error: %s", err.Error()) + return + } + defer (func() { + _ = release() + })() + expectedError := "failed to read file" + + // ACT + _, err = repo.GetDataByKey(filePath, "VAR") + + // ASSERT + if err == nil { + t.Errorf("Expected error, but got nil") + return + } + + if err.Error() != expectedError { + t.Errorf("Expected \"%s\", but got \"%s\"", expectedError, err.Error()) + return + } + }) +} + +func Test_FileDataDonEnvRepository_SetDataByKey(t *testing.T) { + if os.Getenv("INTEGRATION_TEST") == "" { + t.Skip("skipping integration test") + } + + validSourceYaml := `root: + # top comment + simple_key: simple_key_value # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +` + + validYmlTests := []struct { + name string + yamlPath string + newValue string + valueType pb.DataSetValueType + expectedResult string + expectedErr error + }{ + { + name: "It should set value by key and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "newval", + valueType: pb.DataSetValueType_STRING_VAL, + expectedResult: `root: + # top comment + simple_key: newval # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should set int value by key and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "12", + valueType: pb.DataSetValueType_INT_VAL, + expectedResult: `root: + # top comment + simple_key: "12" # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should set float value by key and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "12.12", + valueType: pb.DataSetValueType_FLOAT_VAL, + expectedResult: `root: + # top comment + simple_key: "12.12" # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should set bool value by key and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "false", + valueType: pb.DataSetValueType_BOOL_VAL, + expectedResult: `root: + # top comment + simple_key: "False" # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should set string value as double quoted string and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "new simple val", + valueType: pb.DataSetValueType_STRING_VAL, + expectedResult: `root: + # top comment + simple_key: "new simple val" # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should fails set root value", + yamlPath: "$", + newValue: "new_simple_val", + valueType: pb.DataSetValueType_STRING_VAL, + expectedErr: errors.New("failed to set value: $ could not set scalar of root"), + }, + { + name: "It should fails set sequence value", + yamlPath: "$.root.simple_sequence", + newValue: "new_simple_val", + valueType: pb.DataSetValueType_STRING_VAL, + expectedErr: errors.New("failed to set value: $.root.simple_sequence scalar node is expected, but found mapping node"), + }, + } + + for _, test := range validYmlTests { + t.Run(test.name, func(t *testing.T) { + // ARRANGE + repo := &fileDataYmlRepository{} + release, filePath, err := writeStringToTmpFile(t, "test.yml", validSourceYaml) + if err != nil { + t.Errorf("Unexpected tmp file creation error: %s", err.Error()) + return + } + defer (func() { + _ = release() + })() + + // ACT + err = repo.SetDataByKey(filePath, test.yamlPath, test.newValue, test.valueType) + + // ASSERT + if test.expectedErr != nil && err == nil { + t.Errorf("Expected error \"%s\", but got nil", test.expectedErr) + return + } + + if test.expectedErr != nil && err != nil && test.expectedErr.Error() != err.Error() { + t.Errorf("Expected \"%s\", but got \"%s\"", test.expectedErr, err) + return + } + + updatedYmlBytes, err := os.ReadFile(filePath) + if err != nil { + t.Errorf("Unexpected marshal error: %s", err.Error()) + return + } + + if test.expectedResult != "" && string(updatedYmlBytes) != test.expectedResult { + diff := getLinesDiff(test.expectedResult, string(updatedYmlBytes)) + t.Errorf("Expected not equals updated, diff %s", diff) + return + } + + }) + } + + t.Run("It should fail if file contains not valid yml", func(t *testing.T) { + // ARRANGE + repo := &fileDataYmlRepository{} + yml := "@some: not valid yml" + release, filePath, err := writeStringToTmpFile(t, "test.yml", yml) + if err != nil { + t.Errorf("Unexpected tmp file creation error: %s", err.Error()) + return + } + defer (func() { + _ = release() + })() + expectedError := "failed to parse yaml: yaml: found character that cannot start any token" + + // ACT + err = repo.SetDataByKey(filePath, "$.root.simple_key", "new_val", pb.DataSetValueType_STRING_VAL) + + // ASSERT + if err == nil { + t.Errorf("Expected error, but got nil") + return + } + + if err.Error() != expectedError { + t.Errorf("Expected \"%s\", but got \"%s\"", expectedError, err.Error()) + return + } + }) +} diff --git a/services/localfile/server/infrastructure/output/file-data/file-data-repository.factory.go b/services/localfile/server/infrastructure/output/file-data/file-data-repository.factory.go new file mode 100644 index 00000000..bc6bab76 --- /dev/null +++ b/services/localfile/server/infrastructure/output/file-data/file-data-repository.factory.go @@ -0,0 +1,60 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 file_data + +import ( + "context" + "errors" +) +import pb "github.com/Snowflake-Labs/sansshell/services/localfile" + +// FileDataRepository is an interface to read data from file by key +type FileDataRepository interface { + // GetDataByKey Read data from file by key + GetDataByKey(filePath string, key string) (string, error) + // SetDataByKey Set data to file by key + SetDataByKey(filePath string, key string, value string, valType pb.DataSetValueType) error +} + +// FileDataRepositoryFactory is an interface to get a new [FileDataRepository] by file format +type FileDataRepositoryFactory interface { + // GetRepository Get [FileDataRepository] by file format + GetRepository(context context.Context, fileFormat pb.FileFormat) (FileDataRepository, error) +} + +// NewFileDataRepositoryFactory creates a new instance of [application.FileDataRepositoryFactory] +func NewFileDataRepositoryFactory() FileDataRepositoryFactory { + return &fileDataReaderFactory{} +} + +// fileDataReaderFactory implementation of [application.FileDataRepositoryFactory] interface +type fileDataReaderFactory struct { +} + +// GetRepository implementation of [application.FileDataRepositoryFactory.GetRepository] interface +func (f *fileDataReaderFactory) GetRepository(context context.Context, fileFormat pb.FileFormat) (FileDataRepository, error) { + switch fileFormat { + case pb.FileFormat_YML: + return newFileDataYmlRepository(context), nil + case pb.FileFormat_DOTENV: + return newDotEnvFileDataRepository(context), nil + default: + + return nil, errors.New("Unsupported file type") + } +} diff --git a/services/localfile/server/infrastructure/output/file-data/file-data-yml.repository.go b/services/localfile/server/infrastructure/output/file-data/file-data-yml.repository.go new file mode 100644 index 00000000..208f802f --- /dev/null +++ b/services/localfile/server/infrastructure/output/file-data/file-data-yml.repository.go @@ -0,0 +1,178 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 file_data + +import ( + "context" + "fmt" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + fileUtils "github.com/Snowflake-Labs/sansshell/services/util/file-utils" + stringUtils "github.com/Snowflake-Labs/sansshell/services/util/string-utils" + yaml3Utils "github.com/Snowflake-Labs/sansshell/services/util/yml-utils" + "github.com/go-logr/logr" + "github.com/pkg/errors" + "gopkg.in/yaml.v3" + "io" + "os" + "strings" +) + +// NewFileDataYmlRepository creates a new instance of [application.FileDataRepository] +func newFileDataYmlRepository(context context.Context) FileDataRepository { + return &fileDataYmlRepository{ + context: context, + } +} + +// fileDataYmlRepository implementation of [application.FileDataRepository] interface +type fileDataYmlRepository struct { + context context.Context +} + +// GetDataByKey implementation of [application.FileDataRepository.GetDataByKey] interface +func (y *fileDataYmlRepository) GetDataByKey(filePath string, key string) (string, error) { + path, err := yaml3Utils.ParseYmlPath(key) + if err != nil { + return "", errors.Wrap(err, "failed to parse key") + } + + rawYml, err := os.ReadFile(filePath) + if err != nil { + return "", errors.Wrap(err, "failed to read file content") + } + + // Parse YAML into a map + var data yaml.Node + err = yaml.Unmarshal(rawYml, &data) + if err != nil { + return "", errors.Wrap(err, "failed to parse file") + } + + val, err := path.GetScalarValueFrom(&data) + if err != nil { + return "", fmt.Errorf("failed to get value: %s", err) + } + + return val, nil +} + +// SetDataByKey implementation of [application.FileDataRepository.SetDataByKey] interface +func (y *fileDataYmlRepository) SetDataByKey(filePath string, key string, value string, valType pb.DataSetValueType) error { + path, err := yaml3Utils.ParseYmlPath(key) + if err != nil { + return fmt.Errorf("failed to parse path: %s", err) + } + + f, err := fileUtils.OpenForOverwrite(filePath) + if err != nil { + return fmt.Errorf("failed to open file: %s", err) + } + defer f.Close() // close file when done + + // Apply an exclusive lock + unlock, err := fileUtils.ExclusiveLockFile(f) + if err != nil { + return fmt.Errorf("failed to lock file: %s", err) + } + + defer (func() { + logger := logr.FromContextOrDiscard(y.context) + err := unlock() // unlock the file when done + logger.Error(err, "failed to unlock file") + })() + + reader := io.Reader(f) + yamlFile, err := io.ReadAll(reader) + if err != nil { + return fmt.Errorf("failed to read yaml file: %s", err) + } + + // Parse YAML into a map + var data yaml.Node + err = yaml.Unmarshal(yamlFile, &data) + if err != nil { + return fmt.Errorf("failed to parse yaml: %s", err) + } + + ymlVal, ymlStyle, err := toYamlVal(value, valType) + if err != nil { + return fmt.Errorf("failed to convert value: %s", err) + } + + err = path.SetScalarValueTo(&data, ymlVal, ymlStyle) + if err != nil { + return fmt.Errorf("failed to set value: %s", err) + } + + // Marshal the modified data back to YAML + modifiedYAML, err := yaml.Marshal(data.Content[0]) + if err != nil { + return fmt.Errorf("failed to marshal yaml: %s", err) + } + + // Clear the file content + err = f.Truncate(0) + if err != nil { + return fmt.Errorf("failed to truncate file: %s", err) + } + + // Move the file pointer to the beginning + _, err = f.Seek(0, 0) + if err != nil { + return fmt.Errorf("failed to seek file: %s", err) + } + + // write update content + _, err = f.Write(modifiedYAML) + if err != nil { + return fmt.Errorf("failed to write to file: %s", err) + } + + // Sync the file to disk + err = f.Sync() + if err != nil { + return fmt.Errorf("failed to sync file: %s", err) + } + + return nil +} + +func toYamlVal(val string, t pb.DataSetValueType) (string, yaml.Style, error) { + switch t { + case pb.DataSetValueType_BOOL_VAL: + if strings.ToLower(val) == "true" { + return "True", yaml.FlowStyle, nil + } + + if strings.ToLower(val) == "false" { + return "False", yaml.FlowStyle, nil + } + + return "", 0, fmt.Errorf("invalid boolean value only \"true\" or \"false\" is expected") + case pb.DataSetValueType_INT_VAL, pb.DataSetValueType_FLOAT_VAL: + return val, yaml.FlowStyle, nil + case pb.DataSetValueType_STRING_VAL: + if !stringUtils.IsAlphanumeric(val) { + return val, yaml.DoubleQuotedStyle, nil + } + + return val, yaml.FlowStyle, nil + } + + return "", 0, fmt.Errorf("unsupported value type: %s", t.String()) +} diff --git a/services/localfile/server/infrastructure/output/file-data/file-data-yml.repository_integration_test.go b/services/localfile/server/infrastructure/output/file-data/file-data-yml.repository_integration_test.go new file mode 100644 index 00000000..618310f8 --- /dev/null +++ b/services/localfile/server/infrastructure/output/file-data/file-data-yml.repository_integration_test.go @@ -0,0 +1,393 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 file_data + +import ( + "errors" + pb "github.com/Snowflake-Labs/sansshell/services/localfile" + "os" + "testing" +) + +func Test_FileDataYmlRepository_GetDataByKey(t *testing.T) { + if os.Getenv("INTEGRATION_TEST") == "" { + t.Skip("skipping integration test") + } + + validYml := ` +root: + # top comment + simple_key: simple_key_value # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + nested_sequence: # simple sequence comment + - nested_sequence_value_1: + nested_sequence_key_1: nested_sequence_value_1_key_1 + nested_sequence_key_2: nested_sequence_value_1_key_2 + - nested_sequence_value_2: + nested_sequence_key_1: nested_sequence_value_2_key_1 + nested_sequence_key_2: nested_sequence_value_2_key_2 + - nested_sequence_value_3: + nested_sequence_key_1: nested_sequence_value_3_key_1 + nested_sequence_key_2: nested_sequence_value_3_key_2 + # bottom comment +` + + validYmlTests := []struct { + name string + key string + expectedResult string + expectedErr error + }{ + { + name: "It should fail in case of invalid path", + key: "$.root.[[0]", + expectedErr: errors.New("failed to parse key: root.[[0] invalid path, too many \"[\" in key"), + }, + { + name: "It should get value from sequence by key", + key: "$.root.simple_key", + expectedResult: "simple_key_value", + expectedErr: nil, + }, + { + name: "It should get value from sequence by index", + key: "$.root.simple_sequence[2]", + expectedResult: "simple_sequence_value_3", + expectedErr: nil, + }, + { + name: "It should get value by complex path with mixed nesting", + key: "$.root.nested_sequence[1].nested_sequence_value_2.nested_sequence_key_2", + expectedResult: "nested_sequence_value_2_key_2", + expectedErr: nil, + }, + { + name: "It should return error on get if simple key not exists", + key: "$.root.not_existed_simple_key", + expectedErr: errors.New("failed to get value: $.root.not_existed_simple_key there is no value by key"), + }, + { + name: "It should return error of index out of range", + key: "$.root.simple_sequence[3]", + expectedErr: errors.New("failed to get value: $.root.simple_sequence[3] index out of sequence range"), + }, + { + name: "It should return error on get map key from sequence", + key: "$.root.simple_sequence.some_key", + expectedErr: errors.New("failed to get value: $.root.simple_sequence mapping node expected, but found sequence node"), + }, + { + name: "It should return error on get sequence node", + key: "$.root.simple_sequence", + expectedErr: errors.New("failed to get value: $.root.simple_sequence scalar node is expected, but found mapping node"), + }, + { + name: "It should return error on get mapping node", + key: "$.root", + expectedErr: errors.New("failed to get value: $.root scalar node is expected, but found mapping node"), + }, + { + name: "It should return error on root", + key: "$", + expectedErr: errors.New("failed to get value: $ could not get scalar of root"), + }, + } + + for _, test := range validYmlTests { + t.Run(test.name, func(t *testing.T) { + // ARRANGE + repo := &fileDataYmlRepository{} + release, filePath, err := writeStringToTmpFile(t, "test.yml", validYml) + if err != nil { + t.Errorf("Unexpected tmp file creation error: %s", err.Error()) + return + } + defer (func() { + _ = release() + })() + + // ACT + result, err := repo.GetDataByKey(filePath, test.key) + + // ASSERT + if test.expectedErr != nil && err == nil { + t.Errorf("Expected error \"%s\", but got nil", test.expectedErr) + return + } + + if test.expectedErr != nil && err != nil && test.expectedErr.Error() != err.Error() { + t.Errorf("Expected \"%s\", but got \"%s\"", test.expectedErr, err) + return + } + + if result != test.expectedResult { + t.Errorf("Expected \"%s\", but got \"%s\"", test.expectedResult, result) + return + } + }) + } + + t.Run("It should fail if file no exists", func(t *testing.T) { + // ARRANGE + repo := &fileDataYmlRepository{} + expectedError := "failed to read file content: open not_existed_file.yml: no such file or directory" + + // ACT + _, err := repo.GetDataByKey("not_existed_file.yml", "$.root.simple_key") + + // ASSERT + if err == nil { + t.Errorf("Expected error, but got nil") + return + } + + if err.Error() != expectedError { + t.Errorf("Expected \"%s\", but got \"%s\"", expectedError, err.Error()) + return + } + }) + + t.Run("It should fail if file contains not valid yml", func(t *testing.T) { + // ARRANGE + repo := &fileDataYmlRepository{} + yml := "@some: not valid yml" + release, filePath, err := writeStringToTmpFile(t, "test.yml", yml) + if err != nil { + t.Errorf("Unexpected tmp file creation error: %s", err.Error()) + return + } + defer (func() { + _ = release() + })() + expectedError := "failed to parse file: yaml: found character that cannot start any token" + + // ACT + _, err = repo.GetDataByKey(filePath, "$.some") + + // ASSERT + if err == nil { + t.Errorf("Expected error, but got nil") + return + } + + if err.Error() != expectedError { + t.Errorf("Expected \"%s\", but got \"%s\"", expectedError, err.Error()) + return + } + }) +} + +func Test_FileDataYmlRepository_SetDataByKey(t *testing.T) { + if os.Getenv("INTEGRATION_TEST") == "" { + t.Skip("skipping integration test") + } + + validSourceYaml := `root: + # top comment + simple_key: simple_key_value # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +` + + validYmlTests := []struct { + name string + yamlPath string + newValue string + valueType pb.DataSetValueType + expectedResult string + expectedErr error + }{ + { + name: "It should set value by key and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "newval", + valueType: pb.DataSetValueType_STRING_VAL, + expectedResult: `root: + # top comment + simple_key: newval # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should set int value by key and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "12", + valueType: pb.DataSetValueType_INT_VAL, + expectedResult: `root: + # top comment + simple_key: "12" # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should set float value by key and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "12.12", + valueType: pb.DataSetValueType_FLOAT_VAL, + expectedResult: `root: + # top comment + simple_key: "12.12" # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should set bool value by key and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "false", + valueType: pb.DataSetValueType_BOOL_VAL, + expectedResult: `root: + # top comment + simple_key: "False" # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should set string value as double quoted string and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "new simple val", + valueType: pb.DataSetValueType_STRING_VAL, + expectedResult: `root: + # top comment + simple_key: "new simple val" # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should fails set root value", + yamlPath: "$", + newValue: "new_simple_val", + valueType: pb.DataSetValueType_STRING_VAL, + expectedErr: errors.New("failed to set value: $ could not set scalar of root"), + }, + { + name: "It should fails set sequence value", + yamlPath: "$.root.simple_sequence", + newValue: "new_simple_val", + valueType: pb.DataSetValueType_STRING_VAL, + expectedErr: errors.New("failed to set value: $.root.simple_sequence scalar node is expected, but found mapping node"), + }, + } + + for _, test := range validYmlTests { + t.Run(test.name, func(t *testing.T) { + // ARRANGE + repo := &fileDataYmlRepository{} + release, filePath, err := writeStringToTmpFile(t, "test.yml", validSourceYaml) + if err != nil { + t.Errorf("Unexpected tmp file creation error: %s", err.Error()) + return + } + defer (func() { + _ = release() + })() + + // ACT + err = repo.SetDataByKey(filePath, test.yamlPath, test.newValue, test.valueType) + + // ASSERT + if test.expectedErr != nil && err == nil { + t.Errorf("Expected error \"%s\", but got nil", test.expectedErr) + return + } + + if test.expectedErr != nil && err != nil && test.expectedErr.Error() != err.Error() { + t.Errorf("Expected \"%s\", but got \"%s\"", test.expectedErr, err) + return + } + + updatedYmlBytes, err := os.ReadFile(filePath) + if err != nil { + t.Errorf("Unexpected marshal error: %s", err.Error()) + return + } + + if test.expectedResult != "" && string(updatedYmlBytes) != test.expectedResult { + diff := getLinesDiff(test.expectedResult, string(updatedYmlBytes)) + t.Errorf("Expected not equals updated, diff %s", diff) + return + } + + }) + } + + t.Run("It should fail if file contains not valid yml", func(t *testing.T) { + // ARRANGE + repo := &fileDataYmlRepository{} + yml := "@some: not valid yml" + release, filePath, err := writeStringToTmpFile(t, "test.yml", yml) + if err != nil { + t.Errorf("Unexpected tmp file creation error: %s", err.Error()) + return + } + defer (func() { + _ = release() + })() + expectedError := "failed to parse yaml: yaml: found character that cannot start any token" + + // ACT + err = repo.SetDataByKey(filePath, "$.root.simple_key", "new_val", pb.DataSetValueType_STRING_VAL) + + // ASSERT + if err == nil { + t.Errorf("Expected error, but got nil") + return + } + + if err.Error() != expectedError { + t.Errorf("Expected \"%s\", but got \"%s\"", expectedError, err.Error()) + return + } + }) +} diff --git a/services/localfile/server/infrastructure/output/file-data/test.utils_test.go b/services/localfile/server/infrastructure/output/file-data/test.utils_test.go new file mode 100644 index 00000000..ac22cd01 --- /dev/null +++ b/services/localfile/server/infrastructure/output/file-data/test.utils_test.go @@ -0,0 +1,74 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 file_data + +import ( + "fmt" + "os" + "strings" + "testing" +) + +func writeStringToTmpFile(t *testing.T, fileName string, content string) (func() error, string, error) { + testName := strings.ReplaceAll(t.Name(), " ", "_") + testName = strings.ReplaceAll(testName, "/", "__") + filePath := fmt.Sprintf("./__testdata__/%s__%s", testName, fileName) + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return nil, "", fmt.Errorf("failed to open file: %s", err) + } + defer file.Close() + + _, err = file.WriteString(content) + if err != nil { + return nil, "", fmt.Errorf("failed to write to file: %s", err) + } + + return func() error { + return os.Remove(filePath) + }, filePath, nil +} + +func getLinesDiff(expected string, actual string) string { + expectedLines := strings.Split(expected, "\n") + actualLines := strings.Split(actual, "\n") + var diffLines []string + for i := 0; i < min(len(expectedLines), len(actualLines)); i++ { + line := expectedLines[i] + actualLine := actualLines[i] + + if line == actualLine { + diffLines = append(diffLines, line) + } else { + diffLines = append(diffLines, fmt.Sprintf("-%s", line)) + diffLines = append(diffLines, fmt.Sprintf("+%s", actualLine)) + } + } + + if len(expectedLines) > len(actualLines) { + diffLines = append(diffLines, expectedLines[len(actualLines):]...) + } + + if len(actualLines) > len(expectedLines) { + for _, line := range actualLines[len(expectedLines):] { + diffLines = append(diffLines, fmt.Sprintf("+%s", line)) + } + } + + return strings.Join(diffLines, "\n") +} diff --git a/services/localfile/server/localfile.go b/services/localfile/server/localfile.go index 9c17b7e8..0b6846da 100644 --- a/services/localfile/server/localfile.go +++ b/services/localfile/server/localfile.go @@ -24,6 +24,8 @@ import ( "crypto/sha512" "encoding/hex" "fmt" + app "github.com/Snowflake-Labs/sansshell/services/localfile/server/application" + "github.com/Snowflake-Labs/sansshell/services/localfile/server/infrastructure/output/file-data" "hash" "hash/crc32" "io" @@ -93,6 +95,10 @@ var ( Description: "number of failures when performing localfile.SetFileAttribute"} localfileMkdirFailureCounter = metrics.MetricDefinition{Name: "actions_localfile_mkdir_failure", Description: "number of failures when performing localfile.Mkdir"} + localfileDataGetFailureCounter = metrics.MetricDefinition{Name: "actions_localfile_dataget_failure", + Description: "number of failures when performing localfile.DataGet"} + localfileDataSetFailureCounter = metrics.MetricDefinition{Name: "actions_localfile_dataset_failure", + Description: "number of failures when performing localfile.DataSet"} ) // This encompasses the permission plus the setuid/gid/sticky bits one @@ -830,6 +836,65 @@ func (s *server) Mkdir(ctx context.Context, req *pb.MkdirRequest) (_ *emptypb.Em return &emptypb.Empty{}, nil } +func (s *server) DataGet(ctx context.Context, req *pb.DataGetRequest) (*pb.DataGetReply, error) { + logger := logr.FromContextOrDiscard(ctx) + recorder := metrics.RecorderFromContextOrNoop(ctx) + logger.Info("Data Get request", "filename:", req.Filename, "fileformat:", req.FileFormat, "datakey:", req.DataKey) + + fileFactory := file_data.NewFileDataRepositoryFactory() + usecase := app.NewDataGetUsecase(fileFactory) + + val, err := usecase.Run(ctx, req.Filename, req.DataKey, req.FileFormat) + if err != nil { + switch err.Code() { + case app.DataGetErrorCodes_FilePathInvalid: + recorder.CounterOrLog(ctx, localfileDataGetFailureCounter, 1, attribute.String("reason", "invalid_path")) + return nil, status.Error(codes.InvalidArgument, err.Error()) + case app.DataGetErrorCodes_FileFormatNotSupported: + recorder.CounterOrLog(ctx, localfileDataGetFailureCounter, 1, attribute.String("reason", "unsupported_file_format")) + return nil, status.Error(codes.InvalidArgument, err.Error()) + case app.DataGetErrorCodes_CouldNotGetData: + recorder.CounterOrLog(ctx, localfileDataGetFailureCounter, 1, attribute.String("reason", "could_not_get_data")) + return nil, status.Error(codes.Internal, err.Error()) + } + + recorder.CounterOrLog(ctx, localfileDataGetFailureCounter, 1, attribute.String("reason", "unknown_err")) + return nil, status.Error(codes.Unknown, err.Error()) + } + + logger.Info("Data Get response", "value:", val) + return &pb.DataGetReply{Value: val}, nil +} + +func (s *server) DataSet(ctx context.Context, req *pb.DataSetRequest) (*emptypb.Empty, error) { + logger := logr.FromContextOrDiscard(ctx) + recorder := metrics.RecorderFromContextOrNoop(ctx) + logger.Info("Data Set request", "filename:", req.Filename, "FileFormat:", req.FileFormat, "DataKey:", req.DataKey, "ValueType:", req.ValueType, "Value:", req.Value) + + fileFactory := file_data.NewFileDataRepositoryFactory() + usecase := app.NewDataSetUsecase(fileFactory) + + err := usecase.Run(ctx, req.Filename, req.DataKey, req.FileFormat, req.Value, req.ValueType) + if err != nil { + switch err.Code() { + case app.DataSetErrorCodes_FilePathInvalid: + recorder.CounterOrLog(ctx, localfileDataSetFailureCounter, 1, attribute.String("reason", "invalid_path")) + return nil, status.Error(codes.InvalidArgument, err.Error()) + case app.DataSetErrorCodes_FileFormatNotSupported: + recorder.CounterOrLog(ctx, localfileDataSetFailureCounter, 1, attribute.String("reason", "unsupported_file_format")) + return nil, status.Error(codes.InvalidArgument, err.Error()) + case app.DataSetErrorCodes_CouldNotSetData: + recorder.CounterOrLog(ctx, localfileDataSetFailureCounter, 1, attribute.String("reason", "could_not_set_data")) + return nil, status.Error(codes.Internal, err.Error()) + } + + recorder.CounterOrLog(ctx, localfileDataGetFailureCounter, 1, attribute.String("reason", "unknown_err")) + return nil, status.Error(codes.Unknown, err.Error()) + } + + return &emptypb.Empty{}, nil +} + // Register is called to expose this handler to the gRPC server func (s *server) Register(gs *grpc.Server) { pb.RegisterLocalFileServer(gs, s) diff --git a/services/util/array-utils/array.utils.go b/services/util/array-utils/array.utils.go new file mode 100644 index 00000000..cdf6d501 --- /dev/null +++ b/services/util/array-utils/array.utils.go @@ -0,0 +1,27 @@ +/* Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 array_utils + +// FindIndexBy finds the index of value in array +func FindIndexBy[T comparable](arr []T, predicate func(val T) bool) int { + for i, v := range arr { + if predicate(v) { + return i + } + } + return -1 +} diff --git a/services/util/array-utils/array.utils_test.go b/services/util/array-utils/array.utils_test.go new file mode 100644 index 00000000..e39c4f23 --- /dev/null +++ b/services/util/array-utils/array.utils_test.go @@ -0,0 +1,56 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + +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 array_utils + +import ( + "testing" +) + +func Test_FindIndexBy(t *testing.T) { + tests := []struct { + name string + arr []int + predicate func(val int) bool + expectedResult int + }{ + { + name: "It should return index of value in array", + arr: []int{1, 2, 3, 4, 5}, + predicate: func(val int) bool { return val == 3 }, + expectedResult: 2, + }, + { + name: "It should return -1 if value is not in array", + arr: []int{1, 2, 3, 4, 5}, + predicate: func(val int) bool { return val == 6 }, + expectedResult: -1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // ACT + result := FindIndexBy(test.arr, test.predicate) + + // ASSERT + if result != test.expectedResult { + t.Errorf("Expected %d, but got %d", test.expectedResult, result) + } + }) + } +} diff --git a/services/util/cli/styled-cli-logger.go b/services/util/cli/styled-cli-logger.go index 28bd1413..317806f2 100644 --- a/services/util/cli/styled-cli-logger.go +++ b/services/util/cli/styled-cli-logger.go @@ -24,10 +24,10 @@ import ( type ColorCode = string -var restoreFormatingCode ColorCode = "\033[0m" -var RedText ColorCode = "\033[31m" -var GreenText ColorCode = "\033[32m" -var YellowText ColorCode = "\033[33m" +var restoreFormatingCode = "\033[0m" +var RedText = "\033[31m" +var GreenText = "\033[32m" +var YellowText = "\033[33m" type styledCliLogger struct { out io.Writer @@ -158,3 +158,11 @@ func Colorizef(color ColorCode, format string, a ...any) StyledText { colorCode: color, } } + +func CRed(a any) StyledText { + return Colorize(RedText, a) +} + +func CGreen(a any) StyledText { + return Colorize(GreenText, a) +} diff --git a/services/util/cli/styled-cli-logger_test.go b/services/util/cli/styled-cli-logger_test.go index 2ca57693..7b899fa5 100644 --- a/services/util/cli/styled-cli-logger_test.go +++ b/services/util/cli/styled-cli-logger_test.go @@ -84,6 +84,54 @@ func TestColorizef(t *testing.T) { }) } +func TestCRed(t *testing.T) { + t.Run("It should convert string to red styled text", func(t *testing.T) { + // ARRANGE + text := "Some text" + + // ACT + colorized := CRed(text) + + // ASSERT + styled, ok := colorized.(*styledText) + if ok == false { + t.Errorf("Expected to get styledText, but got %T", colorized) + } + + if styled.text != text { + t.Errorf("Got %s, but expected %s", styled.text, text) + } + + if styled.colorCode != RedText { + t.Errorf("Got %s, but expected %s", styled.colorCode, RedText) + } + }) +} + +func TestCGreen(t *testing.T) { + t.Run("It should convert string to green styled text", func(t *testing.T) { + // ARRANGE + text := "Some text" + + // ACT + colorized := CGreen(text) + + // ASSERT + styled, ok := colorized.(*styledText) + if ok == false { + t.Errorf("Expected to get styledText, but got %T", colorized) + } + + if styled.text != text { + t.Errorf("Got %s, but expected %s", styled.text, text) + } + + if styled.colorCode != GreenText { + t.Errorf("Got %s, but expected %s", styled.colorCode, GreenText) + } + }) +} + func Test_styledCliLogger_Infof(t *testing.T) { tests := []struct { name string diff --git a/services/util/error-utils/errors.go b/services/util/error-utils/errors.go new file mode 100644 index 00000000..ecd79232 --- /dev/null +++ b/services/util/error-utils/errors.go @@ -0,0 +1,59 @@ +/* Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 error_utils + +import "fmt" + +// ErrorWithCode defines a custom error type with a code. +type ErrorWithCode[T comparable] interface { + error + Code() T +} + +// NewErrorWithCode creates a new ErrorWithCode with the given code and message. +func NewErrorWithCode[T comparable](code T, message string) ErrorWithCode[T] { + return &errorWithCode[T]{ + code: code, + Message: message, + } +} + +// NewErrorWithCodef creates a new ErrorWithCode with the given code and formatted message. +func NewErrorWithCodef[T comparable](code T, format string, args ...any) ErrorWithCode[T] { + return NewErrorWithCode[T](code, fmt.Sprintf(format, args...)) +} + +// errorWithCode implements the ErrorWithCode interface. +type errorWithCode[T comparable] struct { + code T + Message string +} + +// Error implements the error interface for ErrorWithCode. +func (e *errorWithCode[T]) Error() string { + return fmt.Sprintf("[%v]: %s", e.code, e.Message) +} + +// Code implements the [ErrorWithCode.Code] method. +func (e *errorWithCode[T]) Code() T { + return e.code +} + +// String implements the [fmt.Stringer] interface for ErrorWithCode. +func (e *errorWithCode[T]) String() string { + return fmt.Sprintf("[%v] %s", e.code, e.Message) +} diff --git a/services/util/file-utils/__testdata__/TestIntegrationOpenForOverwrite.test b/services/util/file-utils/__testdata__/TestIntegrationOpenForOverwrite.test new file mode 100644 index 00000000..70c379b6 --- /dev/null +++ b/services/util/file-utils/__testdata__/TestIntegrationOpenForOverwrite.test @@ -0,0 +1 @@ +Hello world \ No newline at end of file diff --git a/services/util/file-utils/file.utils.go b/services/util/file-utils/file.utils.go new file mode 100644 index 00000000..92db1dc4 --- /dev/null +++ b/services/util/file-utils/file.utils.go @@ -0,0 +1,54 @@ +/* Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 file_utils + +import ( + "fmt" + "os" + "syscall" +) + +// OpenForOverwrite opens a file for writing, truncating the file if it already exists. +func OpenForOverwrite(filePath string) (*os.File, error) { + fileInfo, err := os.Stat(filePath) + if err != nil { + return nil, fmt.Errorf("failed to get file info: %s", err.Error()) + } + mode := fileInfo.Mode() + + // Open the file in read-write mode + f, err := os.OpenFile(filePath, os.O_RDWR, mode) + if err != nil { + return nil, fmt.Errorf("failed to open file: %s", err) + } + + return f, nil +} + +// ExclusiveLockFile applies an exclusive lock to a file. +// Returns a function to unlock the file. +func ExclusiveLockFile(f *os.File) (func() error, error) { + // Apply an exclusive lock + if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { + return nil, fmt.Errorf("failed to lock file: %s", err) + } + + // Return a function to unlock the file + return func() error { + return syscall.Flock(int(f.Fd()), syscall.LOCK_UN) + }, nil +} diff --git a/services/util/file-utils/file.utils_integration_test.go b/services/util/file-utils/file.utils_integration_test.go new file mode 100644 index 00000000..40865488 --- /dev/null +++ b/services/util/file-utils/file.utils_integration_test.go @@ -0,0 +1,222 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 file_utils + +import ( + "io" + "os" + "sync" + "testing" + "time" +) + +func TestIntegration_OpenForOverwrite(t *testing.T) { + if os.Getenv("INTEGRATION_TEST") == "" { + t.Skip("skipping integration test") + } + + t.Run("It should open file and being able read it", func(t *testing.T) { + // ARRANGE + filePath := "./__testdata__/TestIntegrationOpenForOverwrite.test" + expectedFileContent := "Hello world" + + // ACT + f, err := OpenForOverwrite(filePath) + if err != nil { + t.Errorf("Unexpected open error: %s", err.Error()) + return + } + defer f.Close() + + reader := io.Reader(f) + result, err := io.ReadAll(reader) + + // ASSERT + if err != nil { + t.Errorf("Unexpected read error: %s", err.Error()) + return + } + + text := string(result) + if text != expectedFileContent { + t.Errorf("Expected \"%s\", but got \"%s\"", expectedFileContent, text) + return + } + }) + + t.Run("It should overwrite file, but keep the same mode", func(t *testing.T) { + // ARRANGE + sourceFilePath := "./__testdata__/TestIntegrationOpenForOverwrite.test" + newFileContent := "new content" + expectedFileMode := os.FileMode(0767) + + // make source file tmp copy + sourceFile, err := os.Open(sourceFilePath) + if err != nil { + t.Errorf("Unexpected open source file error: %s", err.Error()) + return + } + tmpPath := "./__testdata__/tmp_TestIntegrationOpenForOverwrite.test" + destinationFile, err := os.Create(tmpPath) + if err != nil { + t.Errorf("Unexpected create tmp file error: %s", err.Error()) + return + } + _, err = io.Copy(destinationFile, sourceFile) + if err != nil { + t.Errorf("Unexpected copy file error: %s", err.Error()) + } + + err = destinationFile.Sync() + if err != nil { + t.Errorf("Unexpected sync file error: %s", err.Error()) + return + } + + err = sourceFile.Close() + if err != nil { + t.Errorf("Unexpected close source file error: %s", err.Error()) + return + } + err = destinationFile.Close() + if err != nil { + t.Errorf("Unexpected close destination file error: %s", err.Error()) + return + } + defer (func() { + err := os.Remove(tmpPath) + if err != nil { + t.Errorf("Unexpected remove tmp file error: %s", err.Error()) + return + } + })() + // set for file expected file mode + err = os.Chmod(tmpPath, expectedFileMode) + if err != nil { + t.Errorf("Unexpected chmod error: %s", err.Error()) + return + } + + // ACT + f, err := OpenForOverwrite(tmpPath) + if err != nil { + t.Errorf("Unexpected open error: %s", err.Error()) + return + } + + // write new content + _, err = f.Write([]byte(newFileContent)) + if err != nil { + t.Errorf("Unexpected write error: %s", err.Error()) + return + } + + err = f.Close() + if err != nil { + t.Errorf("Unexpected close error: %s", err.Error()) + return + } + + // ASSERT + // get file mode + fileInfo, err := os.Stat(tmpPath) + if err != nil { + t.Errorf("Unexpected stat error: %s", err.Error()) + return + } + + if fileInfo.Mode() != expectedFileMode { + t.Errorf("Expected %s, but got %s", expectedFileMode, fileInfo.Mode()) + return + } + + // read file content + f, err = os.Open(tmpPath) + if err != nil { + t.Errorf("Unexpected open error: %s", err.Error()) + return + } + defer f.Close() + + content := make([]byte, len(newFileContent)) + _, err = f.Read(content) + if err != nil { + t.Errorf("Unexpected read error: %s", err.Error()) + return + } + + if string(content) != newFileContent { + t.Errorf("Expected %s, but got %s", newFileContent, content) + return + } + }) +} + +func TestIntegration_ExclusiveLockFile(t *testing.T) { + if os.Getenv("INTEGRATION_TEST") == "" { + t.Skip("skipping integration test") + } + + t.Run("It should lock file second time only, when first lock was released", func(t *testing.T) { + // ARRANGE + filePath := "./__testdata__/TestIntegrationOpenForOverwrite.test" + + // preparations + f, err := os.Open(filePath) + if err != nil { + t.Errorf("Unexpected open source file error: %s", err.Error()) + return + } + defer f.Close() + + // ACT + var wg sync.WaitGroup + var firstUnlockTime time.Time + var secondLockTime time.Time + + wg.Add(2) + go (func() { + releaseFn, err := ExclusiveLockFile(f) + if err != nil { + t.Errorf("Unexpected lock error: %s", err.Error()) + return + } + time.Sleep(5 * time.Second) // Simulate work + + firstUnlockTime = time.Now() + _ = releaseFn() + wg.Done() + })() + go (func() { + releaseFn, err := ExclusiveLockFile(f) + secondLockTime = time.Now() + if err != nil { + t.Errorf("Unexpected lock error: %s", err.Error()) + return + } + _ = releaseFn() + wg.Done() + })() + wg.Wait() + + // ASSERT + if secondLockTime.After(firstUnlockTime) { + t.Errorf("Second lock should happen after first unlock, but got %s and %s", firstUnlockTime, secondLockTime) + return + } + }) +} diff --git a/services/util/string-utils/string.utils.go b/services/util/string-utils/string.utils.go new file mode 100644 index 00000000..314f4b02 --- /dev/null +++ b/services/util/string-utils/string.utils.go @@ -0,0 +1,28 @@ +/* Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 string_utils + +import "unicode" + +func IsAlphanumeric(str string) bool { + for _, char := range str { + if !unicode.IsLetter(char) && !unicode.IsDigit(char) { + return false + } + } + return true +} diff --git a/services/util/string-utils/string.utils_test.go b/services/util/string-utils/string.utils_test.go new file mode 100644 index 00000000..a890fa43 --- /dev/null +++ b/services/util/string-utils/string.utils_test.go @@ -0,0 +1,67 @@ +/* Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 string_utils + +import ( + "testing" +) + +func Test_IsAlphanumeric(t *testing.T) { + tests := []struct { + name string + input string + expectedResult bool + }{ + { + name: "It should return true if the string is alphanumeric", + input: "alphanumeric123", + expectedResult: true, + }, + { + name: "It should return true if the string is alpha", + input: "alpha", + expectedResult: true, + }, + { + name: "It should return true if the string is numeric", + input: "1234567890", + expectedResult: true, + }, + { + name: "It should return false if the string include space", + input: "not alphanumeric123", + expectedResult: false, + }, + { + name: "It should return false if the string include special character", + input: "not-alphanumeric123!", + expectedResult: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // ACT + result := IsAlphanumeric(test.input) + + // ASSERT + if result != test.expectedResult { + t.Errorf("Expected %t, but got %t", test.expectedResult, result) + } + }) + } +} diff --git a/services/util/yml-utils/yaml3.utils.go b/services/util/yml-utils/yaml3.utils.go new file mode 100644 index 00000000..0d103a33 --- /dev/null +++ b/services/util/yml-utils/yaml3.utils.go @@ -0,0 +1,424 @@ +/* Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 yml_utils + +import ( + "errors" + "fmt" + "gopkg.in/yaml.v3" + "strconv" + "strings" +) + +// Yaml3PathNode is a node of YmlPath +type Yaml3PathNode interface { + GetPrev() Yaml3PathNode + SetPrev(prev Yaml3PathNode) + GetNext() Yaml3PathNode + SetNext(next Yaml3PathNode) + GetScalarValueFrom(node *yaml.Node) (string, error) + SetScalarValueTo(node *yaml.Node, value string, style yaml.Style) error +} + +////////////////////////////////////////////////////////////////////////////////////// +// Implementation of [yaml3PathGetFromRoot] +////////////////////////////////////////////////////////////////////////////////////// + +// yaml3PathGetFromRoot root node of YmlPath, implementation of [Yaml3PathNode] interface +// e.g. for path "$.items[0]" it would be representation of "$" +type yaml3PathGetFromRoot struct { + next Yaml3PathNode +} + +// GetScalarValueFrom implements [Yaml3PathNode.GetScalarValueFrom] interface +func (p *yaml3PathGetFromRoot) GetScalarValueFrom(node *yaml.Node) (string, error) { + if p.next == nil { + return "", fmt.Errorf("%s could not get scalar of root", getPathToNode(p)) + } + + childNode, err := p.getRelevantNode(node) + if err != nil { + return "", err + } + + return p.next.GetScalarValueFrom(childNode) +} + +// SetScalarValueTo implements [Yaml3PathNode.SetScalarValueTo] interface +func (p *yaml3PathGetFromRoot) SetScalarValueTo(node *yaml.Node, value string, style yaml.Style) error { + childNode, err := p.getRelevantNode(node) + if err != nil { + return err + } + + if p.next == nil { + return fmt.Errorf("%s could not set scalar of root", getPathToNode(p)) + } + + return p.next.SetScalarValueTo(childNode, value, style) +} + +// SetNext implements [Yaml3PathNode.SetNext] interface +func (p *yaml3PathGetFromRoot) SetNext(next Yaml3PathNode) { + p.next = next + if next.GetPrev() != p { + next.SetPrev(p) + } +} + +// SetPrev implements [Yaml3PathNode.SetPrev] interface +func (p *yaml3PathGetFromRoot) SetPrev(prev Yaml3PathNode) { + panic("could not set prev for root") +} + +// GetPrev implements [Yaml3PathNode.GetPrev] interface +func (p *yaml3PathGetFromRoot) GetPrev() Yaml3PathNode { + return nil +} + +// GetNext implements [Yaml3PathNode.GetNext] interface +func (p *yaml3PathGetFromRoot) GetNext() Yaml3PathNode { + return p.next +} + +func (p *yaml3PathGetFromRoot) getRelevantNode(node *yaml.Node) (*yaml.Node, error) { + if node.Kind != yaml.DocumentNode { + return nil, fmt.Errorf("%s document node is expected", getPathToNode(p)) + } + + if len(node.Content) == 0 { + return nil, fmt.Errorf("%s could not get value from empty root", getPathToNode(p)) + } + + return node.Content[0], nil +} + +////////////////////////////////////////////////////////////////////////////////////// +// Implementation of [yaml3PathGetByIndex] +////////////////////////////////////////////////////////////////////////////////////// + +// yaml3PathGetByIndex get item from sequence node of YmlPath, implementation of [Yaml3PathNode] interface +// e.g. for path "$.items[0]" it would be representation of "[0]" +type yaml3PathGetByIndex struct { + next Yaml3PathNode + prev Yaml3PathNode + index int +} + +// GetScalarValueFrom implements [Yaml3PathNode.GetScalarValueFrom] interface +func (p *yaml3PathGetByIndex) GetScalarValueFrom(node *yaml.Node) (string, error) { + childNode, err := p.getRelevantNode(node) + if err != nil { + return "", err + } + + if p.next != nil { + return p.next.GetScalarValueFrom(childNode) + } + + if childNode.Kind != yaml.ScalarNode { + return "", fmt.Errorf("%s scalar node is expected, but found %s", getPathToNode(p), kindToString(childNode)) + } + + return childNode.Value, nil +} + +// SetScalarValueTo implements [Yaml3PathNode.SetScalarValueTo] interface +func (p *yaml3PathGetByIndex) SetScalarValueTo(node *yaml.Node, value string, style yaml.Style) error { + childNode, err := p.getRelevantNode(node) + if err != nil { + return err + } + + if p.next != nil { + return p.next.SetScalarValueTo(childNode, value, style) + } + + if childNode.Kind != yaml.ScalarNode { + return fmt.Errorf("%s scalar node is expected, but found %s", getPathToNode(p), kindToString(childNode)) + } + + childNode.Value = value + childNode.Style = style + return nil +} + +// SetNext implements [Yaml3PathNode.SetNext] interface +func (p *yaml3PathGetByIndex) SetNext(next Yaml3PathNode) { + p.next = next + if next.GetPrev() != p { + next.SetPrev(p) + } +} + +// SetPrev implements [Yaml3PathNode.SetPrev] interface +func (p *yaml3PathGetByIndex) SetPrev(prev Yaml3PathNode) { + p.prev = prev + if prev.GetNext() != p { + prev.SetNext(p) + } +} + +// GetPrev implements [Yaml3PathNode.GetPrev] interface +func (p *yaml3PathGetByIndex) GetPrev() Yaml3PathNode { + return p.prev +} + +// GetNext implements [Yaml3PathNode.GetNext] interface +func (p *yaml3PathGetByIndex) GetNext() Yaml3PathNode { + return p.next +} + +func (p *yaml3PathGetByIndex) getRelevantNode(node *yaml.Node) (*yaml.Node, error) { + if node.Kind != yaml.SequenceNode { + return nil, fmt.Errorf("%s sequence node expected, but found %s", getPathToNode(p.GetPrev()), kindToString(node)) + } + + if p.index >= len(node.Content) { + return nil, fmt.Errorf("%s index out of sequence range", getPathToNode(p)) + } + + childNode := node.Content[p.index] + return childNode, nil +} + +////////////////////////////////////////////////////////////////////////////////////// +// Implementation of [yaml3PathGetByKey] +////////////////////////////////////////////////////////////////////////////////////// + +// yaml3PathGetByKey get item from mapping node of YmlPath, implementation of [Yaml3PathNode] interface +// e.g. for path "$.items[0]" it would be representation of ".items" +type yaml3PathGetByKey struct { + next Yaml3PathNode + prev Yaml3PathNode + key string +} + +// GetScalarValueFrom implements [Yaml3PathNode.GetScalarValueFrom] interface +func (p *yaml3PathGetByKey) GetScalarValueFrom(node *yaml.Node) (string, error) { + nestedNode, err := p.getRelevantNode(node) + if err != nil { + return "", err + } + + if p.next != nil { + return p.next.GetScalarValueFrom(nestedNode) + } + + if nestedNode.Kind != yaml.ScalarNode { + return "", fmt.Errorf("%s scalar node is expected, but found %s", getPathToNode(p), kindToString(node)) + } + + return nestedNode.Value, nil +} + +// SetScalarValueTo implements [Yaml3PathNode.SetScalarValueTo] interface +func (p *yaml3PathGetByKey) SetScalarValueTo(node *yaml.Node, value string, style yaml.Style) error { + nestedNode, err := p.getRelevantNode(node) + if err != nil { + return err + } + + if p.next != nil { + return p.next.SetScalarValueTo(nestedNode, value, style) + } + + if nestedNode.Kind != yaml.ScalarNode { + return fmt.Errorf("%s scalar node is expected, but found %s", getPathToNode(p), kindToString(node)) + } + + nestedNode.Value = value + nestedNode.Style = style + + return nil +} + +// SetNext implements [Yaml3PathNode.SetNext] interface +func (p *yaml3PathGetByKey) SetNext(next Yaml3PathNode) { + p.next = next + if next.GetPrev() != p { + next.SetPrev(p) + } +} + +// SetPrev implements [Yaml3PathNode.SetPrev] interface +func (p *yaml3PathGetByKey) SetPrev(prev Yaml3PathNode) { + p.prev = prev + if prev.GetNext() != p { + prev.SetNext(p) + } +} + +// GetPrev implements [Yaml3PathNode.GetPrev] interface +func (p *yaml3PathGetByKey) GetPrev() Yaml3PathNode { + return p.prev +} + +// GetNext implements [Yaml3PathNode.GetNext] interface +func (p *yaml3PathGetByKey) GetNext() Yaml3PathNode { + return p.next +} + +func (p *yaml3PathGetByKey) getRelevantNode(node *yaml.Node) (*yaml.Node, error) { + if node.Kind != yaml.MappingNode { + return nil, fmt.Errorf("%s mapping node expected, but found %s", getPathToNode(p.GetPrev()), kindToString(node)) + } + + nodeByKeyIndex := -1 + for index, childNode := range node.Content { + if childNode.Value == p.key { + nodeByKeyIndex = index + break + } + } + + if nodeByKeyIndex == -1 { + return nil, fmt.Errorf("%s there is no value by key", getPathToNode(p)) + } + + if nodeByKeyIndex+1 >= len(node.Content) { + // in yaml3 mapping or scalar value node is next to node found by key + return nil, fmt.Errorf("%s there is no value by key", getPathToNode(p)) + } + nestedNode := node.Content[nodeByKeyIndex+1] + + return nestedNode, nil +} + +////////////////////////////////////////////////////////////////////////////////////// +// Public functions +////////////////////////////////////////////////////////////////////////////////////// + +// ParseYmlPath parses yaml path string and returns path node +func ParseYmlPath(path string) (Yaml3PathNode, error) { + artifacts := strings.Split(path, ".") + if len(artifacts) == 0 { + return nil, errors.New("empty path") + } + + if artifacts[0] != "$" { + return nil, errors.New("path not start from root") + } + + var root Yaml3PathNode = &yaml3PathGetFromRoot{} + + node := root + artifacts = artifacts[1:] + for i, artifact := range artifacts { + artifact = strings.TrimSpace(artifact) + keyWithIndex := strings.Split(artifact, "[") + + if len(keyWithIndex) > 2 { + fullKey := strings.Join(artifacts[:i+1], ".") + return nil, fmt.Errorf("%s invalid path, too many \"[\" in key", fullKey) + } + + key := keyWithIndex[0] + byKeyGetPath := &yaml3PathGetByKey{ + key: key, + } + node.SetNext(byKeyGetPath) + node = byKeyGetPath + + if len(keyWithIndex) == 2 { + indexArtifact := keyWithIndex[1] + if indexArtifact[len(indexArtifact)-1] != ']' { + fullKey := strings.Join(artifacts[:i+1], ".") + return nil, fmt.Errorf("%s invalid path, sequence index has no \"]\"", fullKey) + } + + indexStr := indexArtifact[:len(indexArtifact)-1] + seqIndex, err := strconv.Atoi(indexStr) + if err != nil { + fullKey := strings.Join(artifacts[:i+1], ".") + return nil, fmt.Errorf("%s invalid sequence index", fullKey) + } + byIndexGetPath := &yaml3PathGetByIndex{ + index: seqIndex, + } + node.SetNext(byIndexGetPath) + node = byIndexGetPath + } + } + + return root, nil +} + +////////////////////////////////////////////////////////////////////////////////////// +// Private functions +////////////////////////////////////////////////////////////////////////////////////// + +// getPathRoot returns root of provided path +func getPathRoot(path Yaml3PathNode) Yaml3PathNode { + var root = path + for root.GetPrev() != nil { + root = root.GetPrev() + } + + return root +} + +// getPathToNode returns string representation of path to provided path node +// e.g. +// if you have parsed "$.issues.exclude-rules[0].text[0]" path, and you provide "text" path node +// this function will return "$.issues.exclude-rules[0].text" +func getPathToNode(path Yaml3PathNode) string { + var root = getPathRoot(path) + + current := root + builder := strings.Builder{} + for current != nil { + switch v := current.(type) { + case *yaml3PathGetFromRoot: + builder.WriteString("$") + case *yaml3PathGetByKey: + builder.WriteString(".") + builder.WriteString(v.key) + case *yaml3PathGetByIndex: + builder.WriteString("[") + builder.WriteString(strconv.Itoa(v.index)) + builder.WriteString("]") + default: + panic(fmt.Sprintf("unknown path node type: %v", current)) + } + + if current == path { + break + } + + current = current.GetNext() + } + + return builder.String() +} + +var kindMap = map[yaml.Kind]string{ + yaml.DocumentNode: "document node", + yaml.SequenceNode: "sequence node", + yaml.MappingNode: "mapping node", + yaml.ScalarNode: "scalar node", + yaml.AliasNode: "alias node", +} + +func kindToString(node *yaml.Node) string { + // kind is in map + if kindName, ok := kindMap[node.Kind]; ok { + return kindName + } + + panic(fmt.Sprintf("unknown kind: %v", node.Kind)) +} diff --git a/services/util/yml-utils/yaml3.utils_test.go b/services/util/yml-utils/yaml3.utils_test.go new file mode 100644 index 00000000..36258837 --- /dev/null +++ b/services/util/yml-utils/yaml3.utils_test.go @@ -0,0 +1,301 @@ +/* +Copyright (c) 2024 Snowflake Inc. All rights reserved. + + 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 yml_utils + +import ( + "errors" + "fmt" + "gopkg.in/yaml.v3" + "strings" + "testing" +) + +func getLinesDiff(expected string, actual string) string { + expectedLines := strings.Split(expected, "\n") + actualLines := strings.Split(actual, "\n") + var diffLines []string + for i := 0; i < min(len(expectedLines), len(actualLines)); i++ { + line := expectedLines[i] + actualLine := actualLines[i] + + if line == actualLine { + diffLines = append(diffLines, line) + } else { + diffLines = append(diffLines, fmt.Sprintf("-%s", line)) + diffLines = append(diffLines, fmt.Sprintf("+%s", actualLine)) + } + } + + if len(expectedLines) > len(actualLines) { + diffLines = append(diffLines, expectedLines[len(actualLines):]...) + } + + if len(actualLines) > len(expectedLines) { + for _, line := range actualLines[len(expectedLines):] { + diffLines = append(diffLines, fmt.Sprintf("+%s", line)) + } + } + + return strings.Join(diffLines, "\n") +} + +func Test_Yaml3_GetScalarValueFrom(t *testing.T) { + sourceYaml := ` +root: + # top comment + simple_key: simple_key_value # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + nested_sequence: # simple sequence comment + - nested_sequence_value_1: + nested_sequence_key_1: nested_sequence_value_1_key_1 + nested_sequence_key_2: nested_sequence_value_1_key_2 + - nested_sequence_value_2: + nested_sequence_key_1: nested_sequence_value_2_key_1 + nested_sequence_key_2: nested_sequence_value_2_key_2 + - nested_sequence_value_3: + nested_sequence_key_1: nested_sequence_value_3_key_1 + nested_sequence_key_2: nested_sequence_value_3_key_2 + # bottom comment +` + + tests := []struct { + name string + yamlPath string + expectedResult string + expectedErr error + }{ + { + name: "It should get value from sequence by key", + yamlPath: "$.root.simple_key", + expectedResult: "simple_key_value", + expectedErr: nil, + }, + { + name: "It should get value from sequence by index", + yamlPath: "$.root.simple_sequence[2]", + expectedResult: "simple_sequence_value_3", + expectedErr: nil, + }, + { + name: "It should get value by complex path with mixed nesting", + yamlPath: "$.root.nested_sequence[1].nested_sequence_value_2.nested_sequence_key_2", + expectedResult: "nested_sequence_value_2_key_2", + expectedErr: nil, + }, + { + name: "It should return error on get if simple key not exists", + yamlPath: "$.root.not_existed_simple_key", + expectedErr: errors.New("$.root.not_existed_simple_key there is no value by key"), + }, + { + name: "It should return error of index out of range", + yamlPath: "$.root.simple_sequence[3]", + expectedErr: errors.New("$.root.simple_sequence[3] index out of sequence range"), + }, + { + name: "It should return error on get map key from sequence", + yamlPath: "$.root.simple_sequence.some_key", + expectedErr: errors.New("$.root.simple_sequence mapping node expected, but found sequence node"), + }, + { + name: "It should return error on get sequence node", + yamlPath: "$.root.simple_sequence", + expectedErr: errors.New("$.root.simple_sequence scalar node is expected, but found mapping node"), + }, + { + name: "It should return error on get mapping node", + yamlPath: "$.root", + expectedErr: errors.New("$.root scalar node is expected, but found mapping node"), + }, + { + name: "It should return error on root", + yamlPath: "$", + expectedErr: errors.New("$ could not get scalar of root"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // ARRANGE + var data yaml.Node + err := yaml.Unmarshal([]byte(sourceYaml), &data) + if err != nil { + t.Errorf("Unexpected unmarshal error: %s", err.Error()) + return + } + yamlPath, err := ParseYmlPath(test.yamlPath) + if err != nil { + t.Errorf("Unexpected parse error: %s", err.Error()) + return + } + + // ACT + result, err := yamlPath.GetScalarValueFrom(&data) + + // ASSERT + if test.expectedErr != nil && err == nil { + t.Errorf("Expected error \"%s\", but got nil", test.expectedErr) + return + } + + if test.expectedErr != nil && err != nil && test.expectedErr.Error() != err.Error() { + t.Errorf("Expected \"%s\", but got \"%s\"", test.expectedErr, err) + return + } + + if result != test.expectedResult { + t.Errorf("Expected \"%s\", but got \"%s\"", test.expectedResult, result) + return + } + }) + } +} + +func Test_Yaml3_SetScalarValueTo(t *testing.T) { + sourceYaml := `root: + # top comment + simple_key: simple_key_value # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +` + + tests := []struct { + name string + yamlPath string + newValue string + valueStyle yaml.Style + expectedResult string + expectedErr error + }{ + { + name: "It should set value by key and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "new_simple_val", + valueStyle: yaml.FlowStyle, + expectedResult: `root: + # top comment + simple_key: new_simple_val # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should set value by key and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "new_simple_val", + valueStyle: yaml.FlowStyle, + expectedResult: `root: + # top comment + simple_key: new_simple_val # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should set value as double quoted string and keep comments as it is", + yamlPath: "$.root.simple_key", + newValue: "new_simple_val", + valueStyle: yaml.DoubleQuotedStyle, + expectedResult: `root: + # top comment + simple_key: "new_simple_val" # simple key comment + simple_sequence: + # simple sequence comment + - simple_sequence_value_1 + - simple_sequence_value_2 + - simple_sequence_value_3 + # bottom comment +`, + expectedErr: nil, + }, + { + name: "It should fails set root value", + yamlPath: "$", + newValue: "new_simple_val", + valueStyle: yaml.FlowStyle, + expectedErr: errors.New("$ could not set scalar of root"), + }, + { + name: "It should fails set sequence value", + yamlPath: "$.root.simple_sequence", + newValue: "new_simple_val", + valueStyle: yaml.FlowStyle, + expectedErr: errors.New("$.root.simple_sequence scalar node is expected, but found mapping node"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // ARRANGE + var data yaml.Node + err := yaml.Unmarshal([]byte(sourceYaml), &data) + if err != nil { + t.Errorf("Unexpected unmarshal error: %s", err.Error()) + return + } + yamlPath, err := ParseYmlPath(test.yamlPath) + if err != nil { + t.Errorf("Unexpected parse error: %s", err.Error()) + return + } + + // ACT + err = yamlPath.SetScalarValueTo(&data, test.newValue, test.valueStyle) + + // ASSERT + if test.expectedErr != nil && err == nil { + t.Errorf("Expected error \"%s\", but got nil", test.expectedErr) + return + } + + if test.expectedErr != nil && err != nil && test.expectedErr.Error() != err.Error() { + t.Errorf("Expected \"%s\", but got \"%s\"", test.expectedErr, err) + return + } + + updatedYmlBytes, err := yaml.Marshal(data.Content[0]) + if err != nil { + t.Errorf("Unexpected marshal error: %s", err.Error()) + return + } + + if test.expectedResult != "" && string(updatedYmlBytes) != test.expectedResult { + diff := getLinesDiff(test.expectedResult, string(updatedYmlBytes)) + t.Errorf("Expected not equals updated, diff %s", diff) + return + } + }) + } +} diff --git a/testing/integrate.sh b/testing/integrate.sh index 8a02a9a0..8e7678e0 100755 --- a/testing/integrate.sh +++ b/testing/integrate.sh @@ -100,7 +100,7 @@ function print_logs { shift PREFACE=$* - printf "\n%s:\n\n" "${PREFACE}" + printf "\nLogs for %s:\n\n" "${PREFACE}" cat "${LOG}" } @@ -122,7 +122,7 @@ function run_a_test { SUBCMD=$1 shift - echo "${CMD} ${SUBCMD} checks" + echo "\nTEST: ${CMD} ${SUBCMD}" CHECK="${CMD} ${SUBCMD} proxy to 2 hosts" echo "${CHECK}" @@ -171,7 +171,7 @@ function run_a_test { check_logs "${LINE_MIN}" "${CMD}-${SUBCMD}" "${CHECK}" copy_logs "${CMD}-${SUBCMD}" no-proxy - echo "${CMD} ${SUBCMD} passed" + echo "${CMD} ${SUBCMD} PASSED\n" } # Takes 1 arg: