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  }