multi repo config

This commit is contained in:
Roberto Hidalgo 2022-12-09 00:43:07 -06:00
parent 01a85f6f02
commit cfc2cdcc22
10 changed files with 248 additions and 92 deletions

View File

@ -92,7 +92,7 @@ smtp:
```sh ```sh
# NAME can be either a filesystem path or a colon delimited item name # NAME can be either a filesystem path or a colon delimited item name
# for example: config/host/juazeiro.yaml or host:juazeiro # for example: config/host/juazeiro.yaml or [op-vault-name/]host:juazeiro
# DOT_DELIMITED_PATH is # DOT_DELIMITED_PATH is
# for example: tls.cert, roles.0, dc # for example: tls.cert, roles.0, dc

View File

@ -119,13 +119,11 @@ looks at the filesystem or remotely, using 1password (over the CLI if available,
path := cmd.Arguments[0].ToValue().(string) path := cmd.Arguments[0].ToValue().(string)
query := cmd.Arguments[1].ToValue().(string) query := cmd.Arguments[1].ToValue().(string)
var cfg *config.Config
var err error
remote := cmd.Options["remote"].ToValue().(bool) remote := cmd.Options["remote"].ToValue().(bool)
format := cmd.Options["output"].ToValue().(string) format := cmd.Options["output"].ToValue().(string)
redacted := cmd.Options["redacted"].ToValue().(bool) redacted := cmd.Options["redacted"].ToValue().(bool)
cfg, err = loadExisting(path, remote) cfg, err := config.Load(path, remote)
if err != nil { if err != nil {
return err return err
} }

View File

@ -81,7 +81,7 @@ Will read from stdin (or ﹅--from﹅ a file) and store it at the ﹅PATH
input := cmd.Options["input"].ToValue().(string) input := cmd.Options["input"].ToValue().(string)
parseJSON := cmd.Options["json"].ToValue().(bool) parseJSON := cmd.Options["json"].ToValue().(bool)
cfg, err = loadExisting(path, false) cfg, err = config.Load(path, false)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,72 +0,0 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// 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 cmd
import (
"fmt"
"io/ioutil"
"strings"
opClient "git.rob.mx/nidito/joao/internal/op-client"
"git.rob.mx/nidito/joao/pkg/config"
)
func argIsYAMLFile(path string) bool {
return strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")
}
func pathToName(path string) string {
comps := strings.Split(path, "config/")
return strings.ReplaceAll(strings.Replace(comps[len(comps)-1], ".yaml", "", 1), "/", ":")
}
func nameToPath(name string) string {
return "config/" + strings.ReplaceAll(name, ":", "/") + ".yaml"
}
func loadExisting(ref string, preferRemote bool) (*config.Config, error) {
isYaml := argIsYAMLFile(ref)
if preferRemote {
name := ref
if isYaml {
name = pathToName(ref)
}
item, err := opClient.Get("nidito-admin", name)
if err != nil {
return nil, err
}
return config.ConfigFromOP(item)
}
path := ref
var name string
if !isYaml {
path = nameToPath(ref)
name = ref
} else {
name = pathToName(ref)
}
buf, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("could not read file %s", ref)
}
if len(buf) == 0 {
buf = []byte("{}")
}
return config.ConfigFromYAML(buf, name)
}

View File

