2021-09-09 11:25:22 -05:00
|
|
|
package notifier
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
|
|
|
|
"github.com/grafana/grafana/pkg/infra/kvstore"
|
2021-10-12 05:05:02 -05:00
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
2021-09-09 11:25:22 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
const KVNamespace = "alertmanager"
|
|
|
|
|
|
|
|
// State represents any of the two 'states' of the alertmanager. Notification log or Silences.
|
|
|
|
// MarshalBinary returns the binary representation of this internal state based on the protobuf.
|
|
|
|
type State interface {
|
|
|
|
MarshalBinary() ([]byte, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// FileStore is in charge of persisting the alertmanager files to the database.
|
|
|
|
// It uses the KVstore table and encodes the files as a base64 string.
|
|
|
|
type FileStore struct {
|
|
|
|
kv *kvstore.NamespacedKVStore
|
|
|
|
orgID int64
|
|
|
|
workingDirPath string
|
2021-10-12 05:05:02 -05:00
|
|
|
logger log.Logger
|
2021-09-09 11:25:22 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewFileStore(orgID int64, store kvstore.KVStore, workingDirPath string) *FileStore {
|
|
|
|
return &FileStore{
|
|
|
|
workingDirPath: workingDirPath,
|
|
|
|
orgID: orgID,
|
|
|
|
kv: kvstore.WithNamespace(store, orgID, KVNamespace),
|
2021-10-12 05:05:02 -05:00
|
|
|
logger: log.New("filestore", "org", orgID),
|
2021-09-09 11:25:22 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FilepathFor returns the filepath to an Alertmanager file.
|
|
|
|
// If the file is already present on disk it no-ops.
|
|
|
|
// If not, it tries to read the database and if there's no file it no-ops.
|
|
|
|
// If there is a file in the database, it decodes it and writes to disk for Alertmanager consumption.
|
2021-10-12 05:05:02 -05:00
|
|
|
func (fileStore *FileStore) FilepathFor(ctx context.Context, filename string) (string, error) {
|
2021-09-09 11:25:22 -05:00
|
|
|
// Then, let's attempt to read it from the database.
|
2021-10-12 05:05:02 -05:00
|
|
|
content, exists, err := fileStore.kv.Get(ctx, filename)
|
2021-09-09 11:25:22 -05:00
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error reading file '%s' from database: %w", filename, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// if it doesn't exist, let's no-op and let the Alertmanager create one. We'll eventually save it to the database.
|
|
|
|
if !exists {
|
2021-10-12 05:05:02 -05:00
|
|
|
return fileStore.pathFor(filename), nil
|
2021-09-09 11:25:22 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// If we have a file stored in the database, let's decode it and write it to disk to perform that initial load to memory.
|
|
|
|
bytes, err := decode(content)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error decoding file '%s': %w", filename, err)
|
|
|
|
}
|
|
|
|
|
2021-10-12 05:05:02 -05:00
|
|
|
if err := fileStore.WriteFileToDisk(filename, bytes); err != nil {
|
2021-09-09 11:25:22 -05:00
|
|
|
return "", fmt.Errorf("error writing file %s: %w", filename, err)
|
|
|
|
}
|
|
|
|
|
2021-10-12 05:05:02 -05:00
|
|
|
return fileStore.pathFor(filename), err
|
2021-09-09 11:25:22 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Persist takes care of persisting the binary representation of internal state to the database as a base64 encoded string.
|
2021-10-12 05:05:02 -05:00
|
|
|
func (fileStore *FileStore) Persist(ctx context.Context, filename string, st State) (int64, error) {
|
2021-09-09 11:25:22 -05:00
|
|
|
var size int64
|
|
|
|
|
|
|
|
bytes, err := st.MarshalBinary()
|
|
|
|
if err != nil {
|
|
|
|
return size, err
|
|
|
|
}
|
|
|
|
|
2021-10-12 05:05:02 -05:00
|
|
|
if err = fileStore.kv.Set(ctx, filename, encode(bytes)); err != nil {
|
2021-09-09 11:25:22 -05:00
|
|
|
return size, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return int64(len(bytes)), err
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteFileToDisk writes a file with the provided name and contents to the Alertmanager working directory with the default grafana permission.
|
2021-10-12 05:05:02 -05:00
|
|
|
func (fileStore *FileStore) WriteFileToDisk(fn string, content []byte) error {
|
2021-09-14 08:40:59 -05:00
|
|
|
// Ensure the working directory is created
|
2021-10-12 05:05:02 -05:00
|
|
|
err := os.MkdirAll(fileStore.workingDirPath, 0750)
|
2021-09-14 08:40:59 -05:00
|
|
|
if err != nil {
|
2021-10-12 05:05:02 -05:00
|
|
|
return fmt.Errorf("unable to create the working directory %q: %s", fileStore.workingDirPath, err)
|
2021-09-14 08:40:59 -05:00
|
|
|
}
|
|
|
|
|
2021-10-12 05:05:02 -05:00
|
|
|
return os.WriteFile(fileStore.pathFor(fn), content, 0644)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CleanUp will remove the working directory from disk.
|
|
|
|
func (fileStore *FileStore) CleanUp() {
|
|
|
|
if err := os.RemoveAll(fileStore.workingDirPath); err != nil {
|
|
|
|
fileStore.logger.Warn("unable to delete the local working directory", "dir", fileStore.workingDirPath,
|
|
|
|
"err", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
fileStore.logger.Info("successfully deleted working directory", "dir", fileStore.workingDirPath)
|
2021-09-09 11:25:22 -05:00
|
|
|
}
|
|
|
|
|
2021-10-12 05:05:02 -05:00
|
|
|
func (fileStore *FileStore) pathFor(fn string) string {
|
|
|
|
return filepath.Join(fileStore.workingDirPath, fn)
|
2021-09-09 11:25:22 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func decode(s string) ([]byte, error) {
|
|
|
|
return base64.StdEncoding.DecodeString(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
func encode(b []byte) string {
|
|
|
|
return base64.StdEncoding.EncodeToString(b)
|
|
|
|
}
|