1package services
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 "github.com/bnema/gordon/internal/domain"
9)
10
11type Config struct {
12 Name string `mapstructure:"name"`
13 Image string `mapstructure:"image"`
14 Enabled bool `mapstructure:"enabled"`
15 Env []string `mapstructure:"env"`
16 EnvFile string `mapstructure:"env_file"`
17 Secrets []SecretRefConfig `mapstructure:"secrets"`
18 Readiness ReadinessConfig `mapstructure:"readiness"`
19 Cleanup CleanupConfig `mapstructure:"cleanup"`
20 Ports []PortConfig `mapstructure:"ports"`
21 Volumes []VolumeConfig `mapstructure:"volumes"`
22}
23
24type PortConfig struct {
25 Name string `mapstructure:"name"`
26 Container int `mapstructure:"container"`
27 Protocol domain.NetworkProtocol `mapstructure:"protocol"`
28 Publish string `mapstructure:"publish"`
29 Private bool `mapstructure:"private"`
30 Public bool `mapstructure:"public"`
31 TrustedCIDRs []string `mapstructure:"trusted_cidrs"`
32}
33
34type VolumeConfig struct {
35 Source string `mapstructure:"source"`
36 Target string `mapstructure:"target"`
37 ReadOnly bool `mapstructure:"read_only"`
38}
39
40type ResolvedVolumeMount struct {
41 Source string
42 Target string
43 ReadOnly bool
44 Managed bool
45}
46
47type SecretRefConfig struct {
48 Name string `mapstructure:"name"`
49 Key string `mapstructure:"key"`
50}
51
52type ReadinessConfig struct {
53 Type string `mapstructure:"type"`
54 Path string `mapstructure:"path"`
55 Contains string `mapstructure:"contains"`
56 Timeout string `mapstructure:"timeout"`
57}
58
59type CleanupConfig struct {
60 PreserveVolumes *bool `mapstructure:"preserve_volumes"`
61 RemoveContainer *bool `mapstructure:"remove_container"`
62}
63
64func ToDomain(configs []Config) ([]domain.StandaloneService, error) {
65 services := make([]domain.StandaloneService, 0, len(configs))
66 seenNames := make(map[string]struct{}, len(configs))
67 seenRuntimeNames := make(map[string]string, len(configs))
68 for i, cfg := range configs {
69 svc, err := cfg.ToDomain()
70 if err != nil {
71 return nil, fmt.Errorf("service config %d: %w", i, err)
72 }
73 name := strings.TrimSpace(svc.Name)
74 if _, ok := seenNames[name]; ok {
75 return nil, fmt.Errorf("service config %d: duplicate service name %q", i, name)
76 }
77 runtimeName := serviceRuntimeIdentifier(name)
78 if previous, ok := seenRuntimeNames[runtimeName]; ok {
79 return nil, fmt.Errorf("service config %d: duplicate service name %q normalizes to same runtime identifier as %q", i, name, previous)
80 }
81 seenNames[name] = struct{}{}
82 seenRuntimeNames[runtimeName] = name
83 services = append(services, svc)
84 }
85 return services, nil
86}
87
88func (c Config) ToDomain() (domain.StandaloneService, error) {
89 if err := c.Cleanup.validate(); err != nil {
90 return domain.StandaloneService{}, err
91 }
92 readiness, err := c.readinessToDomain()
93 if err != nil {
94 return domain.StandaloneService{}, err
95 }
96 svc := domain.StandaloneService{
97 Name: c.Name,
98 Image: c.Image,
99 Enabled: c.Enabled,
100 Env: append([]string(nil), c.Env...),
101 EnvFile: c.EnvFile,
102 Secrets: secretRefsToDomain(c.Secrets),
103 Readiness: readiness,
104 Cleanup: c.Cleanup.toDomain(),
105 Ports: portsToDomain(c.Ports),
106 Volumes: volumesToDomain(c.Volumes),
107 }
108 if err := svc.Validate(); err != nil {
109 return domain.StandaloneService{}, err
110 }
111 return svc, nil
112}
113
114func (c Config) readinessToDomain() (domain.StandaloneServiceReadiness, error) {
115 readinessType := c.Readiness.Type
116 if readinessType == "" {
117 readinessType = domain.StandaloneServiceReadinessNone
118 }
119 readiness := domain.StandaloneServiceReadiness{Type: readinessType, Path: c.Readiness.Path, Contains: c.Readiness.Contains}
120 if c.Readiness.Timeout != "" {
121 timeout, err := time.ParseDuration(c.Readiness.Timeout)
122 if err != nil {
123 return domain.StandaloneServiceReadiness{}, fmt.Errorf("readiness timeout %q is invalid: %w", c.Readiness.Timeout, err)
124 }
125 if timeout <= 0 {
126 return domain.StandaloneServiceReadiness{}, fmt.Errorf("readiness timeout must be positive when set")
127 }
128 readiness.Timeout = timeout
129 readiness.TimeoutSet = true
130 }
131 return readiness, nil
132}
133
134func (c CleanupConfig) toDomain() domain.StandaloneServiceCleanup {
135 cleanup := domain.StandaloneServiceCleanup{PreserveVolumes: true, RemoveContainer: true}
136 if c.PreserveVolumes != nil {
137 cleanup.PreserveVolumes = *c.PreserveVolumes
138 }
139 if c.RemoveContainer != nil {
140 cleanup.RemoveContainer = *c.RemoveContainer
141 }
142 return cleanup
143}
144
145func (c CleanupConfig) validate() error {
146 if c.PreserveVolumes == nil || c.RemoveContainer == nil {
147 return nil
148 }
149 if !*c.PreserveVolumes && !*c.RemoveContainer {
150 return fmt.Errorf("cleanup cannot set preserve_volumes=false with remove_container=false")
151 }
152 return nil
153}
154
155func portsToDomain(configs []PortConfig) []domain.StandaloneServicePort {
156 ports := make([]domain.StandaloneServicePort, 0, len(configs))
157 for _, cfg := range configs {
158 ports = append(ports, domain.StandaloneServicePort{
159 Name: cfg.Name,
160 Container: cfg.Container,
161 Protocol: cfg.Protocol,
162 Publish: cfg.Publish,
163 Private: cfg.Private,
164 Public: cfg.Public,
165 TrustedCIDRs: append([]string(nil), cfg.TrustedCIDRs...),
166 })
167 }
168 return ports
169}
170
171func volumesToDomain(configs []VolumeConfig) []domain.StandaloneServiceVolume {
172 volumes := make([]domain.StandaloneServiceVolume, 0, len(configs))
173 for _, cfg := range configs {
174 volumes = append(volumes, domain.StandaloneServiceVolume{Source: cfg.Source, Target: cfg.Target, ReadOnly: cfg.ReadOnly})
175 }
176 return volumes
177}
178
179func secretRefsToDomain(configs []SecretRefConfig) []domain.StandaloneServiceSecretRef {
180 secrets := make([]domain.StandaloneServiceSecretRef, 0, len(configs))
181 for _, cfg := range configs {
182 secrets = append(secrets, domain.StandaloneServiceSecretRef{Name: cfg.Name, Key: cfg.Key})
183 }
184 return secrets
185}
186
187func ResolveVolumeMounts(prefix, serviceName string, explicitVolumes []domain.StandaloneServiceVolume, imageVolumePaths []string) []ResolvedVolumeMount {
188 if len(explicitVolumes) > 0 {
189 mounts := make([]ResolvedVolumeMount, 0, len(explicitVolumes))
190 for _, volume := range explicitVolumes {
191 mounts = append(mounts, ResolvedVolumeMount{Source: volume.Source, Target: volume.Target, ReadOnly: volume.ReadOnly})
192 }
193 return mounts
194 }
195
196 mounts := make([]ResolvedVolumeMount, 0, len(imageVolumePaths))
197 for _, path := range imageVolumePaths {
198 mounts = append(mounts, ResolvedVolumeMount{Source: ManagedServiceVolumeName(prefix, serviceName, path), Target: path, Managed: true})
199 }
200 return mounts
201}
202
203func ManagedServiceVolumeName(prefix, serviceName, volumePath string) string {
204 return fmt.Sprintf("%s-%s-%s",
205 prefix,
206 strings.ReplaceAll(serviceName, ".", "-"),
207 strings.ReplaceAll(strings.Trim(volumePath, "/"), "/", "-"))
208}
209
210func serviceRuntimeIdentifier(name string) string {
211 return strings.NewReplacer(".", "-", "_", "-", "/", "-").Replace(name)
212}