@ -179,7 +179,7 @@ func (arg *Argument) ToString() string {
if arg.Variadic { if arg.Variadic {
val := val.([]string) val := val.([]string)
return strings.Join(val, "") return strings.Join(val, " ")
} }
return val.(string) return val.(string)

View File

@ -13,6 +13,7 @@
package command_test package command_test
import ( import (
"reflect"
"strings" "strings"
"testing" "testing"
@ -70,7 +71,7 @@ func TestParse(t *testing.T) {
t.Fatalf("variadic argument isn't on AllKnown map: %v", known) t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
} }
if val != "one two three" { if !reflect.DeepEqual(val, []string{"one", "two", "three"}) {
t.Fatalf("Known argument does not match. expected: %s, got %s", "one two three", val) t.Fatalf("Known argument does not match. expected: %s, got %s", "one two three", val)
} }
@ -96,8 +97,9 @@ func TestParse(t *testing.T) {
t.Fatalf("variadic argument isn't on AllKnown map: %v", known) t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
} }
if val != "defaultVariadic0 defaultVariadic1" { expected := []string{"defaultVariadic0", "defaultVariadic1"}
t.Fatalf("variadic argument does not match. expected: %s, got %s", "defaultVariadic0 defaultVariadic1", val) if !reflect.DeepEqual(val, expected) {
t.Fatalf("variadic argument does not match. expected: %s, got %s", expected, val)
} }
} }
@ -123,8 +125,9 @@ func TestBeforeParse(t *testing.T) {
t.Fatalf("variadic argument isn't on AllKnown map: %v", known) t.Fatalf("variadic argument isn't on AllKnown map: %v", known)
} }
if val != "defaultVariadic0 defaultVariadic1" { expected := []string{"defaultVariadic0", "defaultVariadic1"}
t.Fatalf("variadic argument does not match. expected: %s, got %s", "defaultVariadic0 defaultVariadic1", val) if !reflect.DeepEqual(val, expected) {
t.Fatalf("variadic argument does not match. expected: %s, got %s", expected, val)
} }
} }
@ -195,7 +198,7 @@ func TestArgumentsValidate(t *testing.T) {
Args: []string{"good"}, Args: []string{"good"},
ErrorSuffix: "could not validate argument for command test script bad-exit, ran", ErrorSuffix: "could not validate argument for command test script bad-exit, ran",
Command: (&Command{ Command: (&Command{
// Name: []string{"test", "script", "bad-exit"}, Path: []string{"test", "script", "bad-exit"},
Arguments: []*Argument{ Arguments: []*Argument{
{ {
Name: "first", Name: "first",

View File

@ -28,6 +28,8 @@ func main() {
ForceColors: runtime.ColorEnabled(), ForceColors: runtime.ColorEnabled(),
}) })
logrus.SetLevel(logrus.DebugLevel)
err := registry.Execute(version) err := registry.Execute(version)
if err != nil { if err != nil {

View File

@ -16,6 +16,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"sort" "sort"
"strings" "strings"
@ -25,7 +26,6 @@ import (
) )
type Config struct { type Config struct {
Path string
Vault string Vault string
Name string Name string
Tree *Entry Tree *Entry
@ -85,17 +85,99 @@ func (cfg *Config) ToOP() *op.Item {
return &op.Item{ return &op.Item{
Title: cfg.Name, Title: cfg.Name,
Sections: sections, Sections: sections,
Vault: op.ItemVault{ID: "nidito-admin"}, Vault: op.ItemVault{ID: cfg.Vault},
Category: op.Password, Category: op.Password,
Fields: fields, Fields: fields,
} }
} }
func ConfigFromYAML(data []byte, name string) (*Config, error) { type opDetails struct {
Vault string `yaml:"vault"`
Name string `yaml:"name"`
NameTemplate string `yaml:"nameTemplate"`
}
type opConfig interface {
Name() string
Vault() string
}
type inFileConfig struct {
*opDetails
*yaml.Node
}
type virtualConfig struct {
*opDetails
}
func (ifc *inFileConfig) MarshalYAML() (any, error) {
return ifc.Node, nil
}
func (vc *virtualConfig) MarshalYAML() (any, error) {
return nil, nil
}
func (ifc *inFileConfig) UnmarshalYAML(node *yaml.Node) error {
ifc.Node = node
d := &opDetails{}
if err := node.Decode(&d); err != nil {
return err
}
ifc.opDetails = d
return nil
}
func (ifc *inFileConfig) Name() string {
return ifc.opDetails.Name
}
func (ifc *inFileConfig) Vault() string {
return ifc.opDetails.Name
}
type singleModeConfig struct {
Config *opDetails `yaml:"_config,omitempty"`
}
type repoModeConfig struct {
Repo string
Vault string `yaml:"vault"`
NameTemplate string `yaml:"nameTemplate"`
}
func ConfigFromFile(path string) (*Config, error) {
buf, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("could not read file %s", path)
}
if len(buf) == 0 {
buf = []byte("{}")
}
name, vault, err := vaultAndNameFrom(path, buf)
if err != nil {
return nil, err
}
logrus.Debugf("Found name: %s and vault: %s", name, vault)
cfg, err := ConfigFromYAML(buf)
if err != nil {
return nil, err
}
cfg.Name = name
cfg.Vault = vault
return cfg, nil
}
func ConfigFromYAML(data []byte) (*Config, error) {
cfg := &Config{ cfg := &Config{
Vault: "vault", Tree: NewEntry("root", yaml.MappingNode),
Name: name,
Tree: NewEntry("root", yaml.MappingNode),
} }
yaml.Unmarshal(data, &cfg.Tree) yaml.Unmarshal(data, &cfg.Tree)
@ -135,7 +217,7 @@ func scalarsIn(data map[string]yaml.Node, parents []string) ([]string, error) {
} }
keys = append(keys, ret...) keys = append(keys, ret...)
default: default:
logrus.Fatalf("found unknown %s at %s", leaf.Kind, key) logrus.Fatalf("found unknown %v at %s", leaf.Kind, key)
} }
} }

View File

@ -12,7 +12,13 @@
// limitations under the License. // limitations under the License.
package config package config
import "fmt" import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
func (c *Config) Lookup(query []string) (*Entry, error) { func (c *Config) Lookup(query []string) (*Entry, error) {
if len(query) == 0 || len(query) == 1 && query[0] == "." { if len(query) == 0 || len(query) == 1 && query[0] == "." {
@ -29,3 +35,20 @@ func (c *Config) Lookup(query []string) (*Entry, error) {
return entry, nil return entry, nil
} }
func findRepoConfig(from string) (*repoModeConfig, error) {
parts := strings.Split(from, "/")
for i := len(parts); i > 0; i -= 1 {
query := strings.Join(parts[0:i], "/")
if bytes, err := os.ReadFile(query + "/.joao.yaml"); err == nil {
rmc := &repoModeConfig{Repo: query}
err := yaml.Unmarshal(bytes, rmc)
if err != nil {
return nil, err
}
return rmc, nil
}
}
return nil, nil
}

120
pkg/config/util.go Normal file
View File

@ -0,0 +1,120 @@
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
//
// 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 config
import (
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"text/template"
opClient "git.rob.mx/nidito/joao/internal/op-client"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
func argIsYAMLFile(path string) bool {
return strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")
}
// func pathToName(path string, prefix string) string {
// comps := strings.SplitN(path, prefix+"/", 2)
// return strings.ReplaceAll(strings.Replace(comps[1], ".yaml", "", 1), "/", ":")
// }
// func nameToPath(name string) string {
// return "config/" + strings.ReplaceAll(name, ":", "/") + ".yaml"
// }
func vaultAndNameFrom(path string, buf []byte) (string, string, error) {
smc := &singleModeConfig{}
if buf == nil {
var err error
buf, err = ioutil.ReadFile(path)
if err != nil {
return "", "", fmt.Errorf("could not read file %s", path)
}
}
if err := yaml.Unmarshal(buf, &smc); err == nil && smc.Config != nil {
return smc.Config.Vault, smc.Config.Name, nil
}
rmc, err := findRepoConfig(path)
if err != nil {
return "", "", err
}
if rmc == nil {
return "", "", fmt.Errorf("could not find repo config for %s", path)
}
if rmc.NameTemplate == "" {
rmc.NameTemplate = "{{ DirName }}:{{ FileName}}"
}
logrus.Debugf("Found repo config at %s", rmc.Repo)
tpl := template.Must(template.New("help").Funcs(template.FuncMap{
"DirName": func() string {
return filepath.Base(filepath.Dir(path))
},
"FileName": func() string {
return strings.Split(filepath.Base(path), ".")[0]
},
}).Parse(rmc.NameTemplate))
var nameBuf bytes.Buffer
err = tpl.Execute(&nameBuf, nil)
if err != nil {
return "", "", err
}
return nameBuf.String(), rmc.Vault, nil
}
func Load(ref string, preferRemote bool) (*Config, error) {
isYaml := argIsYAMLFile(ref)
if preferRemote {
name := ref
vault := ""
if isYaml {
var err error
name, vault, err = vaultAndNameFrom(ref, nil)
if err != nil {
return nil, err
}
} else {
parts := strings.SplitN(ref, "/", 2)
if len(parts) > 1 {
vault = parts[0]
name = parts[1]
}
}
item, err := opClient.Get(vault, name)
if err != nil {
return nil, err
}
return ConfigFromOP(item)
}
if !isYaml {
return nil, fmt.Errorf("could not load %s from local as it's not a path", ref)
}
return ConfigFromFile(ref)
}