discourse_docker/launcher_go/v2/config/config.go

233 lines
6.1 KiB
Go

package config
import (
"errors"
"fmt"
"os"
"regexp"
"slices"
"strings"
"dario.cat/mergo"
"github.com/discourse/discourse_docker/launcher_go/v2/utils"
"gopkg.in/yaml.v3"
)
const defaultBootCommand = "/sbin/boot"
const defaultBaseImage = "discourse/base:2.0.20231121-0024"
type Config struct {
Name string `yaml:-`
rawYaml []string
Base_Image string `yaml:,omitempty`
Update_Pups bool `yaml:,omitempty`
Run_Image string `yaml:,omitempty`
Boot_Command string `yaml:,omitempty`
No_Boot_Command bool `yaml:,omitempty`
Docker_Args string `yaml:,omitempty`
Templates []string `yaml:templates,omitempty`
Expose []string `yaml:expose,omitempty`
Params map[string]string `yaml:params,omitempty`
Env map[string]string `yaml:env,omitempty`
Labels map[string]string `yaml:labels,omitempty`
Volumes []struct {
Volume struct {
Host string `yaml:host`
Guest string `yaml:guest`
} `yaml:volume`
} `yaml:volumes,omitempty`
Links []struct {
Link struct {
Name string `yaml:name`
Alias string `yaml:alias`
} `yaml:link`
} `yaml:links,omitempty`
}
func (config *Config) loadTemplate(templateDir string, template string) error {
template_filename := strings.TrimRight(templateDir, "/") + "/" + string(template)
content, err := os.ReadFile(template_filename)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("template file does not exist: " + template_filename)
}
return err
}
templateConfig := &Config{}
if err := yaml.Unmarshal(content, templateConfig); err != nil {
return err
}
if err := mergo.Merge(config, templateConfig, mergo.WithOverride); err != nil {
return err
}
config.rawYaml = append(config.rawYaml, string(content[:]))
return nil
}
func LoadConfig(dir string, configName string, includeTemplates bool, templatesDir string) (*Config, error) {
config := &Config{
Name: configName,
Boot_Command: defaultBootCommand,
Base_Image: defaultBaseImage,
}
matched, _ := regexp.MatchString("[[:upper:]/ !@#$%^&*()+~`=]", configName)
if matched {
msg := "ERROR: Config name '" + configName + "' must not contain upper case characters, spaces or special characters. Correct config name and rerun."
fmt.Println(msg)
return nil, errors.New(msg)
}
config_filename := string(strings.TrimRight(dir, "/") + "/" + config.Name + ".yml")
content, err := os.ReadFile(config_filename)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("config file does not exist: " + config_filename)
}
return nil, err
}
baseConfig := &Config{}
if err := yaml.Unmarshal(content, baseConfig); err != nil {
return nil, err
}
if includeTemplates {
for _, t := range baseConfig.Templates {
if err := config.loadTemplate(templatesDir, t); err != nil {
return nil, err
}
}
}
if err := mergo.Merge(config, baseConfig, mergo.WithOverride); err != nil {
return nil, err
}
config.rawYaml = append(config.rawYaml, string(content[:]))
if err != nil {
return nil, err
}
for k, v := range config.Labels {
val := strings.ReplaceAll(v, "{{config}}", config.Name)
config.Labels[k] = val
}
for k, v := range config.Env {
val := strings.ReplaceAll(v, "{{config}}", config.Name)
config.Env[k] = val
}
return config, nil
}
func (config *Config) Yaml() string {
return strings.Join(config.rawYaml, "_FILE_SEPERATOR_")
}
func (config *Config) Dockerfile(pupsArgs string, bakeEnv bool) string {
builder := strings.Builder{}
builder.WriteString("ARG dockerfile_from_image=" + config.Base_Image + "\n")
builder.WriteString("FROM ${dockerfile_from_image}\n")
builder.WriteString(config.dockerfileArgs() + "\n")
if bakeEnv {
builder.WriteString(config.dockerfileEnvs() + "\n")
}
builder.WriteString(config.dockerfileExpose() + "\n")
builder.WriteString("COPY config.yaml /temp-config.yaml\n")
builder.WriteString("RUN " +
"cat /temp-config.yaml | /usr/local/bin/pups " + pupsArgs + " --stdin " +
"&& rm /temp-config.yaml\n")
builder.WriteString("CMD [\"" + config.BootCommand() + "\"]")
return builder.String()
}
func (config *Config) WriteYamlConfig(dir string) error {
file := strings.TrimRight(dir, "/") + "/config.yaml"
if err := os.WriteFile(file, []byte(config.Yaml()), 0660); err != nil {
return errors.New("error writing config file " + file)
}
return nil
}
func (config *Config) BootCommand() string {
if len(config.Boot_Command) > 0 {
return config.Boot_Command
} else if config.No_Boot_Command {
return ""
} else {
return defaultBootCommand
}
}
func (config *Config) EnvArray(includeKnownSecrets bool) []string {
envs := []string{}
for k, v := range config.Env {
if !includeKnownSecrets && slices.Contains(utils.KnownSecrets, k) {
continue
}
envs = append(envs, k+"="+v)
}
slices.Sort(envs)
return envs
}
func (config *Config) DockerArgs() []string {
return strings.Fields(config.Docker_Args)
}
func (config *Config) dockerfileEnvs() string {
builder := []string{}
for k, _ := range config.Env {
builder = append(builder, "ENV "+k+"=${"+k+"}")
}
slices.Sort(builder)
return strings.Join(builder, "\n")
}
func (config *Config) dockerfileArgs() string {
builder := []string{}
for k, _ := range config.Env {
builder = append(builder, "ARG "+k)
}
slices.Sort(builder)
return strings.Join(builder, "\n")
}
func (config *Config) dockerfileExpose() string {
builder := []string{}
for _, p := range config.Expose {
port := p
if strings.Contains(p, ":") {
_, port, _ = strings.Cut(p, ":")
}
builder = append(builder, "EXPOSE "+port)
}
slices.Sort(builder)
return strings.Join(builder, "\n")
}
func (config *Config) RunImage() string {
if len(config.Run_Image) > 0 {
return config.Run_Image
}
return utils.BaseImageName + config.Name
}
func (config *Config) DockerHostname(defaultHostname string) string {
_, exists := config.Env["DOCKER_USE_HOSTNAME"]
re := regexp.MustCompile(`[^a-zA-Z-]`)
hostname := defaultHostname
if exists {
hostname = config.Env["DISCOURSE_HOSTNAME"]
}
hostname = string(re.ReplaceAll([]byte(hostname), []byte("-"))[:])
return hostname
}