Compare commits
No commits in common. "f0ad7c021ef1e74668017d26c41539e37c125547" and "7c7fe0ba7c54cbd311c98b96151bba34d0c9a1d2" have entirely different histories.
f0ad7c021e
...
7c7fe0ba7c
12 changed files with 140 additions and 300 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -8,19 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [0.2.0] - 2024-07-11
|
|
||||||
### ⚠️ Breaking Changes
|
|
||||||
- Config file syntax was changed to accomodate both private and public key for certificates.
|
|
||||||
|
|
||||||
This change is __NOT__ backwards compatible!
|
|
||||||
The following yaml keys were changed/added:
|
|
||||||
- `api_key`: changed to `cert_secret`
|
|
||||||
- `file_path`: changed to `cert_path`
|
|
||||||
- added keys: `key_secret`, `key_path`
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- config file syntax to accomodate private keys too
|
|
||||||
- refactor code
|
|
||||||
|
|
||||||
## [0.1.1] - 2024-07-03
|
## [0.1.1] - 2024-07-03
|
||||||
|
|
||||||
|
@ -34,7 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- some documentation
|
- some documentation
|
||||||
|
|
||||||
|
|
||||||
[unreleased]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.2.0...HEAD
|
[unreleased]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.1.1...HEAD
|
||||||
[0.2.0]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.1.1...0.2.0
|
|
||||||
[0.1.1]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.1.0...0.1.1
|
[0.1.1]: https://code.lila.network/adoralaura/certwarden-deploy/compare/0.1.0...0.1.1
|
||||||
[0.1.0]: https://code.lila.network/adoralaura/certwarden-deploy/releases/tag/0.1.0
|
[0.1.0]: https://code.lila.network/adoralaura/certwarden-deploy/releases/tag/0.1.0
|
||||||
|
|
|
@ -17,187 +17,126 @@ import (
|
||||||
|
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleCertificates(logger *slog.Logger, config *configuration.ConfigFileData) {
|
func HandleCertificates(logger *slog.Logger, config *configuration.ConfigFileData) {
|
||||||
for _, cert := range config.Certificates {
|
for _, cert := range config.Certificates {
|
||||||
certInfos := GenericCertificate{
|
certBytes, err := getCertFromServer(
|
||||||
Name: cert.Name,
|
logger,
|
||||||
FilePath: cert.CertificatePath,
|
cert.Name,
|
||||||
Secret: cert.CertificateSecret,
|
cert.ApiKey,
|
||||||
IsKey: false,
|
config.BaseURL,
|
||||||
}
|
config.DisableCertificateValidation,
|
||||||
|
)
|
||||||
keyInfos := GenericCertificate{
|
|
||||||
Name: cert.Name,
|
|
||||||
FilePath: cert.KeyPath,
|
|
||||||
Secret: cert.KeySecret,
|
|
||||||
IsKey: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rollout Certificate
|
|
||||||
certOnDiskChanged, err := certInfos.Rollout(logger, config.BaseURL, config.DisableCertificateValidation)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(
|
logger.Error("Failed to get certificate from server", "cert-id", cert.Name, "error", err)
|
||||||
"Failed to roll out Certificate", "path",
|
return
|
||||||
certInfos.FilePath, "name", cert.Name, "error", err,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rollout Key
|
certIsDifferent, err := checkCertIsDifferent(logger, cert.FilePath, certBytes)
|
||||||
keyOnDiskChanged, err := keyInfos.Rollout(logger, config.BaseURL, config.DisableCertificateValidation)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(
|
logger.Error("failed to handle certificate", "cert-id", cert.Name, "error", err)
|
||||||
"Failed to roll out Key", "path",
|
return
|
||||||
keyInfos.FilePath, "name", cert.Name, "error", err,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if cert OR key changed OR --force
|
if certIsDifferent || configuration.Force {
|
||||||
if (certOnDiskChanged || keyOnDiskChanged) || configuration.Force {
|
if configuration.Force {
|
||||||
|
logger.Info("Forcing file system change due to --force", "cert-id", cert.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateCertOnFS(logger, cert.FilePath, certBytes)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to handle certificate", "cert-id", cert.Name, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if configuration.Force {
|
if configuration.Force {
|
||||||
logger.Info("Forcing file system change due to --force", "name", cert.Name)
|
logger.Info("Forcing file system change due to --force", "cert-id", cert.Name)
|
||||||
}
|
}
|
||||||
err = handleCertificateAction(cert.Action)
|
err = handleCertificateAction(cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to execute post-rollout action", "name", cert.Name, "error", err)
|
logger.Error("post certificate change command failed", "cert-id", cert.Name, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if certIsDifferent {
|
||||||
}
|
logger.Info("New certificate rolled out", "cert-id", cert.Name)
|
||||||
|
} else {
|
||||||
// Rollout handles getting the certificate/key data from the
|
logger.Info("Certificate not changed, skipping...", "cert-id", cert.Name)
|
||||||
// server and writing it to disk if the data differs.
|
|
||||||
//
|
|
||||||
// Returns error on error, true if certificate action needs to be executed, false if not
|
|
||||||
func (c *GenericCertificate) Rollout(logger *slog.Logger, baseUrl string, skipInsecure bool) (bool, error) {
|
|
||||||
err := c.fetchFromServer(
|
|
||||||
logger,
|
|
||||||
baseUrl,
|
|
||||||
skipInsecure,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to get certificate from server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileNeedsRollout, err := c.needsRollout(logger)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to check certificate on disk: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileNeedsRollout || configuration.Force {
|
|
||||||
if configuration.Force {
|
|
||||||
logger.Info("Forcing file system change due to --force", "name", c.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.writeToDisk(logger)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to handle certificate: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if fileNeedsRollout {
|
|
||||||
logger.Info("New file deployed", "path", c.FilePath)
|
|
||||||
return true, nil
|
|
||||||
} else {
|
|
||||||
logger.Info("File not changed, skipping...", "path", c.FilePath)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// readFromDisk reads file data from disk and populates the data []byte field.
|
func getCertFromFile(path string) ([]byte, error) {
|
||||||
//
|
filebytes, err := os.ReadFile(path)
|
||||||
// Returns error or nil on success
|
|
||||||
func (c *GenericCertificate) readFromDisk() error {
|
|
||||||
filebytes, err := os.ReadFile(c.FilePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return err
|
return []byte{}, err
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("failed to read file from disk: %w", err)
|
return []byte{}, fmt.Errorf("failed to read certificate file on disk: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return filebytes, nil
|
||||||
c.diskBytes = filebytes
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// needsRollout checks the data []bytes against the data on disk.
|
func checkCertIsDifferent(logger *slog.Logger, path string, data []byte) (bool, error) {
|
||||||
//
|
filebytes, err := getCertFromFile(path)
|
||||||
// Returns true if file needs rollout, false if not
|
|
||||||
func (c *GenericCertificate) needsRollout(logger *slog.Logger) (bool, error) {
|
|
||||||
err := c.readFromDisk()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return true, nil
|
return true, nil
|
||||||
} else {
|
} else {
|
||||||
return false, fmt.Errorf("failed to compare data to file on disk: %w", err)
|
return false, fmt.Errorf("failed to compare certificates: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diskHash := sha256.Sum256(c.diskBytes)
|
existingSha256 := sha256.Sum256(filebytes)
|
||||||
serverHash := sha256.Sum256(c.serverBytes)
|
newSha256 := sha256.Sum256(data)
|
||||||
|
|
||||||
hashesAreDifferent := diskHash != serverHash
|
sumsAreDifferent := existingSha256 != newSha256
|
||||||
if hashesAreDifferent {
|
if sumsAreDifferent {
|
||||||
logger.Debug("File on disk differs from server source", "path", c.FilePath)
|
logger.Debug("Certificate on file differs from the certificate on the server", "cert-path", path)
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("File on disk is identical to server source", "path", c.FilePath)
|
logger.Debug("Certificate on file is identical to the certificate on the server", "cert-path", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return hashesAreDifferent, nil
|
return sumsAreDifferent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeToDisk flushes the certificate data to disk.
|
func updateCertOnFS(logger *slog.Logger, path string, data []byte) error {
|
||||||
//
|
|
||||||
// Returns error or nil on success.
|
|
||||||
func (c *GenericCertificate) writeToDisk(logger *slog.Logger) error {
|
|
||||||
if configuration.DryRun {
|
if configuration.DryRun {
|
||||||
logger.Debug("DRY-RUN: writing data to file", "path", c.FilePath)
|
logger.Debug("DRY-RUN: writing certificate data to file", "cert-path", path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create(c.FilePath)
|
file, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open file for writing: %w", err)
|
return fmt.Errorf("failed to open certificate for writing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func(l *slog.Logger) {
|
defer func(l *slog.Logger) {
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
l.Error("failed to close file", "path", c.FilePath, "error", err)
|
l.Error("failed to close file", "file-path", path, "error", err)
|
||||||
}
|
}
|
||||||
}(logger)
|
}(logger)
|
||||||
|
|
||||||
w := bufio.NewWriter(file)
|
w := bufio.NewWriter(file)
|
||||||
|
|
||||||
if _, err := w.Write(c.serverBytes); err != nil {
|
if _, err := w.Write(data); err != nil {
|
||||||
return fmt.Errorf("failed to write data to file: %w", err)
|
return fmt.Errorf("failed to write certificate data to file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = w.Flush(); err != nil {
|
if err = w.Flush(); err != nil {
|
||||||
return fmt.Errorf("failed to flush data to file: %w", err)
|
return fmt.Errorf("failed to flush certificate data to file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("Successfully wrote to file", "path", c.FilePath)
|
logger.Debug("wrote certificate to file", "file-path", path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchFromServer fetches the cert/key data from the CertWarden server and
|
func getCertFromServer(logger *slog.Logger, certName string, certKey string, baseUrl string, skipInsecure bool) ([]byte, error) {
|
||||||
// fills the serverBytes field.
|
url := baseUrl + constants.CertificateApiPath + certName
|
||||||
//
|
|
||||||
// Returns error or nil on success.
|
|
||||||
func (c *GenericCertificate) fetchFromServer(logger *slog.Logger, baseUrl string, skipInsecure bool) error {
|
|
||||||
var url string
|
|
||||||
if c.IsKey {
|
|
||||||
url = baseUrl + constants.CertificateApiPath + c.Name
|
|
||||||
} else {
|
|
||||||
url = baseUrl + constants.CertificateApiPath + c.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug("Certificate request URL: " + url)
|
logger.Debug("Certificate request URL: " + url)
|
||||||
var transport http.RoundTripper
|
var transport http.RoundTripper
|
||||||
|
|
||||||
|
@ -216,15 +155,17 @@ func (c *GenericCertificate) fetchFromServer(logger *slog.Logger, baseUrl string
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to prepare to request certificate from server: %w", err)
|
return []byte{}, fmt.Errorf("failed to prepare to request certificate from server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("User-Agent", constants.UserAgent)
|
req.Header.Set("User-Agent", constants.UserAgent)
|
||||||
req.Header.Add(constants.ApiKeyHeaderName, c.Secret)
|
req.Header.Add(constants.ApiKeyHeaderName, certKey)
|
||||||
|
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to request certificate from server: %w", err)
|
e := fmt.Errorf("failed to request certificate from server: %w", err)
|
||||||
|
sentry.CaptureException(e)
|
||||||
|
return []byte{}, e
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func(l *slog.Logger) {
|
defer func(l *slog.Logger) {
|
||||||
|
@ -234,27 +175,30 @@ func (c *GenericCertificate) fetchFromServer(logger *slog.Logger, baseUrl string
|
||||||
}(logger)
|
}(logger)
|
||||||
|
|
||||||
if res.StatusCode == http.StatusUnauthorized {
|
if res.StatusCode == http.StatusUnauthorized {
|
||||||
logger.Error("API-Key for Certificate is invalid, skipping certificate!", "name", c.Name)
|
logger.Error("API-Key for Certificate is invalid, skipping certificate!", "cert-id", certName)
|
||||||
return errors.New("API-Key invalid")
|
return []byte{}, errors.New("API-Key invalid")
|
||||||
} else if res.StatusCode != http.StatusOK {
|
} else if res.StatusCode != http.StatusOK {
|
||||||
logger.Error("failed to get certificate from server", "name", c.Name, "http-response", res.Status)
|
logger.Error("failed to get certificate from server", "cert-id", certName, "http-response", res.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyBytes, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read certificate response from server: %w", err)
|
e := fmt.Errorf("failed to read certificate response from server: %w", err)
|
||||||
|
sentry.CaptureException(e)
|
||||||
|
return []byte{}, e
|
||||||
}
|
}
|
||||||
|
|
||||||
c.serverBytes = bodyBytes
|
return body, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCertificateAction executes the user-defined action after successful certificate deployment
|
func handleCertificateAction(cert configuration.CertificateData) error {
|
||||||
func handleCertificateAction(action string) error {
|
if cert.Action == "" {
|
||||||
if action == "" {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
action := strings.ReplaceAll(cert.Action, "{name}", cert.Name)
|
||||||
|
action = strings.ReplaceAll(action, "{path}", cert.FilePath)
|
||||||
|
|
||||||
sargs := strings.Split(action, " ")
|
sargs := strings.Split(action, " ")
|
||||||
|
|
||||||
cmd := exec.Command(sargs[0], sargs[1:]...)
|
cmd := exec.Command(sargs[0], sargs[1:]...)
|
||||||
|
|
1
internal/certificates/certificates_test.go
Normal file
1
internal/certificates/certificates_test.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package certificates
|
|
@ -1,18 +0,0 @@
|
||||||
package certificates
|
|
||||||
|
|
||||||
// GenericCertificate is a generic container to enable us to
|
|
||||||
// handle both certificates and keys with one function
|
|
||||||
type GenericCertificate struct {
|
|
||||||
Name string
|
|
||||||
FilePath string
|
|
||||||
Secret string
|
|
||||||
|
|
||||||
// True if key, false if certificate
|
|
||||||
IsKey bool
|
|
||||||
|
|
||||||
// Bytes fetched from the server
|
|
||||||
serverBytes []byte
|
|
||||||
|
|
||||||
// Bytes fetched from disk
|
|
||||||
diskBytes []byte
|
|
||||||
}
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/certificates"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/certificates"
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/configuration"
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/constants"
|
||||||
|
"code.lila.network/adoralaura/certwarden-deploy/internal/errlog"
|
||||||
"code.lila.network/adoralaura/certwarden-deploy/internal/logger"
|
"code.lila.network/adoralaura/certwarden-deploy/internal/logger"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
@ -31,14 +32,12 @@ func handleRootCmd(cmd *cobra.Command, args []string) {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
log := logger.InitializeLogger()
|
log := logger.InitializeLogger()
|
||||||
config.SubstituteKeys(log)
|
err = errlog.SetupSentry(log, config.Sentry.DSN)
|
||||||
|
if err != nil {
|
||||||
validation := config.IsValid()
|
slog.Error("failed to initialize sentry", "error", err)
|
||||||
if validation.HasMessages() {
|
|
||||||
validation.Print(log)
|
|
||||||
slog.Error("The configuration file has errors! Application cannot start unless all errors are corrected!")
|
|
||||||
panic(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configuration.ValidateConfig(log, *config)
|
||||||
|
|
||||||
certificates.HandleCertificates(log, config)
|
certificates.HandleCertificates(log, config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package configuration
|
package configuration
|
||||||
|
|
||||||
import "log/slog"
|
|
||||||
|
|
||||||
// Config file gets read into here
|
// Config file gets read into here
|
||||||
var Config *ConfigFileData
|
var Config *ConfigFileData
|
||||||
|
|
||||||
|
@ -30,36 +28,12 @@ type ConfigFileData struct {
|
||||||
|
|
||||||
// Struct that holds the details of a single managed certificate
|
// Struct that holds the details of a single managed certificate
|
||||||
type CertificateData struct {
|
type CertificateData struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
CertificateSecret string `yaml:"cert_secret"`
|
ApiKey string `yaml:"api_key"`
|
||||||
CertificatePath string `yaml:"cert_path"`
|
Action string `yaml:"action"`
|
||||||
KeySecret string `yaml:"key_secret"`
|
FilePath string `yaml:"file_path"`
|
||||||
KeyPath string `yaml:"key_path"`
|
|
||||||
Action string `yaml:"action"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SentryData struct {
|
type SentryData struct {
|
||||||
DSN string `yaml:"dsn"`
|
DSN string `yaml:"dsn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigValidationError struct {
|
|
||||||
ErrorMessages []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ConfigValidationError) Error() string {
|
|
||||||
return "Configuration file has errors! Application cannot start unless the errors are corrected."
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ConfigValidationError) Add(msg string) {
|
|
||||||
e.ErrorMessages = append(e.ErrorMessages, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ConfigValidationError) HasMessages() bool {
|
|
||||||
return len(e.ErrorMessages) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ConfigValidationError) Print(logger *slog.Logger) {
|
|
||||||
for _, line := range e.ErrorMessages {
|
|
||||||
logger.Error(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
package configuration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *ConfigFileData) SubstituteKeys(logger *slog.Logger) {
|
|
||||||
for index, cert := range c.Certificates {
|
|
||||||
c.Certificates[index].CertificatePath = strings.ReplaceAll(cert.CertificatePath, "{name}", c.Certificates[index].Name)
|
|
||||||
c.Certificates[index].KeyPath = strings.ReplaceAll(cert.KeyPath, "{name}", c.Certificates[index].Name)
|
|
||||||
|
|
||||||
c.Certificates[index].Action = strings.ReplaceAll(cert.Action, "{name}", c.Certificates[index].Name)
|
|
||||||
c.Certificates[index].Action = strings.ReplaceAll(c.Certificates[index].Action, "{cert_path}", c.Certificates[index].CertificatePath)
|
|
||||||
c.Certificates[index].Action = strings.ReplaceAll(c.Certificates[index].Action, "{key_path}", c.Certificates[index].KeyPath)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package configuration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestStringSubstitutionWithPlaceholders tests the string substitution feature.
|
|
||||||
// It ensures that {name}, {cert_path} and {key_path} get substituted correctly.
|
|
||||||
func TestStringSubstitutionWithPlaceholders(t *testing.T) {
|
|
||||||
cert := CertificateData{
|
|
||||||
Name: "qwer",
|
|
||||||
CertificatePath: "/fake/path/{name}",
|
|
||||||
KeyPath: "/fake/path/{name}-key",
|
|
||||||
Action: "./fake action {cert_path} {key_path}",
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := ConfigFileData{
|
|
||||||
Certificates: []CertificateData{cert},
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.SubstituteKeys(nil)
|
|
||||||
|
|
||||||
if cfg.Certificates[0].CertificatePath != "/fake/path/qwer" {
|
|
||||||
t.Fail()
|
|
||||||
t.Logf(`CertificatePath = %q, want "/fake/path/qwer"`, cfg.Certificates[0].CertificatePath)
|
|
||||||
}
|
|
||||||
if cfg.Certificates[0].KeyPath != "/fake/path/qwer-key" {
|
|
||||||
t.Fail()
|
|
||||||
t.Logf(`KeyPath = %q, want "/fake/path/qwer-key"`, cfg.Certificates[0].KeyPath)
|
|
||||||
}
|
|
||||||
if cfg.Certificates[0].Action != "./fake action /fake/path/qwer /fake/path/qwer-key" {
|
|
||||||
t.Fail()
|
|
||||||
t.Logf(`Action = %q, want "./fake action /fake/path/qwer /fake/path/qwer-key"`, cfg.Certificates[0].Action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestStringSubstitutionWithPlaceholders tests the string substitution feature.
|
|
||||||
// It ensures that if no substitutes are present, the config values are not changed.
|
|
||||||
func TestStringSubstitutionWithoutPlaceholders(t *testing.T) {
|
|
||||||
cert := CertificateData{
|
|
||||||
Name: "qwer",
|
|
||||||
CertificatePath: "/fake/path/asd",
|
|
||||||
KeyPath: "/fake/path/asdf-key",
|
|
||||||
Action: "./fake action abcd efgh",
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := ConfigFileData{
|
|
||||||
Certificates: []CertificateData{cert},
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.SubstituteKeys(nil)
|
|
||||||
|
|
||||||
if cfg.Certificates[0].CertificatePath != "/fake/path/asd" {
|
|
||||||
t.Fail()
|
|
||||||
t.Logf(`CertificatePath = %q, want "/fake/path/asd"`, cfg.Certificates[0].CertificatePath)
|
|
||||||
}
|
|
||||||
if cfg.Certificates[0].KeyPath != "/fake/path/asdf-key" {
|
|
||||||
t.Fail()
|
|
||||||
t.Logf(`KeyPath = %q, want "/fake/path/asdf-key"`, cfg.Certificates[0].KeyPath)
|
|
||||||
}
|
|
||||||
if cfg.Certificates[0].Action != "./fake action abcd efgh" {
|
|
||||||
t.Fail()
|
|
||||||
t.Logf(`Action = %q, want "./fake action abcd efgh"`, cfg.Certificates[0].Action)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +1,45 @@
|
||||||
package configuration
|
package configuration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsValid tests if the config read from file has all required parameters set.
|
func ValidateConfig(logger *slog.Logger, config ConfigFileData) {
|
||||||
//
|
validationFailed := false
|
||||||
// Exits the app if errors are detected
|
|
||||||
func (c *ConfigFileData) IsValid() ConfigValidationError {
|
|
||||||
err := ConfigValidationError{}
|
|
||||||
|
|
||||||
if c.BaseURL == "" {
|
if config.BaseURL == "" {
|
||||||
err.Add(`Field 'base_url' in config file is required!`)
|
logger.Error(`Field 'base_url' in config file is required!`)
|
||||||
|
validationFailed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cert := range c.Certificates {
|
for _, cert := range config.Certificates {
|
||||||
if cert.Name == "" {
|
if cert.Name == "" {
|
||||||
cert.Name = "unnamed_certificate"
|
cert.Name = "unnamed_certificate"
|
||||||
err.Add(`Field 'name' for certificates cannot be blank!`)
|
logger.Error(`Field 'name' for certificates cannot be blank!`)
|
||||||
|
validationFailed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if cert.CertificateSecret == "" {
|
if cert.ApiKey == "" {
|
||||||
err.Add(`Field 'cert_secret' for certificate ` + cert.Name + " cannot be blank!")
|
logger.Error(`Field 'api_key' for certificate ` + cert.Name + " cannot be blank!")
|
||||||
|
validationFailed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if cert.CertificatePath == "" {
|
if cert.FilePath == "" {
|
||||||
err.Add(`Field 'cert_path' for certificate ` + cert.Name + " cannot be blank!")
|
logger.Error(`Field 'file_path' for certificate ` + cert.Name + " cannot be blank!")
|
||||||
|
validationFailed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
re := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
re := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||||
if !re.MatchString(cert.Name) {
|
if !re.MatchString(cert.Name) {
|
||||||
err.Add(`Field 'name' for certificate may only contain -_. and alphanumeric characters!`)
|
logger.Error(`Field 'name' for certificate may only contain -_. and alphanumeric characters!`)
|
||||||
|
validationFailed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
if validationFailed {
|
||||||
|
logger.Error("Config file has errors! Please fix errors above! Exiting...", "config-path", ConfigFile)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package constants
|
package constants
|
||||||
|
|
||||||
const Version = "0.2.0"
|
const Version = "0.1.1"
|
||||||
const CertificateApiPath = "/certwarden/api/v1/download/certificates/"
|
const CertificateApiPath = "/certwarden/api/v1/download/certificates/"
|
||||||
const KeyApiPath = "/certwarden/api/v1/download/privatekeys/"
|
|
||||||
const ApiKeyHeaderName = "X-API-Key"
|
const ApiKeyHeaderName = "X-API-Key"
|
||||||
const UserAgent = "certwarden-deploy/" + Version + " +https://code.lila.network/adoralaura/certwarden-deploy"
|
const UserAgent = "certwarden-deploy/" + Version + " +https://code.lila.network/adoralaura/certwarden-deploy"
|
||||||
|
|
27
internal/errlog/sentry.go
Normal file
27
internal/errlog/sentry.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package errlog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupSentry(logger *slog.Logger, dsn string) error {
|
||||||
|
if dsn == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: dsn,
|
||||||
|
// Set TracesSampleRate to 1.0 to capture 100%
|
||||||
|
// of transactions for performance monitoring.
|
||||||
|
// We recommend adjusting this value in production,
|
||||||
|
TracesSampleRate: 1.0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to set up sentry")
|
||||||
|
}
|
||||||
|
// Flush buffered events before the program terminates.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -10,9 +10,11 @@ import (
|
||||||
|
|
||||||
func InitializeLogger() *slog.Logger {
|
func InitializeLogger() *slog.Logger {
|
||||||
logLevel := slog.LevelInfo
|
logLevel := slog.LevelInfo
|
||||||
|
sourceLogging := false
|
||||||
|
|
||||||
if configuration.VerboseLogging {
|
if configuration.VerboseLogging {
|
||||||
logLevel = slog.LevelDebug
|
logLevel = slog.LevelDebug
|
||||||
|
sourceLogging = true
|
||||||
}
|
}
|
||||||
if configuration.QuietLogging {
|
if configuration.QuietLogging {
|
||||||
logLevel = slog.LevelError
|
logLevel = slog.LevelError
|
||||||
|
@ -22,7 +24,8 @@ func InitializeLogger() *slog.Logger {
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &slog.HandlerOptions{
|
opts := &slog.HandlerOptions{
|
||||||
Level: logLevel,
|
Level: logLevel,
|
||||||
|
AddSource: sourceLogging,
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := slog.NewTextHandler(os.Stdout, opts)
|
handler := slog.NewTextHandler(os.Stdout, opts)
|
||||||
|
|
Loading…
Reference in a new issue