Split the collect Command For Customization (#93)

* feat: initial implementation of command split

* feat: update collect and new send cmd

* chore: cleanup unused code

* chore: refactored getting username

* chore: more refactoring and cleanup

* feat: update send cmd implementation

* chore: changed/updated example config

* chore: made cmd more consistent and added formatting

* refactor: removed --host flag from scan

* chore: cleaned up and fixed issue with client

* chore: cleaned up CLI flags in collect cmd

* feat: updated crawl to include managers and output YAML optionally

* refactor: updated and improved send implementation

* refactor: minor improvements

* refactor: added util func to check for empty slices

* fix: issue with reading from stdin

* refactor: added scheme trimming function for URIs

* refactor: changed host arg back to positional

* refactor: removed unused vars and added --output-dir flag

* fix: make -f for secrets persistent

* refactor: removed --host flag and request in collect

* refactor: changed --output flag to --output-file

* fix: updated flags for collect

* fix: typo in crawler error

* fix: dir being created when outputDir not set

* fix: reading stdin and data args

* fix: made output using -v and -o consistent

* readme: added info about command split

* updated changelog adding missing version entries

* chore: updated example to use host as positional arg

* fix: issue with reading --data arg

* fix: remove unused import from collect pkg

Signed-off-by: Devon Bautista <devonb@lanl.gov>

---------

Signed-off-by: David Allen <16520934+davidallendj@users.noreply.github.com>
Signed-off-by: Devon Bautista <devonb@lanl.gov>
Co-authored-by: Devon Bautista <devonb@lanl.gov>
This commit is contained in:
David Allen 2025-05-29 13:15:46 -06:00 committed by GitHub
parent fba4a89a0e
commit 04e1fb26c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 736 additions and 223 deletions

View file

@ -2,8 +2,6 @@ package cmd
import (
"encoding/json"
"fmt"
"os/user"
"github.com/OpenCHAMI/magellan/internal/cache/sqlite"
urlx "github.com/OpenCHAMI/magellan/internal/url"
@ -17,6 +15,8 @@ import (
"github.com/spf13/viper"
)
var collectOutputFormat string
// The `collect` command fetches data from a collection of BMC nodes.
// This command should be ran after the `scan` to find available hosts
// on a subnet.
@ -122,6 +122,8 @@ var CollectCmd = &cobra.Command{
Verbose: verbose,
CaCertPath: cacertPath,
OutputPath: outputPath,
OutputDir: outputDir,
Format: collectOutputFormat,
ForceUpdate: forceUpdate,
AccessToken: accessToken,
SecretStore: store,
@ -140,22 +142,24 @@ var CollectCmd = &cobra.Command{
}
func init() {
currentUser, _ = user.Current()
CollectCmd.Flags().StringVar(&host, "host", "", "Set the URI to the SMD root endpoint")
CollectCmd.Flags().StringVarP(&username, "username", "u", "", "Set the master BMC username")
CollectCmd.Flags().StringVarP(&password, "password", "p", "", "Set the master BMC password")
CollectCmd.Flags().StringVar(&secretsFile, "secrets-file", "", "Set path to the node secrets file")
CollectCmd.Flags().StringVar(&scheme, "scheme", "https", "Set the default scheme used to query when not included in URI")
CollectCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the protocol used to query")
CollectCmd.Flags().StringVarP(&outputPath, "output", "o", fmt.Sprintf("/tmp/%smagellan/inventory/", currentUser.Username+"/"), "Set the path to store collection data")
CollectCmd.Flags().StringVarP(&outputPath, "output-file", "o", "", "Set the path to store collection data using HIVE partitioning")
CollectCmd.Flags().StringVarP(&outputDir, "output-dir", "O", "", "Set the path to store collection data using HIVE partitioning")
CollectCmd.Flags().BoolVar(&forceUpdate, "force-update", false, "Set flag to force update data sent to SMD")
CollectCmd.Flags().StringVar(&cacertPath, "cacert", "", "Set the path to CA cert file. (defaults to system CAs when blank)")
CollectCmd.Flags().StringVar(&cacertPath, "cacert", "", "Set the path to CA cert file (defaults to system CAs when blank)")
CollectCmd.Flags().StringVarP(&collectOutputFormat, "format", "F", FORMAT_JSON, "Set the output format (json|yaml)")
CollectCmd.MarkFlagsMutuallyExclusive("output-file", "output-dir")
// bind flags to config properties
checkBindFlagError(viper.BindPFlag("collect.host", CollectCmd.Flags().Lookup("host")))
checkBindFlagError(viper.BindPFlag("collect.scheme", CollectCmd.Flags().Lookup("scheme")))
checkBindFlagError(viper.BindPFlag("collect.protocol", CollectCmd.Flags().Lookup("protocol")))
checkBindFlagError(viper.BindPFlag("collect.output", CollectCmd.Flags().Lookup("output")))
checkBindFlagError(viper.BindPFlag("collect.output-file", CollectCmd.Flags().Lookup("output-file")))
checkBindFlagError(viper.BindPFlag("collect.output-dir", CollectCmd.Flags().Lookup("output-dir")))
checkBindFlagError(viper.BindPFlag("collect.force-update", CollectCmd.Flags().Lookup("force-update")))
checkBindFlagError(viper.BindPFlag("collect.cacert", CollectCmd.Flags().Lookup("cacert")))
checkBindFlagError(viper.BindPFlags(CollectCmd.Flags()))

View file

@ -3,8 +3,10 @@ package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
urlx "github.com/OpenCHAMI/magellan/internal/url"
"github.com/OpenCHAMI/magellan/pkg/bmc"
@ -14,6 +16,8 @@ import (
"github.com/spf13/viper"
)
var crawlOutputFormat string
// The `crawl` command walks a collection of Redfish endpoints to collect
// specfic inventory detail. This command only expects host names and does
// not require a scan to be performed beforehand.
@ -37,9 +41,10 @@ var CrawlCmd = &cobra.Command{
},
Run: func(cmd *cobra.Command, args []string) {
var (
uri = args[0]
store secrets.SecretStore
err error
uri = args[0]
store secrets.SecretStore
output []byte
err error
)
if username != "" && password != "" {
@ -76,24 +81,53 @@ var CrawlCmd = &cobra.Command{
store = &nodeCreds
}
systems, err := crawler.CrawlBMCForSystems(crawler.CrawlerConfig{
URI: uri,
CredentialStore: store,
Insecure: insecure,
UseDefault: true,
})
var (
systems []crawler.InventoryDetail
managers []crawler.Manager
config = crawler.CrawlerConfig{
URI: uri,
CredentialStore: store,
Insecure: insecure,
UseDefault: true,
}
)
systems, err = crawler.CrawlBMCForSystems(config)
if err != nil {
log.Error().Err(err).Msg("failed to crawl BMC")
log.Error().Err(err).Msg("failed to crawl BMC for systems")
}
// Marshal the inventory details to JSON
jsonData, err := json.MarshalIndent(systems, "", " ")
managers, err = crawler.CrawlBMCForManagers(config)
if err != nil {
log.Error().Err(err).Msg("failed to marshal JSON")
return
log.Error().Err(err).Msg("failed to crawl BMC for managers")
}
// Print the pretty JSON
fmt.Println(string(jsonData))
data := map[string]any{
"Systems": systems,
"Managers": managers,
}
switch crawlOutputFormat {
case FORMAT_JSON:
// Marshal the inventory details to JSON
output, err = json.MarshalIndent(data, "", " ")
if err != nil {
log.Error().Err(err).Msg("failed to marshal JSON")
return
}
case FORMAT_YAML:
// Marshal the inventory details to JSON
output, err = yaml.Marshal(data)
if err != nil {
log.Error().Err(err).Msg("failed to marshal JSON")
return
}
default:
log.Error().Str("hint", "Try setting --format/-F to 'json' or 'yaml'").Msg("unrecognized format")
os.Exit(1)
}
// Print the pretty JSON or YAML
fmt.Println(string(output))
},
}
@ -101,8 +135,11 @@ func init() {
CrawlCmd.Flags().StringVarP(&username, "username", "u", "", "Set the username for the BMC")
CrawlCmd.Flags().StringVarP(&password, "password", "p", "", "Set the password for the BMC")
CrawlCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Ignore SSL errors")
CrawlCmd.Flags().StringVarP(&secretsFile, "file", "f", "nodes.json", "set the secrets file with BMC credentials")
CrawlCmd.Flags().StringVarP(&secretsFile, "secrets-file", "f", "secrets.json", "Set path to the node secrets file")
CrawlCmd.Flags().StringVarP(&crawlOutputFormat, "format", "F", FORMAT_JSON, "Set the output format (json|yaml)")
checkBindFlagError(viper.BindPFlag("crawl.insecure", CrawlCmd.Flags().Lookup("insecure")))
checkBindFlagError(viper.BindPFlag("crawl.insecure", CrawlCmd.Flags().Lookup("insecure")))
checkBindFlagError(viper.BindPFlag("crawl.insecure", CrawlCmd.Flags().Lookup("insecure")))
rootCmd.AddCommand(CrawlCmd)

View file

@ -3,17 +3,20 @@ package cmd
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/OpenCHAMI/magellan/internal/cache/sqlite"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
"github.com/spf13/cobra"
)
var (
showCache bool
showCache bool
listOutputFormat string
)
// The `list` command provides an easy way to show what was found
@ -41,23 +44,32 @@ var ListCmd = &cobra.Command{
if err != nil {
log.Error().Err(err).Msg("failed to get scanned assets")
}
format = strings.ToLower(format)
if format == "json" {
switch strings.ToLower(listOutputFormat) {
case FORMAT_JSON:
b, err := json.Marshal(scannedResults)
if err != nil {
log.Error().Err(err).Msgf("failed to unmarshal scanned results")
log.Error().Err(err).Msgf("failed to unmarshal cached data to JSON")
}
fmt.Printf("%s\n", string(b))
} else {
case FORMAT_YAML:
b, err := yaml.Marshal(scannedResults)
if err != nil {
log.Error().Err(err).Msgf("failed to unmarshal cached data to YAML")
}
fmt.Printf("%s\n", string(b))
case FORMAT_LIST:
for _, r := range scannedResults {
fmt.Printf("%s:%d (%s) @%s\n", r.Host, r.Port, r.Protocol, r.Timestamp.Format(time.UnixDate))
}
default:
log.Error().Msg("unrecognized format")
os.Exit(1)
}
},
}
func init() {
ListCmd.Flags().StringVar(&format, "format", "", "Set the output format (json|default)")
ListCmd.Flags().StringVarP(&listOutputFormat, "format", "F", FORMAT_LIST, "Set the output format (json|yaml|table)")
ListCmd.Flags().BoolVar(&showCache, "cache-info", false, "Show cache information and exit")
rootCmd.AddCommand(ListCmd)
}

View file

@ -18,28 +18,33 @@ import (
"fmt"
"net"
"os"
"os/user"
magellan "github.com/OpenCHAMI/magellan/internal"
"github.com/OpenCHAMI/magellan/internal/util"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const (
FORMAT_LIST = "list"
FORMAT_JSON = "json"
FORMAT_YAML = "yaml"
)
// CLI arguments as variables to not fiddle with error-prone strings
var (
currentUser *user.User
accessToken string
format string
timeout int
concurrency int
ports []int
hosts []string
protocol string
cacertPath string
username string
password string
cachePath string
outputPath string
outputDir string
configPath string
verbose bool
debug bool
@ -73,15 +78,14 @@ func Execute() {
}
func init() {
currentUser, _ = user.Current()
cobra.OnInitialize(InitializeConfig)
rootCmd.PersistentFlags().IntVarP(&concurrency, "concurrency", "j", -1, "Set the number of concurrent processes")
rootCmd.PersistentFlags().IntVarP(&timeout, "timeout", "t", 5, "Set the timeout for requests")
rootCmd.PersistentFlags().IntVarP(&timeout, "timeout", "t", 5, "Set the timeout for requests in seconds")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Set the config file path")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Set to enable/disable verbose output")
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Set to enable/disable debug messages")
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Set to enable/disable debug messages")
rootCmd.PersistentFlags().StringVar(&accessToken, "access-token", "", "Set the access token")
rootCmd.PersistentFlags().StringVar(&cachePath, "cache", fmt.Sprintf("/tmp/%s/magellan/assets.db", currentUser.Username), "Set the scanning result cache path")
rootCmd.PersistentFlags().StringVar(&cachePath, "cache", fmt.Sprintf("/tmp/%s/magellan/assets.db", util.GetCurrentUsername()), "Set the scanning result cache path")
// bind viper config flags with cobra
checkBindFlagError(viper.BindPFlag("concurrency", rootCmd.PersistentFlags().Lookup("concurrency")))
@ -117,13 +121,12 @@ func InitializeConfig() {
// TODO: This function should probably be moved to 'internal/config.go'
// instead of in this file.
func SetDefaults() {
currentUser, _ = user.Current()
viper.SetDefault("threads", 1)
viper.SetDefault("timeout", 5)
viper.SetDefault("config", "")
viper.SetDefault("verbose", false)
viper.SetDefault("debug", false)
viper.SetDefault("cache", fmt.Sprintf("/tmp/%s/magellan/assets.db", currentUser.Username))
viper.SetDefault("cache", fmt.Sprintf("/tmp/%s/magellan/assets.db", util.GetCurrentUsername()))
viper.SetDefault("scan.hosts", []string{})
viper.SetDefault("scan.ports", []int{})
viper.SetDefault("scan.subnets", []string{})

View file

@ -79,18 +79,8 @@ var ScanCmd = &cobra.Command{
// format and combine flag and positional args
targetHosts = append(targetHosts, urlx.FormatHosts(args, ports, scheme, verbose)...)
targetHosts = append(targetHosts, urlx.FormatHosts(hosts, ports, scheme, verbose)...)
// add more hosts specified with `--subnet` flag
if debug {
log.Debug().Msg("adding hosts from subnets")
}
for _, subnet := range subnets {
// subnet string is empty so nothing to do here
if subnet == "" {
continue
}
// generate a slice of all hosts to scan from subnets
subnetHosts := magellan.GenerateHostsWithSubnet(subnet, &subnetMask, ports, scheme)
targetHosts = append(targetHosts, subnetHosts...)
@ -180,8 +170,6 @@ var ScanCmd = &cobra.Command{
}
func init() {
// scanCmd.Flags().StringSliceVar(&hosts, "host", []string{}, "set additional hosts to scan")
ScanCmd.Flags().StringSliceVar(&hosts, "host", nil, "Add individual hosts to scan. (example: https://my.bmc.com:5000; same as using positional args)")
ScanCmd.Flags().IntSliceVar(&ports, "port", nil, "Adds additional ports to scan for each host with unspecified ports.")
ScanCmd.Flags().StringVar(&scheme, "scheme", "https", "Set the default scheme to use if not specified in host URI. (default is 'https')")
ScanCmd.Flags().StringVar(&protocol, "protocol", "tcp", "Set the default protocol to use in scan. (default is 'tcp')")
@ -190,7 +178,6 @@ func init() {
ScanCmd.Flags().BoolVar(&disableProbing, "disable-probing", false, "Disable probing found assets for Redfish service(s) running on BMC nodes")
ScanCmd.Flags().BoolVar(&disableCache, "disable-cache", false, "Disable saving found assets to a cache database specified with 'cache' flag")
checkBindFlagError(viper.BindPFlag("scan.hosts", ScanCmd.Flags().Lookup("host")))
checkBindFlagError(viper.BindPFlag("scan.ports", ScanCmd.Flags().Lookup("port")))
checkBindFlagError(viper.BindPFlag("scan.scheme", ScanCmd.Flags().Lookup("scheme")))
checkBindFlagError(viper.BindPFlag("scan.protocol", ScanCmd.Flags().Lookup("protocol")))

View file

@ -256,9 +256,9 @@ var secretsRemoveCmd = &cobra.Command{
}
func init() {
secretsCmd.Flags().StringVarP(&secretsFile, "file", "f", "nodes.json", "set the secrets file with BMC credentials")
secretsStoreCmd.Flags().StringVarP(&secretsStoreFormat, "format", "F", "basic", "set the input format for the secrets file (basic|json|base64)")
secretsStoreCmd.Flags().StringVarP(&secretsStoreInputFile, "input-file", "i", "", "set the file to read as input")
secretsCmd.PersistentFlags().StringVarP(&secretsFile, "file", "f", "secrets.json", "Set the secrets file with BMC credentials.")
secretsStoreCmd.Flags().StringVarP(&secretsStoreFormat, "format", "F", "basic", "Set the input format for the secrets file (basic|json|base64).")
secretsStoreCmd.Flags().StringVarP(&secretsStoreInputFile, "input-file", "i", "", "Set the file to read as input.")
secretsCmd.AddCommand(secretsGenerateKeyCmd)
secretsCmd.AddCommand(secretsStoreCmd)
@ -268,7 +268,7 @@ func init() {
rootCmd.AddCommand(secretsCmd)
checkBindFlagError(viper.BindPFlags(secretsCmd.Flags()))
checkBindFlagError(viper.BindPFlags(secretsCmd.PersistentFlags()))
checkBindFlagError(viper.BindPFlags(secretsGenerateKeyCmd.Flags()))
checkBindFlagError(viper.BindPFlags(secretsStoreCmd.Flags()))
checkBindFlagError(viper.BindPFlags(secretsGenerateKeyCmd.Flags()))

283
cmd/send.go Normal file
View file

@ -0,0 +1,283 @@
package cmd
import (
"bufio"
"encoding/json"
"fmt"
"os"
"strings"
urlx "github.com/OpenCHAMI/magellan/internal/url"
"github.com/OpenCHAMI/magellan/pkg/auth"
"github.com/OpenCHAMI/magellan/pkg/client"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
var (
sendInputFormat string
sendDataArgs []string
)
var sendCmd = &cobra.Command{
Use: "send [data]",
Example: ` // minimal working example
magellan send -d @inventory.json https://smd.openchami.cluster
// send data from multiple files (must specify -f/--format if not JSON)
magellan send -d @cluster-1.json -d @cluster-2.json https://smd.openchami.cluster
magellan send -d '{...}' -d @cluster-1.json https://proxy.example.com
// send data to remote host by piping output of collect directly
magellan collect -v -F yaml | magellan send -d @inventory.yaml -F yaml https://smd.openchami.cluster`,
Short: "Send collected node information to specified host.",
Args: func(cmd *cobra.Command, args []string) error {
return nil
},
Run: func(cmd *cobra.Command, args []string) {
// try to load access token either from env var, file, or config if var not set
if accessToken == "" {
var err error
accessToken, err = auth.LoadAccessToken(tokenPath)
if err != nil && verbose {
log.Warn().Err(err).Msgf("could not load access token")
} else if debug && accessToken != "" {
log.Debug().Str("access_token", accessToken).Msg("using access token")
}
}
// try and load cert if argument is passed for client
var smdClient = client.NewSmdClient()
if cacertPath != "" {
log.Debug().Str("path", cacertPath).Msg("using provided certificate path")
err := client.LoadCertificateFromPath(smdClient, cacertPath)
if err != nil {
log.Warn().Err(err).Msg("could not load certificate")
}
}
// make one request be host positional argument (restricted to 1 for now)
var inputData []map[string]any
temp := append(handleArgs(args), processDataArgs(sendDataArgs)...)
for _, data := range temp {
if data != nil {
inputData = append(inputData, data)
}
}
if len(inputData) == 0 {
log.Error().Msg("must include data with standard input or -d/--data flag")
os.Exit(1)
}
// show the data that was just loaded as input
if verbose {
output, err := json.Marshal(inputData)
if err != nil {
log.Error().Err(err).Msg("failed to marshal input data")
}
fmt.Println(string(output))
}
for _, host := range args {
var (
body []byte
err error
)
smdClient.URI = host
for _, dataObject := range inputData {
// skip on to the next thing if it's does not exist
if dataObject == nil {
continue
}
// create and set headers for request
headers := client.HTTPHeader{}
headers.Authorization(accessToken)
headers.ContentType("application/json")
host, err = urlx.Sanitize(host)
if err != nil {
log.Warn().Err(err).Str("host", host).Msg("could not sanitize host")
}
// convert to JSON to send data
body, err = json.MarshalIndent(dataObject, "", " ")
if err != nil {
log.Error().Err(err).Msg("failed to marshal request data")
continue
}
err = smdClient.Add(body, headers)
if err != nil {
// try updating instead
if forceUpdate {
smdClient.Xname = dataObject["ID"].(string)
err = smdClient.Update(body, headers)
if err != nil {
log.Error().Err(err).Msgf("failed to forcibly update Redfish endpoint with ID %s", smdClient.Xname)
}
} else {
log.Error().Err(err).Msgf("failed to add Redfish endpoint with ID %s", smdClient.Xname)
}
}
}
}
},
}
func init() {
sendCmd.Flags().StringArrayVarP(&sendDataArgs, "data", "d", []string{}, "Set the data to send to specified host (prepend @ for files)")
sendCmd.Flags().StringVarP(&sendInputFormat, "format", "F", FORMAT_JSON, "Set the data input format (json|yaml)")
sendCmd.Flags().BoolVarP(&forceUpdate, "force-update", "f", false, "Set flag to force update data sent to SMD")
sendCmd.Flags().StringVar(&cacertPath, "cacert", "", "Set the path to CA cert file (defaults to system CAs when blank)")
rootCmd.AddCommand(sendCmd)
}
// processDataArgs takes a slice of strings that check for the @ symbol and loads
// the contents from the file specified in place (which replaces the path).
//
// NOTE: The purpose is to make the input arguments uniform for our request. This
// function is meant to handle data passed with the `-d/--data` flag and positional
// args from the CLI.
func processDataArgs(args []string) []map[string]any {
// JSON representation
type (
JSONObject = map[string]any
JSONArray = []JSONObject
)
// load data either from file or directly from args
var collection = make(JSONArray, len(args))
for i, arg := range args {
// if arg is empty string, then skip and continue
if len(arg) > 0 {
// determine if we're reading from file to load contents
if strings.HasPrefix(arg, "@") {
var (
path string = strings.TrimLeft(arg, "@")
contents []byte
data JSONArray
err error
)
contents, err = os.ReadFile(path)
if err != nil {
log.Error().Err(err).Str("path", path).Msg("failed to read file")
continue
}
// skip empty files
if len(contents) == 0 {
log.Warn().Str("path", path).Msg("file is empty")
continue
}
// convert/validate JSON input format
data, err = parseInput(contents)
if err != nil {
log.Error().Err(err).Str("path", path).Msg("failed to validate input from file")
}
// add loaded data to collection of all data
collection = append(collection, data...)
} else {
// input should be a valid JSON
var (
data JSONArray
input = []byte(arg)
err error
)
if !json.Valid(input) {
log.Error().Msgf("argument %d not a valid JSON", i)
continue
}
err = json.Unmarshal(input, &data)
if err != nil {
log.Error().Err(err).Msgf("failed to unmarshal input for argument %d", i)
}
return data
}
}
}
return collection
}
func handleArgs(args []string) []map[string]any {
// JSON representation
type (
JSONObject = map[string]any
JSONArray = []JSONObject
)
// no file to load, so we just use the joined args (since each one is a new line)
// and then stop
var (
collection JSONArray
data []byte
err error
)
if len(sendDataArgs) > 0 {
return nil
}
data, err = ReadStdin()
if err != nil {
log.Error().Err(err).Msg("failed to read from standard input")
return nil
}
if len(data) == 0 {
log.Warn().Msg("no data found from standard input")
return nil
}
fmt.Println(string(data))
collection, err = parseInput([]byte(data))
if err != nil {
log.Error().Err(err).Msg("failed to validate input from arg")
}
return collection
}
func parseInput(contents []byte) ([]map[string]any, error) {
var (
data []map[string]any
err error
)
// convert/validate JSON input format
switch sendInputFormat {
case FORMAT_JSON:
err = json.Unmarshal(contents, &data)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal input in JSON: %v", err)
}
case FORMAT_YAML:
err = yaml.Unmarshal(contents, &data)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal input in YAML: %v", err)
}
default:
return nil, fmt.Errorf("unrecognized format")
}
return data, nil
}
// ReadStdin reads all of standard input and returns the bytes. If an error
// occurs during scanning, it is returned.
func ReadStdin() ([]byte, error) {
var b []byte
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
b = append(b, input.Bytes()...)
b = append(b, byte('\n'))
if len(b) == 0 {
break
}
}
if err := input.Err(); err != nil {
return b, fmt.Errorf("failed to read stdin: %w", err)
}
return b, nil
}