refactor: initial commit for major rewrite
This commit is contained in:
parent
3253cb8bbb
commit
bfd83f35a3
45 changed files with 439 additions and 1733 deletions
|
|
@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
[0.2.0]
|
||||
|
||||
- Major rewrite of entire code base
|
||||
|
||||
[0.1.0]
|
||||
|
||||
- Initial prerelease of configurator
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
)
|
||||
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Create a new default config file",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// create a new config at all args (paths)
|
||||
//
|
||||
// TODO: change this to only take a single arg since more
|
||||
// than one arg is *maybe* a mistake
|
||||
for _, path := range args {
|
||||
// check and make sure something doesn't exist first
|
||||
if exists, err := util.PathExists(path); exists || err != nil {
|
||||
log.Error().Err(err).Msg("file or directory exists")
|
||||
continue
|
||||
}
|
||||
config.SaveDefault(path)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(configCmd)
|
||||
}
|
||||
7
cmd/download.go
Normal file
7
cmd/download.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var downloadCmd = cobra.Command{
|
||||
Use: "download",
|
||||
}
|
||||
77
cmd/fetch.go
77
cmd/fetch.go
|
|
@ -1,77 +0,0 @@
|
|||
//go:build client || all
|
||||
// +build client all
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var fetchCmd = &cobra.Command{
|
||||
Use: "fetch",
|
||||
Short: "Fetch a config file from a remote instance of configurator",
|
||||
Long: "This command is simplified to make a HTTP request to the a configurator service.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// make sure a host is set
|
||||
if remoteHost == "" {
|
||||
log.Error().Msg("no '--host' argument set")
|
||||
return
|
||||
}
|
||||
|
||||
// check if we actually have any targets to run
|
||||
if len(targets) <= 0 {
|
||||
log.Error().Msg("must specify a target")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// check to see if an access token is available from env
|
||||
if conf.AccessToken == "" {
|
||||
// check if OCHAMI_ACCESS_TOKEN env var is set if no access token is provided and use that instead
|
||||
accessToken := os.Getenv("ACCESS_TOKEN")
|
||||
if accessToken != "" {
|
||||
conf.AccessToken = accessToken
|
||||
} else {
|
||||
// TODO: try and fetch token first if it is needed
|
||||
if verbose {
|
||||
log.Warn().Msg("No token found. Attempting to generate config without one...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add the "Authorization" header if an access token is supplied
|
||||
headers := map[string]string{}
|
||||
if accessToken != "" {
|
||||
headers["Authorization"] = "Bearer " + accessToken
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
// make a request for each target
|
||||
url := fmt.Sprintf("%s/generate?target=%s", remoteHost, target)
|
||||
res, body, err := util.MakeRequest(url, http.MethodGet, nil, headers)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to fetch files")
|
||||
return
|
||||
}
|
||||
// handle getting other error codes other than a 200
|
||||
if res != nil {
|
||||
// NOTE: the server responses are already marshaled to JSON
|
||||
fmt.Print(string(body))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
fetchCmd.Flags().StringVar(&remoteHost, "host", "", "set the remote configurator host and port")
|
||||
fetchCmd.Flags().StringSliceVar(&targets, "target", nil, "set the target configs to make")
|
||||
fetchCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for config targets")
|
||||
fetchCmd.Flags().StringVar(&accessToken, "access-token", "o", "set the output path for config targets")
|
||||
|
||||
rootCmd.AddCommand(fetchCmd)
|
||||
}
|
||||
212
cmd/generate.go
212
cmd/generate.go
|
|
@ -1,212 +0,0 @@
|
|||
//go:build client || all
|
||||
// +build client all
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/client"
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/generator"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenFetchRetries int
|
||||
templatePaths []string
|
||||
pluginPath string
|
||||
useCompression bool
|
||||
)
|
||||
|
||||
var generateCmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate a config file from state management",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// make sure that we have a token present before trying to make request
|
||||
if conf.AccessToken == "" {
|
||||
// check if OCHAMI_ACCESS_TOKEN env var is set if no access token is provided and use that instead
|
||||
accessToken := os.Getenv("ACCESS_TOKEN")
|
||||
if accessToken != "" {
|
||||
conf.AccessToken = accessToken
|
||||
} else {
|
||||
// TODO: try and fetch token first if it is needed
|
||||
if verbose {
|
||||
log.Warn().Msg("No token found. Attempting to generate conf without one...\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// use cert path from cobra if empty
|
||||
if conf.CertPath == "" {
|
||||
conf.CertPath = cacertPath
|
||||
}
|
||||
|
||||
// show conf as JSON and generators if verbose
|
||||
if verbose {
|
||||
b, err := json.MarshalIndent(conf, "", " ")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal config")
|
||||
}
|
||||
// print the config file as JSON
|
||||
fmt.Printf("%v\n", string(b))
|
||||
}
|
||||
|
||||
// run all of the target recursively until completion if provided
|
||||
if len(targets) > 0 {
|
||||
RunTargets(&conf, args, targets...)
|
||||
} else {
|
||||
if pluginPath == "" {
|
||||
log.Error().Msg("no plugin path specified")
|
||||
return
|
||||
}
|
||||
|
||||
// load the templates to use
|
||||
templates := map[string]generator.Template{}
|
||||
for _, path := range templatePaths {
|
||||
template := generator.Template{}
|
||||
template.LoadFromFile(path)
|
||||
if !template.IsEmpty() {
|
||||
templates[path] = template
|
||||
}
|
||||
}
|
||||
|
||||
params := generator.Params{
|
||||
Templates: templates,
|
||||
}
|
||||
|
||||
// set the client options
|
||||
// params.ClientOpts = append(params.ClientOpts, client.WithHost(remoteHost))
|
||||
if conf.AccessToken != "" {
|
||||
params.ClientOpts = append(params.ClientOpts, client.WithAccessToken(conf.AccessToken))
|
||||
}
|
||||
if conf.CertPath != "" {
|
||||
params.ClientOpts = append(params.ClientOpts, client.WithCertPoolFile(conf.CertPath))
|
||||
}
|
||||
|
||||
// run generator.Generate() with just plugin path and templates provided
|
||||
outputBytes, err := generator.Generate(&conf, pluginPath, params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to generate files")
|
||||
}
|
||||
|
||||
// if we have more than one target and output is set, create configs in directory
|
||||
outputMap := generator.ConvertContentsToString(outputBytes)
|
||||
writeOutput(outputBytes, len(targets), len(outputMap))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Generate files by supplying a list of targets as string values. Currently,
|
||||
// targets are defined statically in a config file. Targets are ran recursively
|
||||
// if more targets are nested in a defined target, but will not run additional
|
||||
// child targets if it is the same as the parent.
|
||||
//
|
||||
// NOTE: This may be changed in the future how this is done.
|
||||
func RunTargets(conf *config.Config, args []string, targets ...string) {
|
||||
// generate config with each supplied target
|
||||
for _, target := range targets {
|
||||
outputBytes, err := generator.GenerateWithTarget(conf, target)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("target", target).Msg("failed to generate config")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// if we have more than one target and output is set, create configs in directory
|
||||
outputMap := generator.ConvertContentsToString(outputBytes)
|
||||
writeOutput(outputBytes, len(targets), len(outputMap))
|
||||
|
||||
// remove any targets that are the same as current to prevent infinite loop
|
||||
nextTargets := util.CopyIf(conf.Targets[target].RunTargets, func(nextTarget string) bool {
|
||||
return nextTarget != target
|
||||
})
|
||||
|
||||
// ...then, run any other targets that the current target has
|
||||
RunTargets(conf, args, nextTargets...)
|
||||
}
|
||||
}
|
||||
|
||||
func writeOutput(outputBytes generator.FileMap, targetCount int, templateCount int) {
|
||||
outputMap := generator.ConvertContentsToString(outputBytes)
|
||||
if outputPath == "" {
|
||||
// write only to stdout by default
|
||||
if len(outputMap) == 1 {
|
||||
for _, contents := range outputMap {
|
||||
fmt.Printf("%s\n", string(contents))
|
||||
}
|
||||
} else {
|
||||
for path, contents := range outputMap {
|
||||
fmt.Printf("-- file: %s, size: %d B\n%s\n", path, len(contents), string(contents))
|
||||
}
|
||||
}
|
||||
} else if outputPath != "" && targetCount == 1 && templateCount == 1 {
|
||||
// write just a single file using provided name
|
||||
for _, contents := range outputBytes {
|
||||
err := os.WriteFile(outputPath, contents, 0o644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", outputPath).Msg("failed to write config file")
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info().Msgf("wrote file to '%s'\n", outputPath)
|
||||
}
|
||||
} else if outputPath != "" && targetCount > 1 && useCompression {
|
||||
// write multiple files to archive, compress, then save to output path
|
||||
out, err := os.Create(fmt.Sprintf("%s.tar.gz", outputPath))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", outputPath).Msg("failed to write archive")
|
||||
os.Exit(1)
|
||||
}
|
||||
files := make([]string, len(outputBytes))
|
||||
i := 0
|
||||
for path := range outputBytes {
|
||||
files[i] = path
|
||||
i++
|
||||
}
|
||||
err = util.CreateArchive(files, out)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", outputPath).Msg("failed to create archive")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
} else if outputPath != "" && targetCount > 1 || templateCount > 1 {
|
||||
// write multiple files in directory using template name
|
||||
err := os.MkdirAll(filepath.Clean(outputPath), 0o755)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", filepath.Clean(outputPath)).Msg("failed to make output directory")
|
||||
os.Exit(1)
|
||||
}
|
||||
for path, contents := range outputBytes {
|
||||
filename := filepath.Base(path)
|
||||
cleanPath := fmt.Sprintf("%s/%s", filepath.Clean(outputPath), filename)
|
||||
err := os.WriteFile(cleanPath, contents, 0o755)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", path).Msg("failed to write config to file")
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info().Msgf("wrote file to '%s'\n", cleanPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
generateCmd.Flags().StringSliceVar(&targets, "target", []string{}, "set the targets to run pre-defined conf")
|
||||
generateCmd.Flags().StringSliceVar(&templatePaths, "template", []string{}, "set the paths for the Jinja 2 templates to use")
|
||||
generateCmd.Flags().StringVar(&pluginPath, "plugin", "", "set the generator plugin path")
|
||||
generateCmd.Flags().StringVarP(&outputPath, "output", "o", "", "set the output path for conf targets")
|
||||
generateCmd.Flags().IntVar(&tokenFetchRetries, "fetch-retries", 5, "set the number of retries to fetch an access token")
|
||||
generateCmd.Flags().StringVar(&remoteHost, "host", "http://localhost", "set the remote host")
|
||||
generateCmd.Flags().BoolVar(&useCompression, "compress", false, "set whether to archive and compress multiple file outputs")
|
||||
|
||||
// requires either 'target' by itself or 'plugin' and 'templates' together
|
||||
// generateCmd.MarkFlagsOneRequired("target", "plugin")
|
||||
generateCmd.MarkFlagsMutuallyExclusive("target", "plugin")
|
||||
generateCmd.MarkFlagsMutuallyExclusive("target", "template")
|
||||
generateCmd.MarkFlagsRequiredTogether("plugin", "template")
|
||||
|
||||
rootCmd.AddCommand(generateCmd)
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/generator"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"github.com/rodaine/table"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
byTarget bool
|
||||
)
|
||||
|
||||
var inspectCmd = &cobra.Command{
|
||||
Use: "inspect",
|
||||
Short: "Inspect generator plugin information",
|
||||
Long: "The 'inspect' sub-command takes a list of directories and prints all found plugin information.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// set up table formatter
|
||||
table.DefaultHeaderFormatter = func(format string, vals ...interface{}) string {
|
||||
return strings.ToUpper(fmt.Sprintf(format, vals...))
|
||||
}
|
||||
|
||||
// remove duplicate clean paths from CLI
|
||||
paths := make([]string, len(args))
|
||||
for _, path := range args {
|
||||
paths = append(paths, filepath.Clean(path))
|
||||
}
|
||||
paths = util.RemoveDuplicates(paths)
|
||||
|
||||
// load specific plugins from positional args
|
||||
var generators = make(map[string]generator.Generator)
|
||||
for _, path := range paths {
|
||||
err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
gen, err := generator.LoadPlugin(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
generators[gen.GetName()] = gen
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to walk directory")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// print all generator plugin information found
|
||||
tbl := table.New("Name", "Version", "Description")
|
||||
for _, g := range generators {
|
||||
tbl.AddRow(g.GetName(), g.GetVersion(), g.GetDescription())
|
||||
}
|
||||
if len(generators) > 0 {
|
||||
tbl.Print()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
inspectCmd.Flags().BoolVar(&byTarget, "by-target", false, "set whether to ")
|
||||
rootCmd.AddCommand(inspectCmd)
|
||||
}
|
||||
0
cmd/list.go
Normal file
0
cmd/list.go
Normal file
0
cmd/profiles.go
Normal file
0
cmd/profiles.go
Normal file
0
cmd/render.go
Normal file
0
cmd/render.go
Normal file
65
cmd/root.go
65
cmd/root.go
|
|
@ -4,35 +4,20 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
conf config.Config
|
||||
configPath string
|
||||
cacertPath string
|
||||
verbose bool
|
||||
targets []string
|
||||
outputPath string
|
||||
accessToken string
|
||||
remoteHost string
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "configurator",
|
||||
Short: "Dynamically generate files defined by generators",
|
||||
var rootCmd = cobra.Command{
|
||||
Use: "configurator",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
cmd.Help()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
// run initialization code first
|
||||
initEnv()
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
|
|
@ -40,39 +25,13 @@ func Execute() {
|
|||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(InitConfig)
|
||||
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "set the config path")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "set to enable verbose output")
|
||||
rootCmd.PersistentFlags().StringVar(&cacertPath, "cacert", "", "path to CA cert. (defaults to system CAs)")
|
||||
// initialize the config a single time
|
||||
}
|
||||
|
||||
func InitConfig() {
|
||||
// empty from not being set
|
||||
if configPath != "" {
|
||||
exists, err := util.PathExists(configPath)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("path", configPath).Msg("failed to load config")
|
||||
os.Exit(1)
|
||||
} else if exists {
|
||||
conf = config.Load(configPath)
|
||||
} else {
|
||||
// show error and exit since a path was specified
|
||||
log.Error().Str("path", configPath).Msg("config file not found")
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
// set to the default value and create a new one
|
||||
configPath = "./config.yaml"
|
||||
conf = config.New()
|
||||
}
|
||||
func initConfigFromFile(path string) {
|
||||
|
||||
}
|
||||
|
||||
func initEnv() {
|
||||
|
||||
//
|
||||
// set environment variables to override config values
|
||||
//
|
||||
|
||||
// set the JWKS url if we find the CONFIGURATOR_JWKS_URL environment variable
|
||||
jwksUrl := os.Getenv("CONFIGURATOR_JWKS_URL")
|
||||
if jwksUrl != "" {
|
||||
conf.Server.Jwks.Uri = jwksUrl
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
cmd/serve.go
67
cmd/serve.go
|
|
@ -1,67 +0,0 @@
|
|||
//go:build server || all
|
||||
// +build server all
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start configurator as a server and listen for requests",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// make sure that we have a token present before trying to make request
|
||||
if conf.AccessToken == "" {
|
||||
// check if ACCESS_TOKEN env var is set if no access token is provided and use that instead
|
||||
accessToken := os.Getenv("ACCESS_TOKEN")
|
||||
if accessToken != "" {
|
||||
conf.AccessToken = accessToken
|
||||
} else {
|
||||
if verbose {
|
||||
log.Warn().Msg("No token found. Continuing without one...\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// show config as JSON and generators if verbose
|
||||
if verbose {
|
||||
b, err := json.MarshalIndent(conf, "", "\t")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal config")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("%v\n", string(b))
|
||||
}
|
||||
|
||||
// start listening with the server
|
||||
var (
|
||||
s *server.Server = server.New(&conf)
|
||||
err error = s.Serve()
|
||||
)
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
if verbose {
|
||||
log.Info().Msg("server closed")
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Error().Err(err).Msg("failed to start server")
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
serveCmd.Flags().StringVar(&conf.Server.Host, "host", conf.Server.Host, "set the server host and port")
|
||||
// serveCmd.Flags().StringVar(&pluginPath, "plugin", "", "set the generator plugins directory path")
|
||||
serveCmd.Flags().StringVar(&conf.Server.Jwks.Uri, "jwks-uri", conf.Server.Jwks.Uri, "set the JWKS url to fetch public key")
|
||||
serveCmd.Flags().IntVar(&conf.Server.Jwks.Retries, "jwks-fetch-retries", conf.Server.Jwks.Retries, "set the JWKS fetch retry count")
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
14
cmd/upload.go
Normal file
14
cmd/upload.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package cmd
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var uploadCmd = &cobra.Command{
|
||||
Use: "upload",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(uploadCmd)
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/generator"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/generator"
|
||||
)
|
||||
|
||||
type TestGenerator struct{}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# Description: {{ plugin_description }}
|
||||
#
|
||||
# Source code: https://github.com/OpenCHAMI/configurator
|
||||
# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins
|
||||
# Creating plugins: https://git.towk2.me/towk/configurator/blob/main/README.md#creating-generator-plugins
|
||||
#
|
||||
SERVER keepalive=ON
|
||||
SERVER logdir="/var/log/conman"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# Description: {{ plugin_description }}
|
||||
#
|
||||
# Source code: https://github.com/OpenCHAMI/configurator
|
||||
# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins
|
||||
# Creating plugins: https://git.towk2.me/towk/configurator/blob/main/README.md#creating-generator-plugins
|
||||
#
|
||||
allow booting;
|
||||
allow bootp;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@
|
|||
# Description: {{ plugin_description }}
|
||||
#
|
||||
# Source code: https://github.com/OpenCHAMI/configurator
|
||||
# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins
|
||||
# Creating plugins: https://git.towk2.me/towk/configurator/blob/main/README.md#creating-generator-plugins
|
||||
#
|
||||
{{ dhcp_hosts }}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# Description: {{ plugin_description }}
|
||||
#
|
||||
# Source code: https://github.com/OpenCHAMI/configurator
|
||||
# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins
|
||||
# Creating plugins: https://git.towk2.me/towk/configurator/blob/main/README.md#creating-generator-plugins
|
||||
#
|
||||
include "/etc/powerman/ipmipower.dev"
|
||||
include "/etc/powerman/ipmi.dev"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# Description: {{ plugin_description }}
|
||||
#
|
||||
# Source code: https://github.com/OpenCHAMI/configurator
|
||||
# Creating plugins: https://github.com/OpenCHAMI/configurator/blob/main/README.md#creating-generator-plugins
|
||||
# Creating plugins: https://git.towk2.me/towk/configurator/blob/main/README.md#creating-generator-plugins
|
||||
#
|
||||
|
||||
# TODO: test variables
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -1,4 +1,4 @@
|
|||
module github.com/OpenCHAMI/configurator
|
||||
module git.towk2.me/towk/configurator
|
||||
|
||||
go 1.21.5
|
||||
|
||||
|
|
@ -11,7 +11,6 @@ require (
|
|||
github.com/openchami/chi-middleware/log v0.0.0-20240812224658-b16b83c70700
|
||||
github.com/rodaine/table v1.2.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
|
|
@ -35,6 +34,7 @@ require (
|
|||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
|
|
|
|||
2
main.go
2
main.go
|
|
@ -1,6 +1,6 @@
|
|||
package main
|
||||
|
||||
import "github.com/OpenCHAMI/configurator/cmd"
|
||||
import "git.towk2.me/towk/configurator/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Option func(*Params)
|
||||
type Params struct {
|
||||
Host string `yaml:"host"`
|
||||
AccessToken string `yaml:"access-token"`
|
||||
Transport *http.Transport
|
||||
}
|
||||
|
||||
func ToParams(opts ...Option) *Params {
|
||||
params := &Params{}
|
||||
for _, opt := range opts {
|
||||
opt(params)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func WithHost(host string) Option {
|
||||
return func(c *Params) {
|
||||
c.Host = host
|
||||
}
|
||||
}
|
||||
|
||||
func WithAccessToken(token string) Option {
|
||||
return func(c *Params) {
|
||||
c.AccessToken = token
|
||||
}
|
||||
}
|
||||
|
||||
func WithCertPool(certPool *x509.CertPool) Option {
|
||||
return func(c *Params) {
|
||||
c.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: certPool,
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
DisableKeepAlives: true,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 120 * time.Second,
|
||||
KeepAlive: 120 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 120 * time.Second,
|
||||
ResponseHeaderTimeout: 120 * time.Second,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Need to check for errors when reading from a file
|
||||
func WithCertPoolFile(certPath string) Option {
|
||||
if certPath == "" {
|
||||
return func(sc *Params) {}
|
||||
}
|
||||
cacert, _ := os.ReadFile(certPath)
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AppendCertsFromPEM(cacert)
|
||||
return WithCertPool(certPool)
|
||||
}
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/pkg"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// An struct that's meant to extend functionality of the base HTTP client by
|
||||
// adding commonly made requests to SMD. The implemented functions are can be
|
||||
// used in generator plugins to fetch data when it is needed to substitute
|
||||
// values for the Jinja templates used.
|
||||
type SmdClient struct {
|
||||
http.Client `json:"-" yaml:"-"`
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
AccessToken string `yaml:"access-token"`
|
||||
}
|
||||
|
||||
// Constructor function that allows supplying Option arguments to set
|
||||
// things like the host, port, access token, etc.
|
||||
func NewSmdClient(opts ...Option) SmdClient {
|
||||
var (
|
||||
params = ToParams(opts...)
|
||||
client = SmdClient{
|
||||
Host: params.Host,
|
||||
AccessToken: params.AccessToken,
|
||||
}
|
||||
)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// Fetch the ethernet interfaces from SMD service using its API. An access token may be required if the SMD
|
||||
// service SMD_JWKS_URL envirnoment variable is set.
|
||||
func (client *SmdClient) FetchEthernetInterfaces(verbose bool) ([]configurator.EthernetInterface, error) {
|
||||
var (
|
||||
eths = []configurator.EthernetInterface{}
|
||||
bytes []byte
|
||||
err error
|
||||
)
|
||||
// make request to SMD endpoint
|
||||
bytes, err = client.makeRequest("/Inventory/EthernetInterfaces")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read HTTP response: %v", err)
|
||||
}
|
||||
|
||||
// unmarshal response body JSON and extract in object
|
||||
err = json.Unmarshal(bytes, ðs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// print what we got if verbose is set
|
||||
if verbose {
|
||||
log.Info().Str("ethernet_interfaces", string(bytes)).Msg("found interfaces")
|
||||
}
|
||||
|
||||
return eths, nil
|
||||
}
|
||||
|
||||
// Fetch the components from SMD using its API. An access token may be required if the SMD
|
||||
// service SMD_JWKS_URL envirnoment variable is set.
|
||||
func (client *SmdClient) FetchComponents(verbose bool) ([]configurator.Component, error) {
|
||||
var (
|
||||
comps = []configurator.Component{}
|
||||
bytes []byte
|
||||
err error
|
||||
)
|
||||
// make request to SMD endpoint
|
||||
bytes, err = client.makeRequest("/State/Components")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
|
||||
}
|
||||
|
||||
// make sure our response is actually JSON
|
||||
if !json.Valid(bytes) {
|
||||
return nil, fmt.Errorf("expected valid JSON response: %v", string(bytes))
|
||||
}
|
||||
|
||||
// unmarshal response body JSON and extract in object
|
||||
var tmp map[string]any
|
||||
err = json.Unmarshal(bytes, &tmp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
bytes, err = json.Marshal(tmp["RedfishEndpoints"].([]any))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal JSON: %v", err)
|
||||
}
|
||||
err = json.Unmarshal(bytes, &comps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// print what we got if verbose is set
|
||||
if verbose {
|
||||
log.Info().Str("components", string(bytes)).Msg("found components")
|
||||
}
|
||||
|
||||
return comps, nil
|
||||
}
|
||||
|
||||
// TODO: improve implementation of this function
|
||||
func (client *SmdClient) FetchRedfishEndpoints(verbose bool) ([]configurator.RedfishEndpoint, error) {
|
||||
var (
|
||||
eps = []configurator.RedfishEndpoint{}
|
||||
tmp map[string]any
|
||||
)
|
||||
|
||||
// make initial request to get JSON with 'RedfishEndpoints' as property
|
||||
b, err := client.makeRequest("/Inventory/RedfishEndpoints")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make HTTP resquest: %v", err)
|
||||
}
|
||||
// make sure response is in JSON
|
||||
if !json.Valid(b) {
|
||||
return nil, fmt.Errorf("expected valid JSON response: %v", string(b))
|
||||
}
|
||||
err = json.Unmarshal(b, &tmp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// marshal RedfishEndpoint JSON back to configurator.RedfishEndpoint
|
||||
b, err = json.Marshal(tmp["RedfishEndpoints"].([]any))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal JSON: %v", err)
|
||||
}
|
||||
err = json.Unmarshal(b, &eps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
// show the final result
|
||||
if verbose {
|
||||
log.Info().Str("redfish_endpoints", string(b)).Msg("found redfish endpoints")
|
||||
}
|
||||
|
||||
return eps, nil
|
||||
}
|
||||
|
||||
func (client *SmdClient) makeRequest(endpoint string) ([]byte, error) {
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("client is nil")
|
||||
}
|
||||
|
||||
// fetch DHCP related information from SMD's endpoint:
|
||||
url := fmt.Sprintf("%s/hsm/v2%s", client.Host, endpoint)
|
||||
req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer([]byte{}))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new HTTP request: %v", err)
|
||||
}
|
||||
|
||||
// include access token in authorzation header if found
|
||||
// NOTE: This shouldn't be needed for this endpoint since it's public
|
||||
if client.AccessToken != "" {
|
||||
req.Header.Add("Authorization", "Bearer "+client.AccessToken)
|
||||
}
|
||||
|
||||
// make the request to SMD
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||
}
|
||||
|
||||
// read the contents of the response body
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/pkg"
|
||||
"github.com/OpenCHAMI/configurator/pkg/client"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Jwks struct {
|
||||
Uri string `yaml:"uri"`
|
||||
Retries int `yaml:"retries,omitempty"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Jwks Jwks `yaml:"jwks,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Version string `yaml:"version,omitempty"`
|
||||
Server Server `yaml:"server,omitempty"`
|
||||
SmdClient client.SmdClient `yaml:"smd,omitempty"`
|
||||
AccessToken string `yaml:"access-token,omitempty"`
|
||||
Targets map[string]configurator.Target `yaml:"targets,omitempty"`
|
||||
PluginDirs []string `yaml:"plugins,omitempty"`
|
||||
CertPath string `yaml:"cacert,omitempty"`
|
||||
}
|
||||
|
||||
// Creates a new config with default parameters.
|
||||
func New() Config {
|
||||
return Config{
|
||||
Version: "",
|
||||
SmdClient: client.SmdClient{Host: "http://127.0.0.1:27779"},
|
||||
Targets: map[string]configurator.Target{},
|
||||
PluginDirs: []string{},
|
||||
Server: Server{
|
||||
Host: "127.0.0.1:3334",
|
||||
Jwks: Jwks{
|
||||
Uri: "",
|
||||
Retries: 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Load(path string) Config {
|
||||
var c Config = New()
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to read config file")
|
||||
return c
|
||||
}
|
||||
err = yaml.Unmarshal(file, &c)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to unmarshal config")
|
||||
return c
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (config *Config) Save(path string) {
|
||||
path = filepath.Clean(path)
|
||||
if path == "" || path == "." {
|
||||
path = "config.yaml"
|
||||
}
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal config")
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(path, data, 0o644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to write default config file")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func SaveDefault(path string) {
|
||||
path = filepath.Clean(path)
|
||||
if path == "" || path == "." {
|
||||
path = "config.yaml"
|
||||
}
|
||||
var c = New()
|
||||
data, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal config")
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(path, data, 0o644)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to write default config file")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package configurator
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Target struct {
|
||||
Plugin string `yaml:"plugin,omitempty"` // Set the plugin or it's path
|
||||
TemplatePaths []string `yaml:"templates,omitempty"` // Set the template paths
|
||||
FilePaths []string `yaml:"files,omitempty"` // Set the file paths
|
||||
RunTargets []string `yaml:"targets,omitempty"` // Set additional targets to run
|
||||
}
|
||||
|
||||
type IPAddr struct {
|
||||
IpAddress string `json:"IPAddress"`
|
||||
Network string `json:"Network"`
|
||||
}
|
||||
|
||||
type EthernetInterface struct {
|
||||
Id string
|
||||
Description string
|
||||
MacAddress string
|
||||
LastUpdate string
|
||||
ComponentId string
|
||||
Type string
|
||||
IpAddresses []IPAddr
|
||||
}
|
||||
|
||||
type Component struct {
|
||||
ID string `json:"ID"`
|
||||
Type string `json:"Type"`
|
||||
State string `json:"State,omitempty"`
|
||||
Flag string `json:"Flag,omitempty"`
|
||||
Enabled *bool `json:"Enabled,omitempty"`
|
||||
SwStatus string `json:"SoftwareStatus,omitempty"`
|
||||
Role string `json:"Role,omitempty"`
|
||||
SubRole string `json:"SubRole,omitempty"`
|
||||
NID json.Number `json:"NID,omitempty"`
|
||||
Subtype string `json:"Subtype,omitempty"`
|
||||
NetType string `json:"NetType,omitempty"`
|
||||
Arch string `json:"Arch,omitempty"`
|
||||
Class string `json:"Class,omitempty"`
|
||||
ReservationDisabled bool `json:"ReservationDisabled,omitempty"`
|
||||
Locked bool `json:"Locked,omitempty"`
|
||||
}
|
||||
|
||||
type RedfishEndpoint struct {
|
||||
ID string `json:"ID"`
|
||||
Type string `json:"Type"`
|
||||
Name string `json:"Name,omitempty"` // user supplied descriptive name
|
||||
Hostname string `json:"Hostname"`
|
||||
Domain string `json:"Domain"`
|
||||
FQDN string `json:"FQDN"`
|
||||
Enabled bool `json:"Enabled"`
|
||||
UUID string `json:"UUID,omitempty"`
|
||||
User string `json:"User"`
|
||||
Password string `json:"Password"` // Temporary until more secure method
|
||||
UseSSDP bool `json:"UseSSDP,omitempty"`
|
||||
MACRequired bool `json:"MACRequired,omitempty"`
|
||||
MACAddr string `json:"MACAddr,omitempty"`
|
||||
IPAddr string `json:"IPAddress,omitempty"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
}
|
||||
|
||||
type BMC struct {
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ package generator
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/pkg"
|
||||
"github.com/OpenCHAMI/configurator/pkg/client"
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
configurator "git.towk2.me/towk/configurator/pkg"
|
||||
"git.towk2.me/towk/configurator/pkg/client"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
)
|
||||
|
||||
type Conman struct{}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package generator
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
)
|
||||
|
||||
type CoreDhcp struct{}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ package generator
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/pkg"
|
||||
"github.com/OpenCHAMI/configurator/pkg/client"
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
configurator "git.towk2.me/towk/configurator/pkg"
|
||||
"git.towk2.me/towk/configurator/pkg/client"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
)
|
||||
|
||||
type DHCPd struct{}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ package generator
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/pkg"
|
||||
"github.com/OpenCHAMI/configurator/pkg/client"
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
configurator "git.towk2.me/towk/configurator/pkg"
|
||||
"git.towk2.me/towk/configurator/pkg/client"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
)
|
||||
|
||||
type DNSMasq struct{}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package generator
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
)
|
||||
|
||||
type Example struct {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import (
|
|||
"path/filepath"
|
||||
"plugin"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/pkg"
|
||||
"github.com/OpenCHAMI/configurator/pkg/client"
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
configurator "git.towk2.me/towk/configurator/pkg"
|
||||
"git.towk2.me/towk/configurator/pkg/client"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package generator
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
)
|
||||
|
||||
type Hostfile struct{}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package generator
|
||||
|
||||
import (
|
||||
configurator "github.com/OpenCHAMI/configurator/pkg"
|
||||
"github.com/OpenCHAMI/configurator/pkg/client"
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
configurator "git.towk2.me/towk/configurator/pkg"
|
||||
"git.towk2.me/towk/configurator/pkg/client"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
)
|
||||
|
||||
type (
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package generator
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
)
|
||||
|
||||
type Powerman struct{}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package generator
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
)
|
||||
|
||||
type Syslog struct{}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
"github.com/nikolalohinski/gonja/v2"
|
||||
"github.com/nikolalohinski/gonja/v2/exec"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import (
|
|||
"maps"
|
||||
"strings"
|
||||
|
||||
"github.com/OpenCHAMI/configurator/pkg/client"
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
"git.towk2.me/towk/configurator/pkg/client"
|
||||
"git.towk2.me/towk/configurator/pkg/config"
|
||||
"git.towk2.me/towk/configurator/pkg/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
|
|
|||
14
pkg/plugin.go
Normal file
14
pkg/plugin.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package configurator
|
||||
|
||||
type Plugin interface {
|
||||
// plugin data
|
||||
Name() string
|
||||
Version() string
|
||||
Description() string
|
||||
Metadata() map[string]string
|
||||
|
||||
// run the plugin
|
||||
Init() error
|
||||
Run() error
|
||||
Cleanup() error
|
||||
}
|
||||
|
|
@ -1,327 +0,0 @@
|
|||
//go:build server || all
|
||||
// +build server all
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/pkg"
|
||||
"github.com/OpenCHAMI/configurator/pkg/client"
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/generator"
|
||||
"github.com/OpenCHAMI/jwtauth/v5"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
openchami_authenticator "github.com/openchami/chi-middleware/auth"
|
||||
openchami_logger "github.com/openchami/chi-middleware/log"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenAuth *jwtauth.JWTAuth = nil
|
||||
)
|
||||
|
||||
type Jwks struct {
|
||||
Uri string
|
||||
Retries int
|
||||
}
|
||||
type Server struct {
|
||||
*http.Server
|
||||
Config *config.Config
|
||||
Jwks Jwks `yaml:"jwks"`
|
||||
GeneratorParams generator.Params
|
||||
TokenAuth *jwtauth.JWTAuth
|
||||
Targets map[string]Target
|
||||
}
|
||||
|
||||
type Target struct {
|
||||
Name string `json:"name"`
|
||||
PluginPath string `json:"plugin"`
|
||||
Templates []generator.Template `json:"templates"`
|
||||
}
|
||||
|
||||
// Constructor to make a new server instance with an optional config.
|
||||
func New(conf *config.Config) *Server {
|
||||
// create default config if none supplied
|
||||
if conf == nil {
|
||||
c := config.New()
|
||||
conf = &c
|
||||
}
|
||||
newServer := &Server{
|
||||
Config: conf,
|
||||
Server: &http.Server{Addr: conf.Server.Host},
|
||||
Jwks: Jwks{
|
||||
Uri: conf.Server.Jwks.Uri,
|
||||
Retries: conf.Server.Jwks.Retries,
|
||||
},
|
||||
}
|
||||
// load templates for server from config
|
||||
newServer.loadTargets()
|
||||
log.Debug().Any("targets", newServer.Targets).Msg("new server targets")
|
||||
return newServer
|
||||
}
|
||||
|
||||
// Main function to start up configurator as a service.
|
||||
func (s *Server) Serve() error {
|
||||
// Setup logger
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
logger := log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
// set the server address with config values
|
||||
s.Server.Addr = s.Config.Server.Host
|
||||
|
||||
// fetch JWKS public key from authorization server
|
||||
if s.Config.Server.Jwks.Uri != "" && tokenAuth == nil {
|
||||
for i := 0; i < s.Config.Server.Jwks.Retries; i++ {
|
||||
var err error
|
||||
tokenAuth, err = configurator.FetchPublicKeyFromURL(s.Config.Server.Jwks.Uri)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed to fetch JWKS")
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// create client with opts to use to fetch data from SMD
|
||||
opts := []client.Option{
|
||||
client.WithHost(s.Config.SmdClient.Host),
|
||||
client.WithAccessToken(s.Config.AccessToken),
|
||||
client.WithCertPoolFile(s.Config.CertPath),
|
||||
}
|
||||
|
||||
// create new go-chi router with its routes
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.RequestID)
|
||||
router.Use(middleware.RealIP)
|
||||
router.Use(middleware.Logger)
|
||||
router.Use(middleware.Recoverer)
|
||||
router.Use(middleware.StripSlashes)
|
||||
router.Use(middleware.Timeout(60 * time.Second))
|
||||
router.Use(openchami_logger.OpenCHAMILogger(logger))
|
||||
if s.Config.Server.Jwks.Uri != "" {
|
||||
router.Group(func(r chi.Router) {
|
||||
r.Use(
|
||||
jwtauth.Verifier(tokenAuth),
|
||||
openchami_authenticator.AuthenticatorWithRequiredClaims(tokenAuth, []string{"sub", "iss", "aud"}),
|
||||
)
|
||||
|
||||
// protected routes if using auth
|
||||
r.HandleFunc("/generate", s.Generate(opts...))
|
||||
r.Post("/targets", s.createTarget)
|
||||
})
|
||||
} else {
|
||||
// public routes without auth
|
||||
router.HandleFunc("/generate", s.Generate(opts...))
|
||||
router.Post("/targets", s.createTarget)
|
||||
}
|
||||
|
||||
// always available public routes go here (none at the moment)
|
||||
router.HandleFunc("/configurator/status", s.GetStatus)
|
||||
|
||||
s.Handler = router
|
||||
return s.ListenAndServe()
|
||||
}
|
||||
|
||||
// TODO: implement a way to shut the server down
|
||||
func (s *Server) Close() {
|
||||
|
||||
}
|
||||
|
||||
// This is the corresponding service function to generate templated files, that
|
||||
// works similarly to the CLI variant. This function takes similiar arguments as
|
||||
// query parameters that are included in the HTTP request URL.
|
||||
func (s *Server) Generate(opts ...client.Option) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// get all of the expect query URL params and validate
|
||||
var (
|
||||
targetParam string = r.URL.Query().Get("target")
|
||||
target *Target = s.getTarget(targetParam)
|
||||
outputs generator.FileMap
|
||||
err error
|
||||
)
|
||||
s.GeneratorParams = parseGeneratorParams(r, target, opts...)
|
||||
if targetParam == "" {
|
||||
err = writeErrorResponse(w, "must specify a target")
|
||||
log.Error().Err(err).Msg("failed to parse generator params")
|
||||
return
|
||||
}
|
||||
|
||||
// try to generate with target supplied by client first
|
||||
if target != nil {
|
||||
log.Debug().Any("target", target).Msg("target for Generate()")
|
||||
outputs, err = generator.Generate(s.Config, target.PluginPath, s.GeneratorParams)
|
||||
log.Debug().Any("outputs map", outputs).Msgf("after generate")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to generate file")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// try and generate a new config file from supplied params
|
||||
log.Debug().Str("target", targetParam).Msg("target for GenerateWithTarget()")
|
||||
outputs, err = generator.GenerateWithTarget(s.Config, targetParam)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, "failed to generate file")
|
||||
log.Error().Err(err).Msgf("failed to generate file with target '%s'", target)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// marshal output to JSON then send response to client
|
||||
tmp := generator.ConvertContentsToString(outputs)
|
||||
b, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, "failed to marshal output: %v", err)
|
||||
log.Error().Err(err).Msg("failed to marshal output")
|
||||
return
|
||||
}
|
||||
_, err = w.Write(b)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, "failed to write response: %v", err)
|
||||
log.Error().Err(err).Msg("failed to write response")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) loadTargets() {
|
||||
// make sure the map is initialized first
|
||||
if s.Targets == nil {
|
||||
s.Targets = make(map[string]Target)
|
||||
}
|
||||
// add default generator targets
|
||||
for name, _ := range generator.DefaultGenerators {
|
||||
serverTarget := Target{
|
||||
Name: name,
|
||||
PluginPath: name,
|
||||
}
|
||||
s.Targets[name] = serverTarget
|
||||
}
|
||||
// add targets from config to server (overwrites default targets)
|
||||
for name, target := range s.Config.Targets {
|
||||
serverTarget := Target{
|
||||
Name: name,
|
||||
}
|
||||
// only overwrite plugin path if it's set
|
||||
if target.Plugin != "" {
|
||||
serverTarget.PluginPath = target.Plugin
|
||||
} else {
|
||||
serverTarget.PluginPath = name
|
||||
}
|
||||
// add templates using template paths from config
|
||||
for _, templatePath := range target.TemplatePaths {
|
||||
template := generator.Template{}
|
||||
template.LoadFromFile(templatePath)
|
||||
serverTarget.Templates = append(serverTarget.Templates, template)
|
||||
}
|
||||
s.Targets[name] = serverTarget
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) GetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
data := map[string]any{
|
||||
"code": 200,
|
||||
"message": "Configurator is healthy",
|
||||
}
|
||||
err := json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to encode JSON: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new target with name, generator, templates, and files.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// curl -X POST /target?name=test&plugin=dnsmasq
|
||||
//
|
||||
// TODO: need to implement template managing API first in "internal/generator/templates" or something
|
||||
func (s *Server) createTarget(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
target = Target{}
|
||||
bytes []byte
|
||||
err error
|
||||
)
|
||||
if r == nil {
|
||||
err = writeErrorResponse(w, "request is invalid")
|
||||
log.Error().Err(err).Msg("request == nil")
|
||||
return
|
||||
}
|
||||
|
||||
bytes, err = io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, "failed to read response body: %v", err)
|
||||
log.Error().Err(err).Msg("failed to read response body")
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
err = json.Unmarshal(bytes, &target)
|
||||
if err != nil {
|
||||
writeErrorResponse(w, "failed to unmarshal target: %v", err)
|
||||
log.Error().Err(err).Msg("failed to unmarshal target")
|
||||
return
|
||||
}
|
||||
|
||||
// make sure a plugin and at least one template is supplied
|
||||
if target.Name == "" {
|
||||
err = writeErrorResponse(w, "target name is required")
|
||||
log.Error().Err(err).Msg("set target as a URL query parameter")
|
||||
return
|
||||
}
|
||||
if target.PluginPath == "" {
|
||||
err = writeErrorResponse(w, "generator name is required")
|
||||
log.Error().Err(err).Msg("must supply a generator name")
|
||||
return
|
||||
}
|
||||
if len(target.Templates) <= 0 {
|
||||
writeErrorResponse(w, "requires at least one template")
|
||||
log.Error().Err(err).Msg("must supply at least one template")
|
||||
return
|
||||
}
|
||||
|
||||
s.Targets[target.Name] = target
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) getTarget(name string) *Target {
|
||||
t, ok := s.Targets[name]
|
||||
if ok {
|
||||
return &t
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wrapper function to simplify writting error message responses. This function
|
||||
// is only intended to be used with the service and nothing else.
|
||||
func writeErrorResponse(w http.ResponseWriter, format string, a ...any) error {
|
||||
errmsg := fmt.Sprintf(format, a...)
|
||||
bytes, _ := json.Marshal(map[string]any{
|
||||
"level": "error",
|
||||
"time": time.Now().Unix(),
|
||||
"message": errmsg,
|
||||
})
|
||||
http.Error(w, string(bytes), http.StatusInternalServerError)
|
||||
return fmt.Errorf(errmsg)
|
||||
}
|
||||
|
||||
func parseGeneratorParams(r *http.Request, target *Target, opts ...client.Option) generator.Params {
|
||||
var params = generator.Params{
|
||||
ClientOpts: opts,
|
||||
Templates: make(map[string]generator.Template, len(target.Templates)),
|
||||
}
|
||||
for i, template := range target.Templates {
|
||||
params.Templates[fmt.Sprintf("%s_%d", target.Name, i)] = template
|
||||
}
|
||||
return params
|
||||
}
|
||||
184
pkg/service/profile.go
Normal file
184
pkg/service/profile.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
ID string `json:"id"` // profile ID
|
||||
Description string `json:"description"` // profile description
|
||||
Tags []string `json:"tags"` // tags used for ...
|
||||
Paths []string `json:"paths"` // paths to download
|
||||
Plugins []string `json:"plugins"` // plugins to run
|
||||
Data map[string]any `json:"data"` // include render data
|
||||
}
|
||||
|
||||
func (s *Service) GetProfiles() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
path = s.RootPath + PROFILES_RELPATH
|
||||
profiles []*Profile
|
||||
contents []byte
|
||||
err error
|
||||
)
|
||||
|
||||
// walk profiles directory to load all profiles
|
||||
err = filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// skip directories
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// read file contents
|
||||
var profile *Profile
|
||||
profile, err = LoadProfile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
profiles = append(profiles, profile)
|
||||
|
||||
fmt.Println(path, info.Size())
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// marshal and send all the profiles
|
||||
contents, err = json.Marshal(profiles)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to marshal profiles: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
_, err = w.Write(contents)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// func (s *Service) CreateProfiles() http.HandlerFunc {
|
||||
// return func(w http.ResponseWriter, r *http.Request) {
|
||||
// var (
|
||||
// path = chi.URLParam(r, "path")
|
||||
// err error
|
||||
// )
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
func (s *Service) GetProfile() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
id = chi.URLParam(r, "id")
|
||||
path = s.BuildProfilePath(id)
|
||||
profile *Profile
|
||||
contents []byte
|
||||
err error
|
||||
)
|
||||
|
||||
profile, err = LoadProfile(path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
contents, err = json.Marshal(profile)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = w.Write(contents)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) CreateProfile() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
type input struct {
|
||||
path string `json:"path"`
|
||||
profile *Profile `json:"profile"`
|
||||
}
|
||||
var (
|
||||
body []byte
|
||||
in input
|
||||
err error
|
||||
)
|
||||
|
||||
body, err = io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// use the request info to build profile
|
||||
err = json.Unmarshal(body, &in)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// create a new profile on disk
|
||||
os.WriteFile()
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) CreateProfileVar() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
|
||||
func (s *Service) DeleteProfileVar() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
|
||||
func (s *Service) GetProfileData() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
|
||||
func (s *Service) GetProfileVar() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
|
||||
func (s *Service) CreateProfilePath() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
|
||||
func (s *Service) DeleteProfilePath() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
|
||||
func (s *Service) GetProfilePath() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
|
||||
func (s *Service) GetPlugins() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
|
||||
func (s *Service) CreatePlugins() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
|
||||
func (s *Service) DeletePlugins() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {}
|
||||
}
|
||||
38
pkg/service/routes.go
Normal file
38
pkg/service/routes.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (s *Service) Download() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Upload() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) List() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
data := map[string]any{
|
||||
"code": 200,
|
||||
"message": "Configurator is healthy",
|
||||
}
|
||||
err := json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to encode JSON: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
122
pkg/service/service.go
Normal file
122
pkg/service/service.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
configurator "git.towk2.me/towk/configurator/pkg"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGINS_RELPATH = "/plugins"
|
||||
TEMPLATES_RELPATH = "/templates"
|
||||
PROFILES_RELPATH = "/profiles"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
RootPath string `yaml:"root,omitempty"`
|
||||
Environment map[string]string
|
||||
|
||||
// max counts
|
||||
PluginsMaxCount int
|
||||
ProfilesMaxCount int
|
||||
}
|
||||
|
||||
// New creates the directories at specified path
|
||||
func New() *Service {
|
||||
return &Service{
|
||||
RootPath: ".",
|
||||
Environment: map[string]string{
|
||||
"CONFIGURATOR_HOST_URI": "",
|
||||
"ACCESS_TOKEN": "",
|
||||
},
|
||||
PluginsMaxCount: 64,
|
||||
ProfilesMaxCount: 256,
|
||||
}
|
||||
}
|
||||
|
||||
// Serve() starts the configurator service and waits for requests.
|
||||
func (s *Service) Serve() error {
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.RequestID)
|
||||
router.Use(middleware.RealIP)
|
||||
router.Use(middleware.Logger)
|
||||
router.Use(middleware.Recoverer)
|
||||
router.Use(middleware.StripSlashes)
|
||||
router.Use(middleware.Timeout(60 * time.Second))
|
||||
|
||||
if s.requireAuth() {
|
||||
|
||||
} else {
|
||||
// general
|
||||
router.Get("/download", s.Download())
|
||||
router.Post("/upload", s.Upload())
|
||||
router.Get("/list", s.List())
|
||||
|
||||
// profiles
|
||||
router.Get("/profiles", s.GetProfiles())
|
||||
// router.Post("/profiles", s.CreateProfiles())
|
||||
router.Get("/profile/{id}", s.GetProfile())
|
||||
router.Post("/profile/{id}", s.CreateProfile())
|
||||
router.Post("/profile/{id}/data/{varname}", s.CreateProfileVar())
|
||||
router.Delete("/profile/{id}/data/{varname}", s.DeleteProfileVar())
|
||||
router.Get("/profile/{id}/data", s.GetProfileData())
|
||||
router.Get("/profile/{id}/data/{varname}", s.GetProfileVar())
|
||||
router.Post("/profile/{id}/paths/{path}", s.CreateProfilePath())
|
||||
router.Delete("/profile/{id}/paths/{path}", s.DeleteProfilePath())
|
||||
router.Get("/profile/{id}/paths/{path}", s.GetProfilePath())
|
||||
|
||||
// plugins
|
||||
router.Get("/plugins", s.GetPlugins())
|
||||
router.Post("/plugins", s.CreatePlugins())
|
||||
router.Delete("/plugins/{id}", s.DeletePlugins())
|
||||
}
|
||||
|
||||
// always available public routes go here
|
||||
router.HandleFunc("/status", s.GetStatus)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) requireAuth() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) FetchJwks(uri string) {
|
||||
|
||||
}
|
||||
|
||||
func LoadProfile(path string) (*Profile, error) {
|
||||
return LoadFromJSONFile[Profile](path)
|
||||
}
|
||||
|
||||
func LoadPlugin(path string) (*configurator.Plugin, error) {
|
||||
return LoadFromJSONFile[configurator.Plugin](path)
|
||||
}
|
||||
|
||||
func LoadFromJSONFile[T any](path string) (*T, error) {
|
||||
var (
|
||||
res *T
|
||||
contents []byte
|
||||
err error
|
||||
)
|
||||
|
||||
contents, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read plugin file: %v", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(contents, &res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal plugin: %v", err)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *Service) BuildProfilePath(id string) string {
|
||||
return s.RootPath + PLUGINS_RELPATH + "/" + id
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ license=('MIT')
|
|||
groups=("openchami")
|
||||
provides=('configurator')
|
||||
conflicts=('configurator')
|
||||
https://github.com/OpenCHAMI/configurator/releases/download/v0.1.0-alpha/configurator
|
||||
https://git.towk2.me/towk/configurator/releases/download/v0.1.0-alpha/configurator
|
||||
source_x86_64=(
|
||||
"${url}/releases/download/v0.1.0-alpha/${pkgname}.tar.gz"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
##
|
||||
## Run these tests after starting server with `configurator serve...`
|
||||
##
|
||||
|
||||
# Generate a `example` config with default plugin and template
|
||||
GET http://127.0.0.1:3334/generate?target=example
|
||||
HTTP 200
|
||||
|
||||
# Create a new target using the API
|
||||
POST http://127.0.0.1:3334/targets
|
||||
{
|
||||
"name": "test",
|
||||
"plugin": "example",
|
||||
"templates": [{
|
||||
"contents": "This is an example template used with the example plugin."
|
||||
}]
|
||||
}
|
||||
HTTP 200
|
||||
|
||||
# Test the new target just add from POST above
|
||||
GET http://127.0.0.1:3334/generate?target=example
|
||||
HTTP 200
|
||||
|
|
@ -1,418 +0,0 @@
|
|||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
configurator "github.com/OpenCHAMI/configurator/pkg"
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/generator"
|
||||
"github.com/OpenCHAMI/configurator/pkg/server"
|
||||
"github.com/OpenCHAMI/configurator/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
workDir string
|
||||
replaceDir string
|
||||
err error
|
||||
)
|
||||
|
||||
// A valid test generator that implements the `Generator` interface.
|
||||
type TestGenerator struct{}
|
||||
|
||||
func (g *TestGenerator) GetName() string { return "test" }
|
||||
func (g *TestGenerator) GetVersion() string { return "v1.0.0" }
|
||||
func (g *TestGenerator) GetDescription() string {
|
||||
return "This is a plugin created for running tests."
|
||||
}
|
||||
func (g *TestGenerator) Generate(config *config.Config, params generator.Params) (generator.FileMap, error) {
|
||||
// Jinja 2 template file
|
||||
files := map[string]generator.Template{
|
||||
"test1": generator.Template{
|
||||
Contents: []byte(`
|
||||
Name: {{plugin_name}}
|
||||
Version: {{plugin_version}}
|
||||
Description: {{plugin_description}}
|
||||
|
||||
This is the first test template file.
|
||||
`),
|
||||
},
|
||||
"test2": generator.Template{
|
||||
Contents: []byte(`
|
||||
This is another testing Jinja 2 template file using {{plugin_name}}.
|
||||
`),
|
||||
},
|
||||
}
|
||||
|
||||
// apply Jinja templates to file
|
||||
fileMap, err := generator.ApplyTemplates(generator.Mappings{
|
||||
"plugin_name": g.GetName(),
|
||||
"plugin_version": g.GetVersion(),
|
||||
"plugin_description": g.GetDescription(),
|
||||
}, files)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to apply templates: %v", err)
|
||||
}
|
||||
|
||||
// make sure we have a valid config we can access
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("invalid config (config is nil)")
|
||||
}
|
||||
|
||||
// TODO: make sure we can get a target
|
||||
|
||||
// make sure we have the same number of files in file list
|
||||
var (
|
||||
fileInputCount = len(files)
|
||||
fileOutputCount = len(fileMap)
|
||||
)
|
||||
if fileInputCount != fileOutputCount {
|
||||
return nil, fmt.Errorf("file output count (%d) is not the same as the input (%d)", fileOutputCount, fileInputCount)
|
||||
}
|
||||
|
||||
return fileMap, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
workDir, err = os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
replaceDir = fmt.Sprintf("%s", filepath.Dir(workDir))
|
||||
}
|
||||
|
||||
// Test building and loading plugins
|
||||
func TestPlugin(t *testing.T) {
|
||||
var (
|
||||
testPluginDir = t.TempDir()
|
||||
testPluginPath = fmt.Sprintf("%s/test-plugin.so", testPluginDir)
|
||||
testPluginSourcePath = fmt.Sprintf("%s/test-plugin.go", testPluginDir)
|
||||
testPluginSource = []byte(
|
||||
`package main
|
||||
|
||||
import (
|
||||
"github.com/OpenCHAMI/configurator/pkg/config"
|
||||
"github.com/OpenCHAMI/configurator/pkg/generator"
|
||||
)
|
||||
|
||||
type TestGenerator struct{}
|
||||
|
||||
func (g *TestGenerator) GetName() string { return "test" }
|
||||
func (g *TestGenerator) GetVersion() string { return "v1.0.0" }
|
||||
func (g *TestGenerator) GetDescription() string {
|
||||
return "This is a plugin creating for running tests."
|
||||
}
|
||||
func (g *TestGenerator) Generate(config *config.Config, params generator.Params) (generator.FileMap, error) {
|
||||
return generator.FileMap{"test": []byte("test")}, nil
|
||||
}
|
||||
|
||||
var Generator TestGenerator`)
|
||||
)
|
||||
|
||||
// get directory to replace remote pkg with local
|
||||
// _, filename, _, _ := runtime.Caller(0)
|
||||
// replaceDir := fmt.Sprintf("%s", filepath.Dir(workDir))
|
||||
|
||||
// show all paths to make sure we're using the correct ones
|
||||
fmt.Printf("(TestPlugin) working directory: %v\n", workDir)
|
||||
fmt.Printf("(TestPlugin) plugin directory: %v\n", testPluginDir)
|
||||
fmt.Printf("(TestPlugin) plugin path: %v\n", testPluginPath)
|
||||
fmt.Printf("(TestPlugin) plugin source path: %v\n", testPluginSourcePath)
|
||||
|
||||
// make temporary directory to test plugin
|
||||
err = os.MkdirAll(testPluginDir, 0o777)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make temporary directory: %v", err)
|
||||
}
|
||||
|
||||
// dump the plugin source code to a file
|
||||
err = os.WriteFile(testPluginSourcePath, testPluginSource, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test plugin file: %v", err)
|
||||
}
|
||||
|
||||
// make sure the source file was actually written
|
||||
fileInfo, err := os.Stat(testPluginSourcePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat path: %v", err)
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
t.Fatalf("expected file but found directory")
|
||||
}
|
||||
|
||||
// change to testing directory to run command
|
||||
err = os.Chdir(testPluginDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to 'cd' to temporary directory: %v", err)
|
||||
}
|
||||
|
||||
// initialize the plugin directory as a Go project
|
||||
cmd := exec.Command("bash", "-c", "go mod init github.com/OpenCHAMI/configurator-test-plugin")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// use the local `pkg` instead of the release one
|
||||
cmd = exec.Command("bash", "-c", fmt.Sprintf("go mod edit -replace=github.com/OpenCHAMI/configurator=%s", replaceDir))
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// run `go mod tidy` for dependencies
|
||||
cmd = exec.Command("bash", "-c", "go mod tidy")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// execute command to build the plugin
|
||||
cmd = exec.Command("bash", "-c", "go build -buildmode=plugin -o=test-plugin.so test-plugin.go")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// stat the file to confirm that the plugin was built
|
||||
fileInfo, err = os.Stat(testPluginPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat plugin file: %v", err)
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
t.Fatalf("directory file but a file was expected")
|
||||
}
|
||||
if fileInfo.Size() <= 0 {
|
||||
t.Fatal("found an empty file or file with size of 0 bytes")
|
||||
}
|
||||
|
||||
// test loading plugins both individually and in a dir
|
||||
gen, err := generator.LoadPlugin("test-plugin.so")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load the test plugin: %v", err)
|
||||
}
|
||||
|
||||
// test that we have all expected methods with type assertions
|
||||
if _, ok := gen.(interface {
|
||||
GetName() string
|
||||
GetVersion() string
|
||||
GetDescription() string
|
||||
Generate(*config.Config, generator.Params) (generator.FileMap, error)
|
||||
}); !ok {
|
||||
t.Error("plugin does not implement all of the generator interface")
|
||||
}
|
||||
|
||||
// test loading plugins from a directory (should just load a single one)
|
||||
gens, err := generator.LoadPlugins(testPluginDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load plugins in '%s': %v", testPluginDir, err)
|
||||
}
|
||||
|
||||
// test all of the plugins loaded from a directory (should expect same result as above)
|
||||
for _, gen := range gens {
|
||||
if _, ok := gen.(interface {
|
||||
GetName() string
|
||||
GetVersion() string
|
||||
GetDescription() string
|
||||
Generate(*config.Config, generator.Params) (generator.FileMap, error)
|
||||
}); !ok {
|
||||
t.Error("plugin does not implement all of the generator interface")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test that expects to fail with a specific error using a partially
|
||||
// implemented generator. The purpose of this test is to make sure we're
|
||||
// seeing the correct error that we would expect in these situations.
|
||||
// The errors should be something like:
|
||||
// - no symbol: "failed to look up symbol at path"
|
||||
// - invalid symbol: "failed to load the correct symbol type at path"
|
||||
func TestPluginWithInvalidOrNoSymbol(t *testing.T) {
|
||||
var (
|
||||
testPluginDir = t.TempDir()
|
||||
testPluginPath = fmt.Sprintf("%s/invalid-plugin.so", testPluginDir)
|
||||
testPluginSourcePath = fmt.Sprintf("%s/invalid-plugin.go", testPluginDir)
|
||||
testPluginSource = []byte(`
|
||||
package main
|
||||
|
||||
// An invalid generator that does not or partially implements
|
||||
// the "Generator" interface.
|
||||
type InvalidGenerator struct{}
|
||||
func (g *InvalidGenerator) GetName() string { return "invalid" }
|
||||
var Generator InvalidGenerator
|
||||
`)
|
||||
)
|
||||
|
||||
// show all paths to make sure we're using the correct ones
|
||||
fmt.Printf("(TestPluginWithInvalidOrNoSymbol) working directory: %v\n", workDir)
|
||||
fmt.Printf("(TestPluginWithInvalidOrNoSymbol) plugin directory: %v\n", testPluginDir)
|
||||
fmt.Printf("(TestPluginWithInvalidOrNoSymbol) plugin path: %v\n", testPluginPath)
|
||||
fmt.Printf("(TestPluginWithInvalidOrNoSymbol) plugin source path: %v\n", testPluginSourcePath)
|
||||
|
||||
// get directory to replace remote pkg with local
|
||||
// _, filename, _, _ := runtime.Caller(0)
|
||||
|
||||
// make temporary directory to test plugin
|
||||
err = os.MkdirAll(testPluginDir, os.ModeDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make temporary directory: %v", err)
|
||||
}
|
||||
|
||||
// dump the plugin source code to a file
|
||||
err = os.WriteFile(testPluginSourcePath, testPluginSource, os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write test plugin file: %v", err)
|
||||
}
|
||||
|
||||
// make sure the source file was actually written
|
||||
fileInfo, err := os.Stat(testPluginSourcePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat path: %v", err)
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
t.Fatalf("expected file but found directory")
|
||||
}
|
||||
|
||||
// change to testing directory to run command
|
||||
err = os.Chdir(testPluginDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to 'cd' to temporary directory: %v", err)
|
||||
}
|
||||
|
||||
// initialize the plugin directory as a Go project
|
||||
cmd := exec.Command("bash", "-c", "go mod init github.com/OpenCHAMI/configurator-test-plugin")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// use the local `pkg` instead of the release one
|
||||
cmd = exec.Command("bash", "-c", fmt.Sprintf("go mod edit -replace=github.com/OpenCHAMI/configurator=%s", replaceDir))
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// run `go mod tidy` for dependencies
|
||||
cmd = exec.Command("bash", "-c", "go mod tidy")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// execute command to build the plugin
|
||||
cmd = exec.Command("bash", "-c", "go build -buildmode=plugin -o=invalid-plugin.so invalid-plugin.go")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to execute command: %v\n%s", err, string(output))
|
||||
}
|
||||
|
||||
// stat the file to confirm that the plugin was built
|
||||
fileInfo, err = os.Stat(testPluginPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat plugin file: %v", err)
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
t.Fatalf("directory file but a file was expected")
|
||||
}
|
||||
if fileInfo.Size() <= 0 {
|
||||
t.Fatal("found an empty file or file with size of 0 bytes")
|
||||
}
|
||||
|
||||
// try and load plugin, but expect specific error
|
||||
_, err = generator.LoadPlugin(testPluginSourcePath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error, but returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that expects to successfully "generate" a file using the built-in
|
||||
// example plugin with no fetching.
|
||||
//
|
||||
// NOTE: Normally we would dynamically load a generator from a plugin, but
|
||||
// we're not doing it here since that's not what is being tested.
|
||||
func TestGenerateExample(t *testing.T) {
|
||||
var (
|
||||
conf = config.New()
|
||||
gen = TestGenerator{}
|
||||
)
|
||||
|
||||
// make sure our generator returns expected strings
|
||||
t.Run("properties", func(t *testing.T) {
|
||||
if gen.GetName() != "test" {
|
||||
t.Error("test generator return unexpected name")
|
||||
}
|
||||
if gen.GetVersion() != "v1.0.0" {
|
||||
t.Error("test generator return unexpected version")
|
||||
}
|
||||
if gen.GetDescription() != "This is a plugin created for running tests." {
|
||||
t.Error("test generator return unexpected description")
|
||||
}
|
||||
})
|
||||
|
||||
// try to generate a file with templating applied
|
||||
fileMap, err := gen.Generate(&conf, generator.Params{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate file: %v", err)
|
||||
}
|
||||
|
||||
// test for 2 expected files to be generated in the output (hint: check the
|
||||
// TestGenerator.Generate implementation)
|
||||
if len(fileMap) != 2 {
|
||||
t.Error("expected 2 files in generated output")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that expects to successfully "generate" a file using the built-in
|
||||
// example plugin but by making a HTTP request to a service instance instead.
|
||||
//
|
||||
// NOTE: This test uses the default server settings to run. Also, no need to
|
||||
// try and load the plugin from a lib here either.
|
||||
func TestGenerateExampleWithServer(t *testing.T) {
|
||||
var (
|
||||
conf = config.New()
|
||||
gen = TestGenerator{}
|
||||
headers = make(map[string]string, 0)
|
||||
)
|
||||
|
||||
// NOTE: Currently, the server needs a config to know where to get load plugins,
|
||||
// and how to handle targets/templates. This will be simplified in the future to
|
||||
// decoupled the server from required a config altogether.
|
||||
conf.Targets["test"] = configurator.Target{
|
||||
TemplatePaths: []string{},
|
||||
FilePaths: []string{},
|
||||
}
|
||||
|
||||
// create new server, add test generator, and start in background
|
||||
server := server.New(&conf)
|
||||
generator.DefaultGenerators["test"] = &gen
|
||||
go server.Serve()
|
||||
|
||||
// make request to server to generate a file
|
||||
res, b, err := util.MakeRequest("http://127.0.0.1:3334/generate?target=test", http.MethodGet, nil, headers)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make request: %v", err)
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expect status code 200 from response but received %d instead", res.StatusCode)
|
||||
}
|
||||
|
||||
// test for specific output from request
|
||||
//
|
||||
// NOTE: we don't actually use the config in this plugin implementation,
|
||||
// but we do check that a valid config was passed.
|
||||
fileMap, err := gen.Generate(&conf, generator.Params{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate file: %v", err)
|
||||
}
|
||||
for path, contents := range fileMap {
|
||||
tmp := make(map[string]string, 1)
|
||||
err := json.Unmarshal(b, &tmp)
|
||||
if err != nil {
|
||||
t.Errorf("failed to unmarshal response: %v", err)
|
||||
continue
|
||||
}
|
||||
if string(contents) != string(tmp[path]) {
|
||||
t.Fatalf("response does not match expected output...\nexpected:%s\noutput:%s", string(contents), string(tmp[path]))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue