cli/dockercompose/compose.go
Rama main · réplica de lectura del repositorio original.
package dockercompose
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/nhost/be/services/mimir/model"
"github.com/nhost/nhost/cli/ssl"
)
const (
authPort = 4000
mailhogPort = 8025
dashboardPort = 3000
storagePort = 5000
functionsPort = 3000
hasuraPort = 8080
consolePort = 9695
postgresPort = 5432
configserverPort = 8088
)
const (
minimumHasuraVerson = "v2.18.0"
)
func rootNodeModules(branch string) string {
return sanitizeBranch(branch) + "-root_node_modules"
}
func functionsNodeModules(branch string) string {
return sanitizeBranch(branch) + "-functions_node_modules"
}
func ports(host, container uint) []Port {
if host == 0 {
return nil
}
return []Port{
{
Mode: "ingress",
Target: container,
Published: strconv.FormatUint(uint64(host), 10),
Protocol: "tcp",
},
}
}
type ComposeFile struct {
Services map[string]*Service `yaml:"services"`
Volumes map[string]struct{} `yaml:"volumes"`
}
// Environment is a map of environment variables that escapes literal `$` as
// `$$` when marshaled to YAML so Docker Compose doesn't interpret them as
// variable substitution.
type Environment map[string]string
func (e Environment) MarshalYAML() (any, error) {
escaped := make(map[string]string, len(e))
for k, v := range e {
escaped[k] = strings.ReplaceAll(v, "$", "$$")
}
return escaped, nil
}
//nolint:tagliatelle
type Service struct {
Image string `yaml:"image"`
DependsOn map[string]DependsOn `yaml:"depends_on,omitempty"`
EntryPoint []string `yaml:"entrypoint,omitempty"`
Command []string `yaml:"command,omitempty"`
Environment Environment `yaml:"environment,omitempty"`
ExtraHosts []string `yaml:"extra_hosts"`
HealthCheck *HealthCheck `yaml:"healthcheck,omitempty"`
Labels map[string]string `yaml:"labels,omitempty"`
Networks map[string]*NetworkConfig `yaml:"networks,omitempty"`
Ports []Port `yaml:"ports,omitempty"`
Restart string `yaml:"restart"`
User *string `yaml:"user,omitempty"`
Volumes []Volume `yaml:"volumes,omitempty"`
WorkingDir *string `yaml:"working_dir,omitempty"`
}
type DependsOn struct {
Condition string `yaml:"condition"`
}
type NetworkConfig struct {
Aliases []string `yaml:"aliases,omitempty"`
}
func networkAliases(aliases ...string) map[string]*NetworkConfig {
return map[string]*NetworkConfig{
"default": {Aliases: aliases},
}
}
//nolint:tagliatelle
type HealthCheck struct {
Test []string `yaml:"test"`
Timeout string `yaml:"timeout"`
Interval string `yaml:"interval"`
StartPeriod string `yaml:"start_period"`
}
type Port struct {
Mode string `yaml:"mode"`
Target uint `yaml:"target"`
Published string `yaml:"published"`
Protocol string `yaml:"protocol"`
}
//nolint:tagliatelle,tagalign // golines tag order conflicts with golangci-lint tagalign
type Volume struct {
Type string `yaml:"type"`
Source string `yaml:"source"`
Target string `yaml:"target"`
ReadOnly *bool `yaml:"read_only,omitempty"`
Bind *BindOptions `yaml:"bind,omitempty" exhaustruct:"optional"`
}
// BindOptions holds bind-mount-specific settings. SELinux carries the
// relabel option ("z" for a shared label) so that on SELinux or Podman
// hosts the container is granted access to the host source without the
// user having to relax SELinux enforcement themselves.
type BindOptions struct {
SELinux string `yaml:"selinux,omitempty"`
}
// extraHosts is the set of /etc/hosts entries injected into every bridge
// service. Public local.nhost.run hostnames are intentionally absent: on
// Linux, `host-gateway` resolves to the default docker0 bridge gateway,
// which is unroutable from containers attached to the user-defined
// project bridge. Resolution for those hostnames is provided by network
// aliases on the traefik service (see traefikAliases).
var extraHosts = []string{ //nolint:gochecknoglobals // immutable /etc/hosts entries shared by all bridge services
"host.docker.internal:host-gateway",
}
// traefikAliases returns the set of public local hostnames that resolve
// to the traefik container via Docker's embedded DNS on the project
// bridge. Container-to-container HTTPS via these hostnames terminates at
// traefik and is routed using the existing ingress labels.
func traefikAliases(subdomain string) []string {
return []string{
subdomain + ".auth.local.nhost.run",
subdomain + ".db.local.nhost.run",
subdomain + ".functions.local.nhost.run",
subdomain + ".graphql.local.nhost.run",
subdomain + ".hasura.local.nhost.run",
subdomain + ".storage.local.nhost.run",
subdomain + ".dashboard.local.nhost.run",
subdomain + ".mailhog.local.nhost.run",
"local.auth.nhost.run",
"local.db.nhost.run",
"local.functions.nhost.run",
"local.graphql.nhost.run",
"local.hasura.nhost.run",
"local.storage.nhost.run",
"local.dashboard.nhost.run",
"local.mailhog.nhost.run",
}
}
// hostGatewayHosts returns the legacy host-gateway based mapping used by
// containers that run outside the project's bridge network (e.g. the
// standalone hasura-cli helper started via `docker run` without
// --network), where the default docker0 bridge gateway can reach the
// host-published traefik port.
func hostGatewayHosts(subdomain string) []string {
return []string{
"host.docker.internal:host-gateway",
subdomain + ".auth.local.nhost.run:host-gateway",
subdomain + ".db.local.nhost.run:host-gateway",
subdomain + ".functions.local.nhost.run:host-gateway",
subdomain + ".graphql.local.nhost.run:host-gateway",
subdomain + ".hasura.local.nhost.run:host-gateway",
subdomain + ".storage.local.nhost.run:host-gateway",
"local.auth.nhost.run:host-gateway",
"local.db.nhost.run:host-gateway",
"local.functions.nhost.run:host-gateway",
"local.graphql.nhost.run:host-gateway",
"local.hasura.nhost.run:host-gateway",
"local.storage.nhost.run:host-gateway",
}
}
const traefikConfig = `
# v1
# DO NOT EDIT THIS FILE
tls:
certificates:
- certFile: /opt/traefik/certs/local.crt
keyFile: /opt/traefik/certs/local.key
- certFile: /opt/traefik/certs/sub.crt
keyFile: /opt/traefik/certs/sub.key
log:
level: DEBUG
accessLog: {}
`
func dumpCert(
cert []byte,
key []byte,
dstName string,
dotnhostfolder string,
) error {
f1, err := os.OpenFile(
filepath.Join(dotnhostfolder, "traefik", "certs", dstName+".crt"),
os.O_TRUNC|os.O_CREATE|os.O_WRONLY,
0o644, //nolint:mnd
)
if err != nil {
return fmt.Errorf("failed to open local.crt: %w", err)
}
defer f1.Close()
if _, err := f1.Write(cert); err != nil {
return fmt.Errorf("failed to write local.crt: %w", err)
}
f2, err := os.OpenFile(
filepath.Join(dotnhostfolder, "traefik", "certs", dstName+".key"),
os.O_TRUNC|os.O_CREATE|os.O_WRONLY,
0o644, //nolint:mnd
)
if err != nil {
return fmt.Errorf("failed to open local.key: %w", err)
}
defer f2.Close()
if _, err := f2.Write(key); err != nil {
return fmt.Errorf("failed to write local.cert: %w", err)
}
return nil
}
func trafikFiles(dotnhostfolder string) error {
if err := os.MkdirAll(
filepath.Join(dotnhostfolder, "traefik", "certs"),
0o755, //nolint:mnd
); err != nil {
return fmt.Errorf("failed to create traefik folder: %w", err)
}
if err := dumpCert(ssl.LocalCertFile, ssl.LocalKeyFile, "local", dotnhostfolder); err != nil {
return fmt.Errorf("failed to dump local cert: %w", err)
}
if err := dumpCert(ssl.SubCertFile, ssl.SubKeyFile, "sub", dotnhostfolder); err != nil {
return fmt.Errorf("failed to dump sub cert: %w", err)
}
f, err := os.OpenFile(
filepath.Join(dotnhostfolder, "traefik", "traefik.yaml"),
os.O_TRUNC|os.O_CREATE|os.O_WRONLY,
0o644, //nolint:mnd
)
if err != nil {
return fmt.Errorf("failed to open traefik.yaml: %w", err)
}
defer f.Close()
if _, err := f.WriteString(traefikConfig); err != nil {
return fmt.Errorf("failed to write traefik.yaml: %w", err)
}
return nil
}
func traefik(
subdomain, projectName string,
port uint,
dotnhostfolder string,
dockerURL *url.URL,
) (*Service, error) {
if err := trafikFiles(dotnhostfolder); err != nil {
return nil, fmt.Errorf("failed to create traefik files: %w", err)
}
volumes := []Volume{{
Type: "bind",
Source: filepath.Join(dotnhostfolder, "traefik"),
Target: "/opt/traefik",
ReadOnly: new(true),
}}
containerDockerEndpoint := dockerURL.String()
if dockerURL.Scheme == "unix" {
volumes = append(volumes, Volume{
Type: "bind",
Source: dockerURL.Path,
Target: "/var/run/docker.sock",
ReadOnly: new(true),
// Relabel the socket with a shared SELinux label so traefik can
// reach the docker daemon on SELinux/Podman hosts (no-op elsewhere).
Bind: &BindOptions{SELinux: "z"},
})
containerDockerEndpoint = defaultDockerEndpoint
}
return &Service{
Image: "traefik:v3.6",
DependsOn: nil,
EntryPoint: nil,
Command: []string{
"--api.insecure=true",
"--providers.docker=true",
"--providers.docker.endpoint=" + containerDockerEndpoint,
"--providers.file.directory=/opt/traefik",
"--providers.file.watch=true",
"--providers.docker.exposedbydefault=false",
fmt.Sprintf(
"--providers.docker.constraints=Label(`com.docker.compose.project`,`%s`)",
projectName,
),
fmt.Sprintf("--entrypoints.web.address=:%d", port),
},
Environment: nil,
ExtraHosts: extraHosts,
HealthCheck: nil,
Labels: nil,
Networks: networkAliases(traefikAliases(subdomain)...),
Ports: []Port{
{
Mode: "ingress",
Target: port,
Published: strconv.FormatUint(uint64(port), 10),
Protocol: "tcp",
},
},
Restart: "always",
User: nil,
Volumes: volumes,
WorkingDir: nil,
}, nil
}
func minio(volumeName string) *Service {
return &Service{
Image: "nhost/minio:RELEASE.2025-02-28T09-55-16Z",
DependsOn: nil,
EntryPoint: []string{"/bin/sh"},
Command: []string{
"-c", "mkdir -p /data/nhost && /usr/bin/minio server --address :9000 /data",
},
Environment: map[string]string{
"MINIO_ROOT_PASSWORD": "minioaccesskey123123",
"MINIO_ROOT_USER": "minioaccesskey123123",
},
ExtraHosts: extraHosts,
Ports: nil,
Restart: "always",
User: nil,
HealthCheck: nil,
Labels: nil,
Networks: nil,
Volumes: []Volume{
{
Type: "volume",
Source: volumeName,
Target: "/data",
ReadOnly: nil,
},
},
WorkingDir: nil,
}
}
func dashboard( //nolint:funlen // single env-var config map, not decomposable
cfg *model.ConfigConfig,
subdomain string,
dashboardVersion string,
httpPort uint,
useTLS bool,
appID string,
) *Service {
// With constellation enabled, the dashboard's hasura admin/metadata API
// calls flow through constellation (which proxies unmatched paths to hasura)
// instead of hitting hasura directly. Console UI and migrations API stay on
// the hasura-cli helper containers and are not affected.
hasuraAPISubdomain := "hasura"
if cfg.GetExperimental().GetConstellation() != nil {
hasuraAPISubdomain = "graphql"
}
return &Service{
Image: dashboardVersion,
DependsOn: nil,
EntryPoint: nil,
Command: nil,
Environment: map[string]string{
"NEXT_PUBLIC_ENV": "dev",
"NEXT_PUBLIC_NHOST_PLATFORM": "false",
"NEXT_PUBLIC_NHOST_APP_ID": appID,
"NEXT_PUBLIC_NHOST_ADMIN_SECRET": cfg.Hasura.AdminSecret,
"NEXT_PUBLIC_NHOST_AUTH_URL": URL(
subdomain, "auth", httpPort, useTLS,
) + "/v1",
"NEXT_PUBLIC_NHOST_CONFIGSERVER_URL": URL(
subdomain, "dashboard", httpPort, useTLS,
) + "/v1/configserver/graphql",
"NEXT_PUBLIC_NHOST_FUNCTIONS_URL": URL(
subdomain, "functions", httpPort, useTLS,
) + "/v1",
"NEXT_PUBLIC_NHOST_LOGS_GRAPHQL_URL": URL(
subdomain, "dashboard", httpPort, useTLS,
) + "/v1/logs/graphql",
"NEXT_PUBLIC_NHOST_LOGS_WEBSOCKET": WebsocketURL(
subdomain, "dashboard", httpPort, useTLS,
) + "/v1/logs/graphql",
"NEXT_PUBLIC_NHOST_GRAPHQL_URL": URL(
subdomain, "graphql", httpPort, useTLS,
) + "/v1",
"NEXT_PUBLIC_NHOST_HASURA_API_URL": URL(
subdomain, hasuraAPISubdomain, httpPort, useTLS,
),
"NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL": URL(
subdomain, "hasura", httpPort, useTLS,
) + "/console",
"NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL": URL(
subdomain, "hasura", httpPort, useTLS,
) + "/apis/migrate",
"NEXT_PUBLIC_NHOST_STORAGE_URL": URL(
subdomain, "storage", httpPort, useTLS,
) + "/v1",
},
ExtraHosts: extraHosts,
HealthCheck: nil,
Labels: Ingresses{
{
Name: "dashboard",
TLS: useTLS,
Rule: traefikHostMatch("dashboard"),
Port: dashboardPort,
Rewrite: nil,
},
}.Labels(),
Networks: nil,
Ports: []Port{},
Restart: "",
User: nil,
Volumes: []Volume{},
WorkingDir: new(string),
}
}
func stripJWTSecretToPublic(value string) (string, error) {
var full map[string]any
if err := json.Unmarshal([]byte(value), &full); err != nil {
return value, nil //nolint:nilerr
}
public := make(map[string]any)
if v, ok := full["key"]; ok {
public["key"] = v
}
if v, ok := full["type"]; ok {
public["type"] = v
}
b, err := json.Marshal(public)
if err != nil {
return "", fmt.Errorf("failed to marshal JWT secret: %w", err)
}
return string(b), nil
}
func functions( //nolint:funlen
cfg *model.ConfigConfig,
subdomain string,
httpPort uint,
useTLS bool,
rootFolder string,
jwtSecret string,
port uint,
branch string,
functionsVersion string,
) (*Service, error) {
jwtSecret, err := stripJWTSecretToPublic(jwtSecret)
if err != nil {
return nil, fmt.Errorf("failed to strip JWT secret for %s: %w", "functions", err)
}
envVars := map[string]string{
"HASURA_GRAPHQL_ADMIN_SECRET": cfg.Hasura.AdminSecret,
"HASURA_GRAPHQL_DATABASE_URL": "postgres://nhost_auth_admin@local.db.nhost.run:5432/local",
"HASURA_GRAPHQL_GRAPHQL_URL": "http://graphql:8080/v1/graphql",
"HASURA_GRAPHQL_JWT_SECRET": jwtSecret,
"NHOST_ADMIN_SECRET": cfg.Hasura.AdminSecret,
"NHOST_AUTH_URL": URL(subdomain, "auth", httpPort, useTLS) + "/v1",
"NHOST_FUNCTIONS_URL": URL(subdomain, "functions", httpPort, useTLS) + "/v1",
"NHOST_GRAPHQL_URL": URL(subdomain, "graphql", httpPort, useTLS) + "/v1",
"NHOST_HASURA_URL": URL(subdomain, "hasura", httpPort, useTLS) + "/console",
"NHOST_STORAGE_URL": URL(subdomain, "storage", httpPort, useTLS) + "/v1",
"NHOST_JWT_SECRET": jwtSecret,
"NHOST_REGION": "local",
"NHOST_SUBDOMAIN": subdomain,
"NHOST_WEBHOOK_SECRET": cfg.Hasura.WebhookSecret,
"GRAPHITE_WEBHOOK_SECRET": cfg.GetAi().GetWebhookSecret(),
}
for _, envVar := range cfg.GetGlobal().GetEnvironment() {
envVars[envVar.GetName()] = envVar.GetValue()
}
return &Service{
Image: fmt.Sprintf(
"nhost/functions:%d-%s",
*cfg.GetFunctions().GetNode().Version,
functionsVersion,
),
DependsOn: nil,
EntryPoint: nil,
Command: nil,
Environment: envVars,
ExtraHosts: extraHosts,
HealthCheck: &HealthCheck{
Test: []string{"CMD", "wget", "--spider", "-S", "http://localhost:3000/healthz"},
Interval: "5s",
Timeout: "600s",
StartPeriod: "600s",
},
Labels: Ingresses{
{
Name: "functions",
TLS: useTLS,
Rule: traefikHostMatch("functions") + "&& PathPrefix(`/v1`)",
Port: functionsPort,
Rewrite: &Rewrite{
Regex: "/v1(/|$$)(.*)",
Replacement: "/$$2",
},
},
}.Labels(),
Networks: networkAliases("functions-service"),
Ports: ports(port, functionsPort),
Restart: "always",
User: nil,
Volumes: []Volume{
{
Type: "bind",
Source: rootFolder,
Target: "/opt/project",
ReadOnly: new(false),
},
{
Type: "volume",
Source: rootNodeModules(branch),
Target: "/opt/project/node_modules",
ReadOnly: new(false),
},
{
Type: "volume",
Source: functionsNodeModules(branch),
Target: "/opt/project/functions/node_modules",
ReadOnly: new(false),
},
},
WorkingDir: nil,
}, nil
}
func mailhog(volumeName string, useTLS bool) *Service {
return &Service{
Image: "nhost/mailhog:v1.0.1",
DependsOn: nil,
EntryPoint: []string{},
Command: []string{},
Environment: map[string]string{
"SMTP_HOST": "mailhog",
"SMTP_PASS": "password",
"SMTP_PORT": "1025",
"SMTP_SECURE": "false",
"SMTP_SENDER": "auth@example.com",
"SMTP_USER": "user",
},
ExtraHosts: extraHosts,
HealthCheck: nil,
Labels: Ingresses{
{
Name: "mailhog",
TLS: useTLS,
Rule: traefikHostMatch("mailhog"),
Port: mailhogPort,
Rewrite: nil,
},
}.Labels(),
Networks: nil,
Ports: nil,
Restart: "always",
User: nil,
Volumes: []Volume{
{
Type: "volume",
Source: volumeName,
Target: "/maildir",
ReadOnly: new(false),
},
},
WorkingDir: nil,
}
}
type ExposePorts struct {
Auth uint
Storage uint
Graphql uint
Console uint
Functions uint
}
func sanitizeBranch(name string) string {
re := regexp.MustCompile(`[^a-zA-Z0-9_-]`)
return strings.ToLower(re.ReplaceAllString(name, ""))
}
func IsJWTSecretCompatibleWithHasuraAuth(
jwtSecret *model.ConfigJWTSecret,
) bool {
if jwtSecret != nil && jwtSecret.Type != nil && *jwtSecret.Type != "" && jwtSecret.Key != nil &&
*jwtSecret.Key != "" {
return *jwtSecret.Type == "HS256" || *jwtSecret.Type == "HS384" ||
*jwtSecret.Type == "HS512" || *jwtSecret.Type == "RS256" ||
*jwtSecret.Type == "RS384" || *jwtSecret.Type == "RS512"
}
return false
}
func getServices( //nolint: funlen,cyclop
cfg *model.ConfigConfig,
dockerURL *url.URL,
subdomain string,
projectName string,
httpPort uint,
useTLS bool,
postgresPort uint,
nhostFolder string,
dotNhostFolder string,
rootFolder string,
ports ExposePorts,
branch string,
dashboardVersion string,
functionsVersion string,
configserviceImage string,
appID string,
startFunctions bool,
hostUser string,
runServices ...*RunService,
) (map[string]*Service, error) {
minioVolumeName := "minio_" + sanitizeBranch(branch)
minio := minio(minioVolumeName)
storage, err := storage(cfg, subdomain, useTLS, httpPort, ports.Storage)
if err != nil {
return nil, err
}
pgVolumeName := "pgdata_" + sanitizeBranch(branch)
dataFolder := filepath.Join(dotNhostFolder, "data")
postgres, err := postgres(cfg, postgresPort, dataFolder, pgVolumeName)
if err != nil {
return nil, err
}
graphql, err := graphql(
cfg,
subdomain,
useTLS,
httpPort,
ports.Graphql,
)
if err != nil {
return nil, err
}
jwtSecret := graphql.Environment["HASURA_GRAPHQL_JWT_SECRET"]
console, err := console(
cfg,
subdomain,
httpPort,
useTLS,
nhostFolder,
ports.Console,
hostUser,
)
if err != nil {
return nil, err
}
traefik, err := traefik(subdomain, projectName, httpPort, dotNhostFolder, dockerURL)
if err != nil {
return nil, err
}
mailhogVolumeName := "mailhog_" + sanitizeBranch(branch)
mailhog := mailhog(mailhogVolumeName, useTLS)
cs := configserver(
dockerURL,
configserviceImage,
rootFolder,
nhostFolder,
projectName,
appID,
useTLS,
runServices...,
)
services := map[string]*Service{
"console": console,
"dashboard": dashboard(cfg, subdomain, dashboardVersion, httpPort, useTLS, appID),
"graphql": graphql,
"minio": minio,
"postgres": postgres,
"storage": storage,
"mailhog": mailhog,
"traefik": traefik,
"configserver": cs,
}
if startFunctions {
services["functions"], err = functions(
cfg,
subdomain,
httpPort,
useTLS,
rootFolder,
jwtSecret,
ports.Functions,
branch,
functionsVersion,
)
if err != nil {
return nil, err
}
}
if cfg.GetExperimental().GetConstellation() != nil {
c, err := constellation(
cfg,
subdomain,
useTLS,
httpPort,
nhostFolder,
"nhost/constellation:"+*cfg.GetExperimental().GetConstellation().GetVersion(),
hostUser,
)
if err != nil {
return nil, err
}
services["constellation"] = c
}
if len(cfg.GetHasura().GetJwtSecrets()) > 0 &&
IsJWTSecretCompatibleWithHasuraAuth(cfg.GetHasura().GetJwtSecrets()[0]) &&
cfg.GetHasura().GetAuthHook() == nil {
auth, err := auth(cfg, subdomain, httpPort, useTLS, nhostFolder, ports.Auth)
if err != nil {
return nil, err
}
services["auth"] = auth
if cfg.Ai != nil {
services["ai"] = ai(cfg)
}
}
for _, runService := range runServices {
svc := run(runService.Config, branch)
if len(runService.BindMounts) > 0 {
svc.Volumes = append(svc.Volumes, runService.BindMounts...)
}
services["run-"+runService.Config.Name] = svc
}
return services, nil
}
type RunService struct {
Config *model.ConfigRunServiceConfig
Path string
BindMounts []Volume
}
func mountCACertificates(
path string,
services map[string]*Service,
) {
for _, service := range services {
service.Volumes = append(service.Volumes, Volume{
Type: "bind",
Source: path,
Target: "/etc/ssl/certs/ca-certificates.crt",
ReadOnly: new(true),
})
}
}
// osLinux is runtime.GOOS on Linux hosts.
const osLinux = "linux"
// hostUserSpec returns the `user: <uid>:<gid>` value that makes a
// container write host-visible files (migrations, metadata, generated
// config) as the caller instead of root, or nil when host-user mapping
// must not be applied. The mapping is resolved by ResolveHostUser.
//
// Only services that write into user-owned bind mounts should use this.
// The `functions` service must not: its Nix-built image ships a
// read-only /tmp (mode 0555) and relies on root's DAC_OVERRIDE to
// create /tmp/corepack-shims. The `configserver` service must not
// either: it bind-mounts the host Docker socket (owned by root:docker,
// reached via the caller's `docker` supplementary group), and forcing a
// primary gid drops those supplementary groups and loses socket access.
func hostUserSpec(hostUser string) *string {
if hostUser == "" {
return nil
}
return &hostUser
}
func ComposeFileFromConfig( //nolint:funlen
ctx context.Context,
cfg *model.ConfigConfig,
subdomain string,
projectName string,
httpPort uint,
useTLS bool,
postgresPort uint,
nhostFolder string,
dotNhostFolder string,
rootFolder string,
ports ExposePorts,
branch string,
dashboardVersion string,
functionsVersion string,
configserverImage string,
appID string,
startFunctions bool,
hostUser string,
caCertificatesPath string,
runServices ...*RunService,
) (*ComposeFile, error) {
dockerURL, err := resolveDockerHost(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get Docker host: %w", err)
}
services, err := getServices(
cfg,
dockerURL,
subdomain,
projectName,
httpPort,
useTLS,
postgresPort,
nhostFolder,
dotNhostFolder,
rootFolder,
ports,
branch,
dashboardVersion,
functionsVersion,
configserverImage,
appID,
startFunctions,
hostUser,
runServices...,
)
if err != nil {
return nil, err
}
volumes := map[string]struct{}{
rootNodeModules(branch): {},
"pgdata_" + sanitizeBranch(branch): {},
"minio_" + sanitizeBranch(branch): {},
"mailhog_" + sanitizeBranch(branch): {},
}
if startFunctions {
volumes[functionsNodeModules(branch)] = struct{}{}
}
for _, runService := range runServices {
for _, s := range runService.Config.GetResources().GetStorage() {
volumes[runVolumeName(runService.Config.Name, s.GetName(), branch)] = struct{}{}
}
}
if caCertificatesPath != "" {
mountCACertificates(caCertificatesPath, services)
}
return &ComposeFile{
Services: services,
Volumes: volumes,
}, nil
}