multi repo config
This commit is contained in:
parent
01a85f6f02
commit
cfc2cdcc22
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
72
cmd/util.go
72
cmd/util.go
|
@ -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)
|
|
||||||
}
|
|
|
@ -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",
|
||||||
|
|
2
main.go
2
main.go
|
@ -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 {
|
||||||
|
|
|
@ -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,16 +85,98 @@ 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",
|
|
||||||
Name: name,
|
|
||||||
Tree: NewEntry("root", yaml.MappingNode),
|
Tree: NewEntry("root", yaml.MappingNode),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue