mirror of
https://github.com/dokku/dokku.git
synced 2025-12-29 00:25:08 +01:00
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.
318 lines
7.7 KiB
Go
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
|
|
}
|