1
0
Fork 0

Infisical API, config update

main v0.19.0
Jordan Hotmann 2023-12-20 11:29:23 -07:00
parent e86694c57b
commit 7e9647b1ae
No known key found for this signature in database
GPG Key ID: 01B504170C2A2EA3
12 changed files with 339 additions and 68 deletions

View File

@ -12,6 +12,8 @@
- [qBittorrent](https://github.com/qbittorrent/qBittorrent)
- [Syncthing](https://github.com/syncthing/syncthing)
- [National Weather Service](https://www.weather.gov/)
- [Amcrest Cameras](https://amcrest.com/)
- [Infisical](https://infisical.com/)
## NATS Topics

View File

@ -34,15 +34,15 @@ const (
HA_STATE_PREFIX = "homeassistant.states"
)
func Listen(parentLogger *slog.Logger, readme []byte) {
func Listen(parentLogger *slog.Logger, parentConfig *config.HatsConfig, readme []byte) {
logger = parentLogger
cfg = parentConfig
// render readme to HTML
p := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock)
doc := p.Parse(readme)
homepage = string(markdown.Render(doc, html.NewRenderer(html.RendererOptions{Flags: html.CommonFlags | html.HrefTargetBlank})))
cfg = config.FromEnvironment()
haClient = homeassistant.NewRestClient(cfg.GetHomeAssistantBaseUrl(), cfg.HomeAssistantToken)
router := chi.NewRouter()

View File

@ -41,9 +41,9 @@ func CloseSubscription() error {
return nil
}
func Subscribe(parentLogger *slog.Logger) error {
func Subscribe(parentLogger *slog.Logger, parentConfig *config.HatsConfig) error {
logger = parentLogger
cfg = config.FromEnvironment()
cfg = parentConfig
var err error
url := cfg.GetHomeAssistantWebsocketUrl()
@ -72,7 +72,7 @@ func reconnect() {
time.Sleep(time.Duration(attempts) * 5 * time.Second)
logger.Info("Trying to reconnect to Home Assistant", "attempt", attempts)
err := Subscribe(logger)
err := Subscribe(logger, cfg)
if err == nil {
break
}

View File

@ -25,10 +25,10 @@ func Close() {
client.Close()
}
func JetstreamConnect(parentContext context.Context, parentLogger *slog.Logger) error {
func JetstreamConnect(parentContext context.Context, parentLogger *slog.Logger, parentConfig *config.HatsConfig) error {
ctx = parentContext
logger = parentLogger
cfg = config.FromEnvironment()
cfg = parentConfig
var err error
client = n.DefaultNatsConnection().WithHostName(cfg.NatsHost).WithPort(cfg.NatsPort).WithConnectionOption(nats.Name(cfg.NatsClientName))

View File

@ -5,7 +5,6 @@ import (
"strings"
"time"
"code.jhot.me/jhot/hats/pkg/config"
"code.jhot.me/jhot/hats/pkg/homeassistant"
)
@ -15,7 +14,6 @@ var (
func initHomeAssistantClient() {
if haClient == nil {
cfg = config.FromEnvironment()
haClient = homeassistant.NewRestClient(cfg.GetHomeAssistantBaseUrl(), cfg.HomeAssistantToken)
}
}

14
main.go
View File

@ -26,15 +26,21 @@ var (
)
func main() {
cfg = config.FromEnvironment()
var err error
cfg, err = config.New()
ctx, cancel = context.WithCancel(context.Background())
logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: cfg.GetLogLevel(),
}))
if err != nil {
logger.Error("Error during config initialization", "error", err)
}
interrupt = make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
err := nats.JetstreamConnect(ctx, logger)
err = nats.JetstreamConnect(ctx, logger, cfg)
if err != nil {
panic(err)
}
@ -52,7 +58,7 @@ func main() {
nats.GetExistingSchedules()
defer nats.StopSchedules()
err = homeassistant.Subscribe(logger)
err = homeassistant.Subscribe(logger, cfg)
if err != nil {
panic(err)
}
@ -60,7 +66,7 @@ func main() {
ntfy.InitClient(cfg)
api.Listen(logger, readme)
api.Listen(logger, cfg, readme)
defer api.Close()
for sig := range interrupt {

View File

@ -3,71 +3,168 @@ package config
import (
"fmt"
"log/slog"
"os"
"reflect"
"strconv"
"strings"
"code.jhot.me/jhot/hats/internal/util"
"code.jhot.me/jhot/hats/pkg/infisical"
)
type HatsConfig struct {
LogLevl string
LogLevl string `config:"LOG_LEVEL" default:"INFO"`
HomeAssistantHost string
HomeAssistantPort string
HomeAssistantSecure bool
HomeAssistantToken string
InfisicalHost string `config:"INFISICAL_HOST" default:"http://infisical:8080"`
InfisicalClientID string `config:"INFISICAL_CLIENT" default:""`
InfisicalClientSecret string `config:"INFISICAL_SECRET" default:""`
InfisicalProjectID string `config:"INFISICAL_PROJECT" default:""`
InfisicalEnvironment string `config:"INFISICAL_ENVIRONMENT" default:"prod"`
NatsHost string
NatsPort string
NatsToken string
NatsClientName string
HomeAssistantHost string `config:"HASS_HOST" default:"127.0.0.1"`
HomeAssistantPort string `config:"HASS_PORT" default:"8123"`
HomeAssistantSecure bool `config:"HASS_SECURE" default:"false"`
HomeAssistantToken string `config:"HASS_TOKEN" default:""`
HatsHost string
HatsPort string
HatsToken string
HatsSecure bool
NatsHost string `config:"NATS_HOST" default:"127.0.0.1"`
NatsPort string `config:"NATS_PORT" default:"4222"`
NatsToken string `config:"NATS_TOKEN" default:""`
NatsClientName string `config:"NATS_CLIENT_NAME" default:"hats"`
NtfyHost string
NtfyToken string
HatsHost string `config:"HATS_HOST" default:"hats"`
HatsPort string `config:"HATS_PORT" default:"8888"`
HatsToken string `config:"HATS_TOKEN" default:""`
HatsSecure bool `config:"HATS_SECURE" default:"false"`
ConfigDir string
NtfyHost string `config:"NTFY_HOST" default:"https://ntfy.sh"`
NtfyToken string `config:"NTFY_TOKEN" default:""`
SyncthingHost string `config:"SYNCTHING_HOST" default:"http://127.0.0.1:8384"`
SyncthingToken string `config:"SYNCTHING_TOKEN" default:""`
GokapiHost string `config:"GOKAPI_HOST" default:"http://gokapi:53842"`
GokapiToken string `config:"GOKAPI_TOKEN" default:""`
QbittorrentHost string `config:"QBITTORRENT_HOST" default:"http://qbittorrent:8080"`
QbittorrentUser string `config:"QBITTORRENT_USER" default:""`
QbittorrentPassword string `config:"QBITTORRENT_PASS" default:""`
ConfigDir string `config:"CONFIG_DIR" default:"/config"`
infisicalClient *infisical.InfisicalClient
infisicalRetrievalOpts *infisical.RetrieveSecretOptions
}
func FromEnvironment() *HatsConfig {
config := &HatsConfig{
LogLevl: util.GetEnvWithDefault("LOG_LEVEL", "INFO"),
func New() (*HatsConfig, error) {
cfg := &HatsConfig{}
cfg.SetValues(ConfigValueSourceDefault).SetValues(ConfigValueSourceEnv)
HomeAssistantHost: util.GetEnvWithDefault("HASS_HOST", "127.0.0.1"),
HomeAssistantPort: util.GetEnvWithDefault("HASS_PORT", "8123"),
HomeAssistantToken: util.GetEnvWithDefault("HASS_TOKEN", ""),
if cfg.InfisicalConfigured() {
cfg.infisicalClient = infisical.New(cfg.InfisicalHost, cfg.InfisicalClientID, cfg.InfisicalClientSecret)
NatsHost: util.GetEnvWithDefault("NATS_HOST", "127.0.0.1"),
NatsPort: util.GetEnvWithDefault("NATS_PORT", "4222"),
NatsToken: util.GetEnvWithDefault("NATS_TOKEN", ""),
NatsClientName: util.GetEnvWithDefault("NATS_CLIENT_NAME", "hats"),
err := cfg.infisicalClient.Login()
if err != nil {
cfg.infisicalClient = nil
return cfg, fmt.Errorf("error logging in to Infisical: %w", err)
}
HatsHost: util.GetEnvWithDefault("HATS_HOST", "hats"),
HatsPort: util.GetEnvWithDefault("HATS_PORT", "8888"),
HatsToken: util.GetEnvWithDefault("HATS_TOKEN", ""),
cfg.infisicalRetrievalOpts = &infisical.RetrieveSecretOptions{
WorkspaceID: cfg.InfisicalProjectID,
Environment: cfg.InfisicalEnvironment,
SecretPath: "/",
IncludeImports: false,
}
NtfyHost: util.GetEnvWithDefault("NTFY_HOST", "https://ntfy.sh"),
NtfyToken: util.GetEnvWithDefault("NTFY_TOKEN", ""),
secrets, err := cfg.infisicalClient.ListSecrets(cfg.infisicalRetrievalOpts)
if err != nil {
return cfg, fmt.Errorf("error getting Infisical secrets: %w", err)
}
ConfigDir: util.GetEnvWithDefault("CONFIG_DIR", "/config"),
secretsMap := make(map[string]string)
for _, secret := range secrets {
secretsMap[secret.SecretKey] = secret.SecretValue
}
cfg.SetValues(ConfigValueSourceInfisical, secretsMap)
}
config.HomeAssistantSecure, _ = strconv.ParseBool(util.GetEnvWithDefault("HASS_SECURE", "false"))
config.HatsSecure, _ = strconv.ParseBool(util.GetEnvWithDefault("HATS_SECURE", "false"))
return cfg, nil
}
return config
type ConfigValueSource string
const ConfigValueSourceEnv ConfigValueSource = "env"
const ConfigValueSourceDefault ConfigValueSource = "default"
const ConfigValueSourceInfisical ConfigValueSource = "infisical"
func (c *HatsConfig) SetValues(source ConfigValueSource, inputs ...map[string]string) *HatsConfig {
fields := reflect.VisibleFields(reflect.TypeOf(*c))
for _, field := range fields {
envName := field.Tag.Get("config")
if envName == "" {
continue
}
var envValue string
switch source {
case ConfigValueSourceDefault:
envValue = field.Tag.Get("default")
case ConfigValueSourceEnv:
envValue = os.Getenv(envName)
case ConfigValueSourceInfisical:
if len(inputs) == 0 {
break
}
if val, found := inputs[0][envName]; found {
envValue = val
}
}
if envValue == "" {
continue
}
f := reflect.ValueOf(c).Elem().FieldByName(field.Name)
switch field.Type.Kind() {
case reflect.Bool:
f.SetBool(strings.EqualFold(envValue, "true"))
case reflect.Int:
parsed, err := strconv.ParseInt(envValue, 10, 64)
if err == nil {
f.SetInt(parsed)
}
case reflect.String:
f.SetString(envValue)
}
}
return c
}
func (c *HatsConfig) GetCustomSetting(name string, defaultValue string) string {
returnValue := defaultValue
envValue := os.Getenv(name)
if envValue != "" {
returnValue = envValue
}
if c.InfisicalConfigured() {
secret, _ := c.infisicalClient.GetSecret(name, c.infisicalRetrievalOpts)
if secret.SecretValue != "" {
returnValue = secret.SecretValue
}
}
return returnValue
}
func (c *HatsConfig) GetHomeAssistantBaseUrl() string {
hassProtocol := "http"
protocol := "http"
if c.HomeAssistantSecure {
hassProtocol += "s"
protocol += "s"
}
return fmt.Sprintf("%s://%s:%s", hassProtocol, c.HomeAssistantHost, c.HomeAssistantPort)
return fmt.Sprintf("%s://%s:%s", protocol, c.HomeAssistantHost, c.HomeAssistantPort)
}
func (c *HatsConfig) GetHomeAssistantWebsocketUrl() string {
@ -102,3 +199,11 @@ func (c *HatsConfig) GetLogLevel() slog.Level {
return slog.LevelInfo
}
}
func (c *HatsConfig) InfisicalConfigured() bool {
return c.InfisicalHost != "" &&
c.InfisicalClientID != "" &&
c.InfisicalClientSecret != "" &&
c.InfisicalProjectID != "" &&
c.InfisicalEnvironment != ""
}

View File

@ -27,10 +27,6 @@ func New(host string, token string) *GokapiClient {
}
}
func NewFromEnv() *GokapiClient {
return New(os.Getenv("GOKAPI_HOST"), os.Getenv("GOKAPI_TOKEN"))
}
type GokapiFile struct {
ID string `json:"Id"`
Name string `json:"Name"`

114
pkg/infisical/api.go Normal file
View File

@ -0,0 +1,114 @@
package infisical
import (
"fmt"
"time"
"code.jhot.me/jhot/hats/internal/util"
"github.com/go-resty/resty/v2"
)
type InfisicalClient struct {
restClient *resty.Client
host string
clientId string
clientSecret string
accessToken string
tokenExpiration time.Time
}
func New(host string, clientId string, clientSecret string) *InfisicalClient {
return &InfisicalClient{
restClient: resty.New().SetBaseURL(fmt.Sprintf("%s/api", host)),
host: host,
clientId: clientId,
clientSecret: clientSecret,
accessToken: "",
tokenExpiration: time.Now(),
}
}
func (c *InfisicalClient) Login() error {
var result LoginResponse
_, err := util.CheckSuccess(c.restClient.R().SetFormData(map[string]string{
"clientId": c.clientId,
"clientSecret": c.clientSecret,
}).SetResult(&result).Post("v1/auth/universal-auth/login"))
if err != nil {
return err
}
c.restClient.SetAuthScheme(result.TokenType).SetAuthToken(result.AccessToken)
c.accessToken = result.AccessToken
c.tokenExpiration = time.Now().Add(time.Duration(result.ExpiresIn) * time.Second)
return nil
}
func (c *InfisicalClient) CheckToken() error {
if c.accessToken == "" {
return fmt.Errorf("access token empty, please login first")
}
if time.Now().Before(c.tokenExpiration) {
return nil
}
var result LoginResponse
_, err := util.CheckSuccess(c.restClient.R().SetResult(&result).SetBody(map[string]interface{}{
"accessToken": c.accessToken,
}).Post("v1/auth/token/renew"))
if err != nil {
return fmt.Errorf("error renewing token: %w", err)
}
c.restClient.SetAuthScheme(result.TokenType).SetAuthToken(result.AccessToken)
c.accessToken = result.AccessToken
c.tokenExpiration = time.Now().Add(time.Duration(result.ExpiresIn) * time.Second)
return nil
}
func (c *InfisicalClient) ListSecrets(opts *RetrieveSecretOptions) ([]Secret, error) {
err := c.CheckToken()
if err != nil {
return []Secret{}, err
}
var result SecretsResponse
_, err = util.CheckSuccess(c.restClient.R().SetResult(&result).SetQueryParams(map[string]string{
"workspaceId": opts.WorkspaceID,
"environment": opts.Environment,
"secretPath": opts.SecretPath,
"include_imports": fmt.Sprintf("%t", opts.IncludeImports),
}).Get("v3/secrets/raw"))
if err != nil {
return []Secret{}, err
}
return result.Secrets, nil
}
func (c *InfisicalClient) GetSecret(name string, opts *RetrieveSecretOptions) (Secret, error) {
err := c.CheckToken()
if err != nil {
return Secret{}, err
}
var result SecretResponse
_, err = util.CheckSuccess(c.restClient.R().SetResult(&result).SetQueryParams(map[string]string{
"workspaceId": opts.WorkspaceID,
"environment": opts.Environment,
"secretPath": opts.SecretPath,
"include_imports": fmt.Sprintf("%t", opts.IncludeImports),
"type": "shared",
}).Get("v3/secrets/raw/" + name))
if err != nil {
return Secret{}, err
}
return result.Secret, nil
}

62
pkg/infisical/structs.go Normal file
View File

@ -0,0 +1,62 @@
package infisical
type LoginResponse struct {
AccessToken string `json:"accessToken"`
ExpiresIn int `json:"expiresIn"`
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
TokenType string `json:"tokenType"`
}
type RetrieveSecretOptions struct {
WorkspaceID string
Environment string
SecretPath string
IncludeImports bool
}
func DefaultRetrieveSecretOptions() *RetrieveSecretOptions {
return &RetrieveSecretOptions{
WorkspaceID: "",
Environment: "",
SecretPath: "/",
IncludeImports: false,
}
}
func (o *RetrieveSecretOptions) WithWorkspaceID(workspaceId string) *RetrieveSecretOptions {
o.WorkspaceID = workspaceId
return o
}
func (o *RetrieveSecretOptions) WithEnvironment(environment string) *RetrieveSecretOptions {
o.Environment = environment
return o
}
func (o *RetrieveSecretOptions) WithSecretPath(secretPath string) *RetrieveSecretOptions {
o.SecretPath = secretPath
return o
}
func (o *RetrieveSecretOptions) WithImports(includeImports bool) *RetrieveSecretOptions {
o.IncludeImports = includeImports
return o
}
type Secret struct {
ID string `json:"_id"`
Environment string `json:"environment,omitempty"`
SecretComment string `json:"secretComment,omitempty"`
SecretKey string `json:"secretKey,omitempty"`
SecretValue string `json:"secretValue,omitempty"`
Version int `json:"version,omitempty"`
Workspace string `json:"workspace,omitempty"`
}
type SecretsResponse struct {
Secrets []Secret `json:"secrets"`
}
type SecretResponse struct {
Secret Secret `json:"secret"`
}

View File

@ -3,7 +3,6 @@ package qbittorrent
import (
"fmt"
"net/http"
"os"
"strings"
"time"
@ -21,12 +20,6 @@ func New(host string) *QbittorrentClient {
}
}
func NewFromEnv() (*QbittorrentClient, error) {
c := New(os.Getenv("QBITTORRENT_HOST"))
err := c.Login(os.Getenv("QBITTORRENT_USER"), os.Getenv("QBITTORRENT_PASS"))
return c, err
}
func (c *QbittorrentClient) Login(user string, pass string) error {
resp, err := util.CheckSuccess(c.restClient.R().SetFormData(map[string]string{
"username": user,

View File

@ -2,7 +2,6 @@ package syncthing
import (
"fmt"
"os"
"code.jhot.me/jhot/hats/internal/util"
"github.com/go-resty/resty/v2"
@ -20,10 +19,6 @@ func New(host string, token string) *SyncthingClient {
}
}
func NewFromEnv() *SyncthingClient {
return New(os.Getenv("SYNCTHING_HOST"), os.Getenv("SYNCTHING_TOKEN"))
}
func (c *SyncthingClient) GetVersion() (VersionInfo, error) {
var data VersionInfo
_, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get("system/version"))