refactor: initial commit for major rewrite
This commit is contained in:
parent
3253cb8bbb
commit
bfd83f35a3
45 changed files with 439 additions and 1733 deletions
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue