Skip to content

Commit fea8c52

Browse files
committed
refactor: docker-compose dependency support
1 parent 1e3c3f2 commit fea8c52

49 files changed

Lines changed: 1470 additions & 817 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package compose
2+
3+
import (
4+
"regexp"
5+
6+
composetypes "github.com/compose-spec/compose-go/types"
7+
"github.com/loft-sh/devspace/pkg/devspace/config/versions/latest"
8+
"github.com/loft-sh/devspace/pkg/util/log"
9+
)
10+
11+
type ConfigBuilder interface {
12+
AddDependencies(dockerCompose composetypes.Project, service composetypes.ServiceConfig) error
13+
AddDeployment(dockerCompose composetypes.Project, service composetypes.ServiceConfig) error
14+
AddDev(service composetypes.ServiceConfig) error
15+
AddImage(service composetypes.ServiceConfig) error
16+
AddSecret(dockerCompose composetypes.Project, service composetypes.ServiceConfig) error
17+
Config() *latest.Config
18+
SetName(name string)
19+
}
20+
21+
type configBuilder struct {
22+
config *latest.Config
23+
log log.Logger
24+
workingDir string
25+
}
26+
27+
func NewConfigBuilder(workingDir string, log log.Logger) ConfigBuilder {
28+
return &configBuilder{
29+
config: latest.New().(*latest.Config),
30+
log: log,
31+
workingDir: workingDir,
32+
}
33+
}
34+
35+
func (cb *configBuilder) Config() *latest.Config {
36+
return cb.config
37+
}
38+
39+
func (cb *configBuilder) SetName(name string) {
40+
cb.config.Name = name
41+
}
42+
43+
func formatName(name string) string {
44+
return regexp.MustCompile(`[\._]`).ReplaceAllString(name, "-")
45+
}
46+
47+
func labelSelector(serviceName string) map[string]string {
48+
return map[string]string{
49+
"app.kubernetes.io/component": serviceName,
50+
}
51+
}

pkg/devspace/compose/dependency.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package compose
2+
3+
import (
4+
composetypes "github.com/compose-spec/compose-go/types"
5+
"github.com/loft-sh/devspace/pkg/devspace/config/versions/latest"
6+
)
7+
8+
func (cb *configBuilder) AddDependencies(dockerCompose composetypes.Project, service composetypes.ServiceConfig) error {
9+
for _, dependency := range service.GetDependencies() {
10+
if cb.config.Dependencies == nil {
11+
cb.config.Dependencies = map[string]*latest.DependencyConfig{}
12+
}
13+
14+
depName := formatName(dependency)
15+
cb.config.Dependencies[depName] = &latest.DependencyConfig{
16+
Source: &latest.SourceConfig{
17+
Path: "devspace-" + depName + ".yaml",
18+
},
19+
}
20+
}
21+
return nil
22+
}

pkg/devspace/compose/deployment.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package compose
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"strconv"
7+
"strings"
8+
"time"
9+
10+
composetypes "github.com/compose-spec/compose-go/types"
11+
"github.com/loft-sh/devspace/pkg/devspace/config/versions/latest"
12+
v1 "k8s.io/api/core/v1"
13+
)
14+
15+
func (cb *configBuilder) AddDeployment(dockerCompose composetypes.Project, service composetypes.ServiceConfig) error {
16+
values := map[string]interface{}{}
17+
18+
volumes, volumeMounts, _ := volumesConfig(service, dockerCompose.Volumes, cb.log)
19+
if len(volumes) > 0 {
20+
values["volumes"] = volumes
21+
}
22+
23+
container, err := containerConfig(service, volumeMounts)
24+
if err != nil {
25+
return err
26+
}
27+
values["containers"] = []interface{}{container}
28+
29+
if service.Restart != "" {
30+
restartPolicy := string(v1.RestartPolicyNever)
31+
switch service.Restart {
32+
case "always":
33+
restartPolicy = string(v1.RestartPolicyAlways)
34+
case "on-failure":
35+
restartPolicy = string(v1.RestartPolicyOnFailure)
36+
}
37+
values["restartPolicy"] = restartPolicy
38+
}
39+
40+
ports := []interface{}{}
41+
if len(service.Ports) > 0 {
42+
for _, port := range service.Ports {
43+
var protocol string
44+
switch port.Protocol {
45+
case "tcp":
46+
protocol = string(v1.ProtocolTCP)
47+
case "udp":
48+
protocol = string(v1.ProtocolUDP)
49+
default:
50+
return fmt.Errorf("invalid protocol %s", port.Protocol)
51+
}
52+
53+
if port.Published == 0 {
54+
cb.log.Warnf("Unassigned port ranges are not supported: %s", port.Target)
55+
continue
56+
}
57+
58+
ports = append(ports, map[string]interface{}{
59+
"port": int(port.Published),
60+
"containerPort": int(port.Target),
61+
"protocol": protocol,
62+
})
63+
}
64+
}
65+
66+
if len(service.Expose) > 0 {
67+
for _, port := range service.Expose {
68+
intPort, err := strconv.Atoi(port)
69+
if err != nil {
70+
return fmt.Errorf("expected integer for port number: %s", err.Error())
71+
}
72+
ports = append(ports, map[string]interface{}{
73+
"port": intPort,
74+
})
75+
}
76+
}
77+
78+
if len(ports) > 0 {
79+
values["service"] = map[string]interface{}{
80+
"ports": ports,
81+
}
82+
}
83+
84+
if len(service.ExtraHosts) > 0 {
85+
hostsMap := map[string][]interface{}{}
86+
for _, host := range service.ExtraHosts {
87+
hostTokens := strings.Split(host, ":")
88+
hostName := hostTokens[0]
89+
hostIP := hostTokens[1]
90+
hostsMap[hostIP] = append(hostsMap[hostIP], hostName)
91+
}
92+
93+
hostAliases := []interface{}{}
94+
for ip, hosts := range hostsMap {
95+
hostAliases = append(hostAliases, map[string]interface{}{
96+
"ip": ip,
97+
"hostnames": hosts,
98+
})
99+
}
100+
101+
values["hostAliases"] = hostAliases
102+
}
103+
104+
deployment := &latest.DeploymentConfig{
105+
Helm: &latest.HelmConfig{
106+
Values: values,
107+
},
108+
}
109+
110+
if cb.config.Deployments == nil {
111+
cb.config.Deployments = map[string]*latest.DeploymentConfig{}
112+
}
113+
114+
deploymentName := formatName(service.Name)
115+
cb.config.Deployments[deploymentName] = deployment
116+
117+
return nil
118+
}
119+
120+
func containerConfig(service composetypes.ServiceConfig, volumeMounts []interface{}) (map[string]interface{}, error) {
121+
container := map[string]interface{}{
122+
"name": containerName(service),
123+
"image": resolveImage(service),
124+
}
125+
126+
if len(service.Command) > 0 {
127+
container["args"] = shellCommandToSlice(service.Command)
128+
}
129+
130+
if service.Build == nil && len(service.Entrypoint) > 0 {
131+
container["command"] = shellCommandToSlice(service.Entrypoint)
132+
}
133+
134+
if service.Environment != nil {
135+
env := containerEnv(service.Environment)
136+
if len(env) > 0 {
137+
container["env"] = env
138+
}
139+
}
140+
141+
if service.HealthCheck != nil {
142+
livenessProbe, err := containerLivenessProbe(service.HealthCheck)
143+
if err != nil {
144+
return nil, err
145+
}
146+
if livenessProbe != nil {
147+
container["livenessProbe"] = livenessProbe
148+
}
149+
}
150+
151+
if len(volumeMounts) > 0 {
152+
container["volumeMounts"] = volumeMounts
153+
}
154+
155+
return container, nil
156+
}
157+
158+
func containerEnv(env composetypes.MappingWithEquals) []interface{} {
159+
envs := []interface{}{}
160+
keys := []string{}
161+
for name := range env {
162+
keys = append(keys, name)
163+
}
164+
sort.Strings(keys)
165+
166+
for _, name := range keys {
167+
value := env[name]
168+
envs = append(envs, map[string]interface{}{
169+
"name": name,
170+
"value": *value,
171+
})
172+
}
173+
return envs
174+
}
175+
176+
func containerName(service composetypes.ServiceConfig) string {
177+
if service.ContainerName != "" {
178+
return formatName(service.ContainerName)
179+
}
180+
return fmt.Sprintf("%s-container", formatName(service.Name))
181+
}
182+
183+
func containerLivenessProbe(health *composetypes.HealthCheckConfig) (map[string]interface{}, error) {
184+
if len(health.Test) == 0 {
185+
return nil, nil
186+
}
187+
188+
var command []interface{}
189+
testKind := health.Test[0]
190+
switch testKind {
191+
case "NONE":
192+
return nil, nil
193+
case "CMD":
194+
for _, test := range health.Test[1:] {
195+
command = append(command, test)
196+
}
197+
case "CMD-SHELL":
198+
command = append(command, "sh")
199+
command = append(command, "-c")
200+
command = append(command, health.Test[1])
201+
default:
202+
command = append(command, health.Test[0:])
203+
}
204+
205+
livenessProbe := map[string]interface{}{
206+
"exec": map[string]interface{}{
207+
"command": command,
208+
},
209+
}
210+
211+
if health.Retries != nil {
212+
livenessProbe["failureThreshold"] = int(*health.Retries)
213+
}
214+
215+
if health.Interval != nil {
216+
period, err := time.ParseDuration(health.Interval.String())
217+
if err != nil {
218+
return nil, err
219+
}
220+
livenessProbe["periodSeconds"] = int(period.Seconds())
221+
}
222+
223+
if health.StartPeriod != nil {
224+
initialDelay, err := time.ParseDuration(health.Interval.String())
225+
if err != nil {
226+
return nil, err
227+
}
228+
livenessProbe["initialDelaySeconds"] = int(initialDelay.Seconds())
229+
}
230+
231+
return livenessProbe, nil
232+
}
233+
234+
func shellCommandToSlice(command composetypes.ShellCommand) []interface{} {
235+
var slice []interface{}
236+
for _, item := range command {
237+
slice = append(slice, item)
238+
}
239+
return slice
240+
}

0 commit comments

Comments
 (0)