github.com/defang-io/defang/src@v0.0.0-20240505002154-bdf411911834/pkg/cli/composeStart.go (about) 1 package cli 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "regexp" 8 "slices" 9 "strconv" 10 "strings" 11 12 compose "github.com/compose-spec/compose-go/v2/types" 13 "github.com/defang-io/defang/src/pkg/cli/client" 14 "github.com/defang-io/defang/src/pkg/term" 15 defangv1 "github.com/defang-io/defang/src/protos/io/defang/v1" 16 ) 17 18 func convertServices(ctx context.Context, c client.Client, serviceConfigs compose.Services, force bool) ([]*defangv1.Service, error) { 19 // Create a regexp to detect private service names in environment variable values 20 var serviceNames []string 21 for _, svccfg := range serviceConfigs { 22 if network(&svccfg) == defangv1.Network_PRIVATE && slices.ContainsFunc(svccfg.Ports, func(p compose.ServicePortConfig) bool { 23 return p.Mode == "host" // only private services with host ports get DNS names 24 }) { 25 serviceNames = append(serviceNames, regexp.QuoteMeta(svccfg.Name)) 26 } 27 } 28 var serviceNameRegex *regexp.Regexp 29 if len(serviceNames) > 0 { 30 serviceNameRegex = regexp.MustCompile(`\b(?:` + strings.Join(serviceNames, "|") + `)\b`) 31 } 32 33 // 34 // Publish updates 35 // 36 var services []*defangv1.Service 37 for _, svccfg := range serviceConfigs { 38 var healthcheck *defangv1.HealthCheck 39 if svccfg.HealthCheck != nil && len(svccfg.HealthCheck.Test) > 0 && !svccfg.HealthCheck.Disable { 40 healthcheck = &defangv1.HealthCheck{ 41 Test: svccfg.HealthCheck.Test, 42 } 43 if nil != svccfg.HealthCheck.Interval { 44 healthcheck.Interval = uint32(*svccfg.HealthCheck.Interval / 1e9) 45 } 46 if nil != svccfg.HealthCheck.Timeout { 47 healthcheck.Timeout = uint32(*svccfg.HealthCheck.Timeout / 1e9) 48 } 49 if nil != svccfg.HealthCheck.Retries { 50 healthcheck.Retries = uint32(*svccfg.HealthCheck.Retries) 51 } 52 } 53 54 var deploy *defangv1.Deploy 55 if svccfg.Deploy != nil { 56 deploy = &defangv1.Deploy{} 57 deploy.Replicas = 1 // default to one replica per service; allow the user to override this to 0 58 if svccfg.Deploy.Replicas != nil { 59 deploy.Replicas = uint32(*svccfg.Deploy.Replicas) 60 } 61 62 reservations := getResourceReservations(svccfg.Deploy.Resources) 63 if reservations != nil { 64 cpus := 0.0 65 var err error 66 if reservations.NanoCPUs != "" { 67 cpus, err = strconv.ParseFloat(reservations.NanoCPUs, 32) 68 if err != nil { 69 panic(err) // was already validated above 70 } 71 } 72 var devices []*defangv1.Device 73 for _, d := range reservations.Devices { 74 devices = append(devices, &defangv1.Device{ 75 Capabilities: d.Capabilities, 76 Count: uint32(d.Count), 77 Driver: d.Driver, 78 }) 79 } 80 deploy.Resources = &defangv1.Resources{ 81 Reservations: &defangv1.Resource{ 82 Cpus: float32(cpus), 83 Memory: float32(reservations.MemoryBytes) / MiB, 84 Devices: devices, 85 }, 86 } 87 } 88 } 89 90 // Upload the build context, if any; TODO: parallelize 91 var build *defangv1.Build 92 if svccfg.Build != nil { 93 // Pack the build context into a tarball and upload 94 url, err := getRemoteBuildContext(ctx, c, svccfg.Name, svccfg.Build, force) 95 if err != nil { 96 return nil, err 97 } 98 99 build = &defangv1.Build{ 100 Context: url, 101 Dockerfile: svccfg.Build.Dockerfile, 102 ShmSize: float32(svccfg.Build.ShmSize) / MiB, 103 Target: svccfg.Build.Target, 104 } 105 106 if len(svccfg.Build.Args) > 0 { 107 build.Args = make(map[string]string) 108 for key, value := range svccfg.Build.Args { 109 if value == nil { 110 value = resolveEnv(key) 111 } 112 if value != nil { 113 build.Args[key] = *value 114 } 115 } 116 } 117 } 118 119 // Extract environment variables 120 unsetEnvs := []string{} 121 envs := make(map[string]string) 122 for key, value := range svccfg.Environment { 123 if value == nil { 124 value = resolveEnv(key) 125 } 126 127 // keep track of what environment variables were declared but not set in the compose environment section 128 if value == nil { 129 unsetEnvs = append(unsetEnvs, key) 130 continue 131 } 132 133 val := *value 134 if serviceNameRegex != nil { 135 // Replace service names with their actual DNS names 136 val = serviceNameRegex.ReplaceAllStringFunc(*value, func(serviceName string) string { 137 return c.ServiceDNS(NormalizeServiceName(serviceName)) 138 }) 139 if val != *value { 140 warnf("service names were replaced in environment variable %q: %q", key, val) 141 } 142 } 143 envs[key] = val 144 } 145 146 // Extract secret references 147 var configs []*defangv1.Secret 148 for i, secret := range svccfg.Secrets { 149 if i == 0 { 150 warnf("secrets will be exposed as environment variables, not files (use 'environment' to silence)") 151 } 152 configs = append(configs, &defangv1.Secret{ 153 Source: secret.Source, 154 }) 155 } 156 // add unset environment variables as secrets 157 for _, unsetEnv := range unsetEnvs { 158 configs = append(configs, &defangv1.Secret{ 159 Source: unsetEnv, 160 }) 161 } 162 163 init := false 164 if svccfg.Init != nil { 165 init = *svccfg.Init 166 } 167 168 var dnsRole string 169 if dnsRoleVal := svccfg.Extensions["x-defang-dns-role"]; dnsRoleVal != nil { 170 dnsRole = dnsRoleVal.(string) // already validated above 171 } 172 173 var staticFiles string 174 if staticFilesVal := svccfg.Extensions["x-defang-static-files"]; staticFilesVal != nil { 175 staticFiles = staticFilesVal.(string) // already validated above 176 } 177 178 network := network(&svccfg) 179 ports := convertPorts(svccfg.Ports) 180 services = append(services, &defangv1.Service{ 181 Name: NormalizeServiceName(svccfg.Name), 182 Image: svccfg.Image, 183 Build: build, 184 Internal: network == defangv1.Network_PRIVATE, 185 Networks: network, 186 Init: init, 187 Ports: ports, 188 Healthcheck: healthcheck, 189 Deploy: deploy, 190 Environment: envs, 191 Secrets: configs, 192 Command: svccfg.Command, 193 Domainname: svccfg.DomainName, 194 Platform: convertPlatform(svccfg.Platform), 195 DnsRole: dnsRole, 196 StaticFiles: staticFiles, 197 }) 198 } 199 return services, nil 200 } 201 202 // ComposeStart validates a compose project and uploads the services using the client 203 func ComposeStart(ctx context.Context, c client.Client, force bool) (*defangv1.DeployResponse, error) { 204 project, err := c.LoadProject() 205 if err != nil { 206 return nil, err 207 } 208 209 if err := validateProject(project); err != nil { 210 return nil, &ComposeError{err} 211 } 212 213 services, err := convertServices(ctx, c, project.Services, force) 214 if err != nil { 215 return nil, err 216 } 217 218 if len(services) == 0 { 219 return nil, &ComposeError{fmt.Errorf("no services found")} 220 } 221 222 if DoDryRun { 223 for _, service := range services { 224 PrintObject(service.Name, service) 225 } 226 return nil, ErrDryRun 227 } 228 229 for _, service := range services { 230 term.Info(" * Deploying service", service.Name) 231 } 232 233 resp, err := c.Deploy(ctx, &defangv1.DeployRequest{ 234 Services: services, 235 }) 236 if err != nil { 237 return nil, err 238 } 239 240 if term.DoDebug { 241 for _, service := range resp.Services { 242 PrintObject(service.Service.Name, service) 243 } 244 } 245 return resp, nil 246 } 247 248 func getResourceReservations(r compose.Resources) *compose.Resource { 249 if r.Reservations == nil { 250 // TODO: we might not want to default to all the limits, maybe only memory? 251 return r.Limits 252 } 253 return r.Reservations 254 } 255 256 func resolveEnv(k string) *string { 257 // TODO: per spec, if the value is nil, then the value is taken from an interactive prompt 258 v, ok := os.LookupEnv(k) 259 if !ok { 260 warnf("environment variable not found: %q", k) 261 // If the value could not be resolved, it should be removed 262 return nil 263 } 264 return &v 265 } 266 267 func convertPlatform(platform string) defangv1.Platform { 268 switch platform { 269 default: 270 warnf("Unsupported platform: %q (assuming linux)", platform) 271 fallthrough 272 case "", "linux": 273 return defangv1.Platform_LINUX_ANY 274 case "linux/amd64": 275 return defangv1.Platform_LINUX_AMD64 276 case "linux/arm64", "linux/arm64/v8", "linux/arm64/v7", "linux/arm64/v6": 277 return defangv1.Platform_LINUX_ARM64 278 } 279 } 280 281 func network(svccfg *compose.ServiceConfig) defangv1.Network { 282 // HACK: Use magic network name "public" to determine if the service is public 283 if _, ok := svccfg.Networks["public"]; ok { 284 return defangv1.Network_PUBLIC 285 } 286 // TODO: support external services (w/o LB), 287 return defangv1.Network_PRIVATE 288 }