Skip to content

Commit

Permalink
Add command to get and set data in files of specific formats (#492)
Browse files Browse the repository at this point in the history
* SNOW-1665681 Add set/get operators
  • Loading branch information
sfc-gh-ikryvanos authored Oct 10, 2024
1 parent 39af558 commit 6817e07
Show file tree
Hide file tree
Showing 37 changed files with 4,496 additions and 412 deletions.
4 changes: 3 additions & 1 deletion docs/services-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
- `./<service-name>.go` contains the service related commands
- `./<service-name>.proto` protobuf definition of client-server communication
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
Expand Down Expand Up @@ -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
)
10 changes: 6 additions & 4 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions services/localfile/README.md
Original file line number Diff line number Diff line change
@@ -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 <sanssh-args> file get-data [--format <file-format>] <file-path> <data-key>
```
Where:
- `<sanssh-args>` common sanssh arguments
- `<file-path>` is the path to the file on remote machine. If --format is not provided, format would be detected from file extension.
- `<data-key>` 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
- `<file-format>` 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 <sanssh-args> file set-data [--format <file-format>] [--value-type <value-type>] <file-path> <data-key> <value>
```
Where:
- `<sanssh-args>` common sanssh arguments
- `<file-path>` is the path to the file on remote machine. If --format is not provided, format would be detected from file extension.
- `<data-key>` 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
- `<value>` is the value to set in the file
- `<file-format>` is the format of the file, if specified it would override the format detected from file extension. Supported formats are:
- `yml`
- `dotenv`
- `<value-type>` 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
```
37 changes: 37 additions & 0 deletions services/localfile/client/cli-controllers/file.utils.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
151 changes: 151 additions & 0 deletions services/localfile/client/cli-controllers/get-data.controller.go
Original file line number Diff line number Diff line change
@@ -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] <file-path> <data-key>:
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),
}),
}
}
Loading

0 comments on commit 6817e07

Please sign in to comment.