Files
dokku/plugins/logs/functions.go
Jose Diaz-Gonzalez 1c0bd39d42 feat: ensure referenced images get updated by dependabot
Dokku uses a few external images for various bits of functionality, but these must be manually updated every so often due to being referenced in code.

By moving the references to Dockerfiles, we can take advantage of dependabot to issue automatic PRs when they are out of date.
2024-02-25 00:54:11 -05:00

318 lines
7.7 KiB
Go

package logs
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/joncalhoun/qson"
)
type vectorConfig struct {
Sources map[string]vectorSource `json:"sources"`
Sinks map[string]vectorSink `json:"sinks"`
}
type vectorSource struct {
Type string `json:"type"`
IncludeLabels []string `json:"include_labels,omitempty"`
}
type vectorTemplateData struct {
DokkuLibRoot string
DokkuLogsDir string
VectorImage string
}
type vectorSink map[string]interface{}
const vectorContainerName = "vector-vector-1"
const vectorOldContainerName = "vector"
func getComposeFile() ([]byte, error) {
result, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "vector-template-source",
})
if err == nil && result.ExitCode == 0 && strings.TrimSpace(result.Stdout) != "" {
contents, err := os.ReadFile(strings.TrimSpace(result.Stdout))
if err != nil {
return []byte{}, fmt.Errorf("Unable to read compose template: %s", err)
}
return contents, nil
}
contents, err := templates.ReadFile("templates/compose.yml.tmpl")
if err != nil {
return []byte{}, fmt.Errorf("Unable to read compose template: %s", err)
}
return contents, nil
}
func startVectorContainer(vectorImage string) error {
if !common.IsComposeInstalled() {
return errors.New("Required docker compose plugin is not installed")
}
if common.ContainerExists(vectorOldContainerName) {
return errors.New("Vector container %s already exists in old format, run 'dokku logs:vector-stop' once to remove it")
}
tmpFile, err := os.CreateTemp(os.TempDir(), "vector-compose-*.yml")
if err != nil {
return fmt.Errorf("Unable to create temporary file: %s", err)
}
defer os.Remove(tmpFile.Name())
contents, err := getComposeFile()
if err != nil {
return fmt.Errorf("Unable to read compose template: %s", err)
}
tmpl, err := template.New("compose.yml").Parse(string(contents))
if err != nil {
return fmt.Errorf("Unable to parse compose template: %s", err)
}
dokkuLibRoot := os.Getenv("DOKKU_LIB_HOST_ROOT")
if dokkuLibRoot == "" {
dokkuLibRoot = os.Getenv("DOKKU_LIB_ROOT")
}
dokkuLogsDir := os.Getenv("DOKKU_LOGS_HOST_DIR")
if dokkuLogsDir == "" {
dokkuLogsDir = os.Getenv("DOKKU_LOGS_DIR")
}
data := vectorTemplateData{
DokkuLibRoot: dokkuLibRoot,
DokkuLogsDir: dokkuLogsDir,
VectorImage: vectorImage,
}
if err := tmpl.Execute(tmpFile, data); err != nil {
return fmt.Errorf("Unable to execute compose template: %s", err)
}
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: common.DockerBin(),
Args: []string{
"compose",
"--file", tmpFile.Name(),
"--project-name", "vector",
"up",
"--detach",
"--quiet-pull",
},
StreamStdio: true,
})
if err != nil || result.ExitCode != 0 {
return fmt.Errorf("Unable to start vector container: %s", result.Stderr)
}
return nil
}
func getComputedVectorImage() string {
return common.PropertyGetDefault("logs", "--global", "vector-image", getDefaultVectorImage())
}
// getDefaultVectorImage returns the default image used for the vector container
func getDefaultVectorImage() string {
contents := strings.TrimSpace(VectorDockerfile)
parts := strings.SplitN(contents, " ", 2)
return parts[1]
}
func stopVectorContainer() error {
if !common.IsComposeInstalled() {
return errors.New("Required docker compose plugin is not installed")
}
if common.ContainerExists(vectorOldContainerName) {
common.ContainerRemove(vectorOldContainerName)
}
tmpFile, err := os.CreateTemp(os.TempDir(), "vector-compose-*.yml")
if err != nil {
return fmt.Errorf("Unable to create temporary file: %s", err)
}
defer os.Remove(tmpFile.Name())
contents, err := getComposeFile()
if err != nil {
return fmt.Errorf("Unable to read compose template: %s", err)
}
tmpl, err := template.New("compose.yml").Parse(string(contents))
if err != nil {
return fmt.Errorf("Unable to parse compose template: %s", err)
}
dokkuLibRoot := os.Getenv("DOKKU_LIB_HOST_ROOT")
if dokkuLibRoot == "" {
dokkuLibRoot = os.Getenv("DOKKU_LIB_ROOT")
}
dokkuLogsDir := os.Getenv("DOKKU_LOGS_HOST_DIR")
if dokkuLogsDir == "" {
dokkuLogsDir = os.Getenv("DOKKU_LOGS_DIR")
}
data := vectorTemplateData{
DokkuLibRoot: dokkuLibRoot,
DokkuLogsDir: dokkuLogsDir,
VectorImage: getComputedVectorImage(),
}
if err := tmpl.Execute(tmpFile, data); err != nil {
return fmt.Errorf("Unable to execute compose template: %s", err)
}
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: common.DockerBin(),
Args: []string{
"compose",
"--file", tmpFile.Name(),
"--project-name", "vector",
"down",
"--remove-orphans",
},
StreamStdio: true,
})
if err != nil || result.ExitCode != 0 {
return fmt.Errorf("Unable to stop vector container: %s", result.Stderr)
}
return nil
}
func sinkValueToConfig(appName string, sinkValue string) (vectorSink, error) {
var data vectorSink
if strings.Contains(sinkValue, "://") {
parts := strings.SplitN(sinkValue, "://", 2)
parts[0] = strings.ReplaceAll(parts[0], "_", "-")
sinkValue = strings.Join(parts, "://")
}
u, err := url.Parse(sinkValue)
if err != nil {
return data, err
}
if u.Query().Get("sinks") != "" {
return data, errors.New("Invalid option sinks")
}
u.Scheme = strings.ReplaceAll(u.Scheme, "-", "_")
query := u.RawQuery
query = strings.TrimPrefix(query, "&")
b, err := qson.ToJSON(query)
if err != nil {
return data, err
}
if err := json.Unmarshal(b, &data); err != nil {
return data, err
}
data["type"] = u.Scheme
data["inputs"] = []string{"docker-source:" + appName}
if appName == "--global" {
data["inputs"] = []string{"docker-global-source"}
}
if appName == "--null" {
data["inputs"] = []string{"docker-null-source"}
}
return data, nil
}
func writeVectorConfig() error {
apps, _ := common.UnfilteredDokkuApps()
data := vectorConfig{
Sources: map[string]vectorSource{},
Sinks: map[string]vectorSink{},
}
for _, appName := range apps {
value := common.PropertyGet("logs", appName, "vector-sink")
if value == "" {
continue
}
inflectedAppName := strings.ReplaceAll(appName, ".", "-")
sink, err := sinkValueToConfig(inflectedAppName, value)
if err != nil {
return err
}
data.Sources[fmt.Sprintf("docker-source:%s", inflectedAppName)] = vectorSource{
Type: "docker_logs",
IncludeLabels: []string{fmt.Sprintf("com.dokku.app-name=%s", appName)},
}
data.Sinks[fmt.Sprintf("docker-sink:%s", inflectedAppName)] = sink
}
value := common.PropertyGet("logs", "--global", "vector-sink")
if value != "" {
sink, err := sinkValueToConfig("--global", value)
if err != nil {
return err
}
data.Sources["docker-global-source"] = vectorSource{
Type: "docker_logs",
IncludeLabels: []string{"com.dokku.app-name"},
}
data.Sinks["docker-global-sink"] = sink
}
if len(data.Sources) == 0 {
// pull from no containers
data.Sources["docker-null-source"] = vectorSource{
Type: "docker_logs",
IncludeLabels: []string{"com.dokku.vector-null"},
}
}
if len(data.Sinks) == 0 {
// write logs to a blackhole
sink, err := sinkValueToConfig("--null", VectorDefaultSink)
if err != nil {
return err
}
data.Sinks["docker-null-sink"] = sink
}
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
b = bytes.Replace(b, []byte("\\u0026"), []byte("&"), -1)
b = bytes.Replace(b, []byte("\\u002B"), []byte("+"), -1)
vectorConfig := filepath.Join(common.GetDataDirectory("logs"), "vector.json")
if err := common.WriteBytesToFile(common.WriteBytesToFileInput{
Bytes: b,
Filename: vectorConfig,
Mode: os.FileMode(0600),
}); err != nil {
return err
}
return nil
}