Skip to content

Commit

Permalink
Basic schema validation and improved help output.
Browse files Browse the repository at this point in the history
  • Loading branch information
kristofferahl committed Feb 25, 2023
1 parent 5bdbd59 commit e3def98
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 26 deletions.
16 changes: 6 additions & 10 deletions cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"os"

"github.com/dotnetmentor/rq/internal/pkg/schema"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
Expand All @@ -19,21 +18,17 @@ var (
)

var getCmd = &cobra.Command{
Use: "get",
Use: "get [resource]",
Short: "Get resource by key",
Long: `Get a single resource by key`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug().Msg("reading manifest...")
m, err := schema.NewManifest(opt.FilePath)
if err != nil {
return err
}
m := manifest

log.Debug().Msg("validating resource type...")
rt, ok := m.ResourceType(args[0])
if !ok {
return fmt.Errorf("unknown resource type %s, valid resource types: %s", args[0], m.ResourceTypeNames())
rt, err := m.ValidateResourceType(args[0])
if err != nil {
return err
}

resourceKey := args[1]
Expand Down Expand Up @@ -66,4 +61,5 @@ var getCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(getCmd)
getCmd.Flags().StringVarP(&getOpt.Property, "select", "s", "", "selects value of a property (eg. build)")
getCmd.SetUsageFunc(customUsageFunc)
}
15 changes: 6 additions & 9 deletions cmd/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,20 @@ var (
)

var queryCmd = &cobra.Command{
Use: "query",
Use: "query [resource]",
Short: "Query resources",
Long: `Query resources`,
Args: cobra.ExactArgs(1),
PostRun: func(cmd *cobra.Command, args []string) {
resetQueryOpt()
},
RunE: func(cmd *cobra.Command, args []string) error {
log.Debug().Msg("reading manifest...")
m, err := schema.NewManifest(opt.FilePath)
if err != nil {
return err
}
m := manifest

log.Debug().Msg("validating resource type...")
rt, ok := m.ResourceType(args[0])
if !ok {
return fmt.Errorf("unknown resource type %s, valid resource types: %s", args[0], m.ResourceTypeNames())
rt, err := m.ValidateResourceType(args[0])
if err != nil {
return err
}

conditions, err := query.ParseArgs(queryOpt.Parameters)
Expand Down Expand Up @@ -93,6 +89,7 @@ func init() {
queryCmd.Flags().BoolVarP(&queryOpt.DisableStrictMatching, "disable-strict-matching", "d", false, "disable strict matching (matches conditions where parameter is missing)")
queryCmd.Flags().BoolVarP(&queryOpt.Sort, "sort", "s", false, "sort output (lexicographically)")
queryCmd.Flags().StringVarP(&queryOpt.Output, "out", "o", output.Newline, "output type (eg. json/xargs)")
queryCmd.SetUsageFunc(customUsageFunc)
}

func resetQueryOpt() {
Expand Down
25 changes: 23 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"time"

"github.com/dotnetmentor/rq/internal/pkg/schema"
"github.com/dotnetmentor/rq/version"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
Expand All @@ -17,7 +18,8 @@ type GlobalOptions struct {
}

var (
opt GlobalOptions
opt GlobalOptions
manifest schema.Manifest
)

var RootCmd = &cobra.Command{
Expand All @@ -26,13 +28,21 @@ var RootCmd = &cobra.Command{
Long: `rq - for querying resources`,
SilenceUsage: true,
Version: fmt.Sprintf("%s (commit=%s)", version.Version, version.Commit),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
level, err := zerolog.ParseLevel(opt.LogLevel)
if err != nil {
fmt.Printf("invalid log level %s, err: %s", opt.LogLevel, err)
os.Exit(1)
}
log.Logger = log.Level(level).Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})

log.Debug().Msg("reading manifest...")
err = tryReadManifest()
if err != nil {
return err
}

return nil
},
}

Expand All @@ -53,6 +63,17 @@ func environmentOrDefault(key string, defaultValue string) string {
return value
}

func tryReadManifest() error {
if !manifest.Parsed() {
m, err := schema.NewManifest(opt.FilePath)
if err != nil {
return err
}
manifest = m
}
return nil
}

func init() {
RootCmd.PersistentFlags().StringVarP(&opt.FilePath, "file", "f", environmentOrDefault("RQ_DEFAULT_FILE", "rq.yaml"), "Manifest file (eg. rq.yaml)")
RootCmd.PersistentFlags().StringVarP(&opt.LogLevel, "level", "l", "info", "Log level (eg. trace/debug/warn/error)")
Expand Down
56 changes: 56 additions & 0 deletions cmd/usage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package cmd

import (
"io"
"strings"
"text/template"
"unicode"

"github.com/spf13/cobra"
)

const customUsageTemplate = `Usage:{{if .Runnable}}
{{.UseLine}}{{end}}
Resources:{{resourceTypes | trimTrailingWhitespaces}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
`

var templateFuncs = template.FuncMap{
"trimTrailingWhitespaces": trimTrailingWhitespaces,
"resourceTypes": resourceTypes,
}

func customUsageFunc(cmd *cobra.Command) error {
tryReadManifest()

err := tmpl(cmd.OutOrStderr(), customUsageTemplate, cmd)
if err != nil {
cmd.PrintErrln(err)
}
return err
}

func resourceTypes() string {
if len(manifest.Config.Resources) > 0 {
return manifest.ResourceTypeNames()
}
return " unknown"
}

func trimTrailingWhitespaces(s string) string {
return strings.TrimRightFunc(s, unicode.IsSpace)
}

// tmpl executes the given template text on data, writing the result to w.
func tmpl(w io.Writer, text string, data interface{}) error {
t := template.New("top")
t.Funcs(templateFuncs)
template.Must(t.Parse(text))
return t.Execute(w, data)
}
17 changes: 14 additions & 3 deletions internal/pkg/schema/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,20 @@ func (n CustomResourceNames) Match(t string) bool {
}

func (n CustomResourceNames) String() string {
if strings.HasPrefix(n.Plural, n.Singular) {
out := ""
if n.Plural != "" && n.Singular != "" && strings.HasPrefix(n.Plural, n.Singular) {
pe := strings.ReplaceAll(n.Plural, n.Singular, "")
return fmt.Sprintf(" - %s(%s) (short: %s)", n.Singular, pe, n.Short)
out = fmt.Sprintf(" - %s(%s)", n.Singular, pe)
} else {
v := []string{n.Plural}
if n.Singular != "" {
v = append(v, n.Singular)
}
out = fmt.Sprintf(" - %s", strings.Join(v, "/"))
}
return fmt.Sprintf(" - %s/%s (short: %s)", n.Plural, n.Singular, n.Short)

if n.Short != "" {
out = fmt.Sprintf("%s (short: %s)", out, n.Short)
}
return out
}
51 changes: 49 additions & 2 deletions internal/pkg/schema/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

yaml2 "gopkg.in/yaml.v2"
)
Expand Down Expand Up @@ -34,18 +35,64 @@ func NewManifest(path string) (Manifest, error) {
if err != nil {
return Manifest{}, fmt.Errorf("failed to parse manifest yaml. %v", err)
}
m.parsed = true

// TODO: validate manifest
// - Resource keys must be unique for each resource type
errors := make([]string, 0)
for i, cr := range m.Config.Resources {
if cr.Kind == "" {
errors = append(errors, fmt.Sprintf("config.resources[%d]: missing property \"kind\"", i))
}
if cr.Names.Plural == "" {
errors = append(errors, fmt.Sprintf("config.resources[%d]: missing property \"plural\"", i))
}
}

for _, rt := range m.Config.Resources {
keys := make([]string, 0)
for _, key := range m.ResourcesOfType(rt).SelectMany(func(i Resource) string {
return i.Key
}) {
if contains(keys, key) {
errors = append(errors, fmt.Sprintf("resources[%s].key: key \"%s\" must be unique", rt.Kind, key))
}
keys = append(keys, key)
}
}

if len(errors) > 0 {
return m, fmt.Errorf("invalid resource manifest, error(s):\n\n%s", strings.Join(errors, "\n"))
}

return m, nil
}

func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}

type Manifest struct {
parsed bool
Config Config `yaml:"config,omitempty"`
Root map[string]ResourceList `yaml:"resources,omitempty"`
}

func (m Manifest) Parsed() bool {
return m.parsed
}

func (m Manifest) ValidateResourceType(t string) (CustomResource, error) {
rt, ok := m.ResourceType(t)
if !ok {
return CustomResource{}, fmt.Errorf("unknown resource type %s, valid resource types: \n%s", t, m.ResourceTypeNames())
}
return rt, nil
}

func (m Manifest) ResourcesOfType(cr CustomResource) ResourceList {
for rk, rl := range m.Root {
if cr.Names.Match(rk) {
Expand Down

0 comments on commit e3def98

Please sign in to comment.