github.com/defang-io/defang/src@v0.0.0-20240505002154-bdf411911834/pkg/cli/compose_validation.go (about) 1 package cli 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strconv" 9 "strings" 10 11 compose "github.com/compose-spec/compose-go/v2/types" 12 "github.com/defang-io/defang/src/pkg" 13 ) 14 15 func validateProject(project *compose.Project) error { 16 if project == nil { 17 return errors.New("no project found") 18 } 19 for _, svccfg := range project.Services { 20 normalized := NormalizeServiceName(svccfg.Name) 21 if !pkg.IsValidServiceName(normalized) { 22 // FIXME: this is too strict; we should allow more characters like underscores and dots 23 return fmt.Errorf("service name is invalid: %q", svccfg.Name) 24 } 25 if normalized != svccfg.Name { 26 warnf("service name %q was normalized to %q", svccfg.Name, normalized) 27 } 28 if svccfg.ReadOnly { 29 warnf("unsupported compose directive: read_only") 30 } 31 if svccfg.Restart == "" { 32 warnf("missing compose directive: restart; assuming 'unless-stopped' (add 'restart' to silence)") 33 } else if svccfg.Restart != "always" && svccfg.Restart != "unless-stopped" { 34 warnf("unsupported compose directive: restart; assuming 'unless-stopped' (add 'restart' to silence)") 35 } 36 if svccfg.ContainerName != "" { 37 warnf("unsupported compose directive: container_name") 38 } 39 if svccfg.Hostname != "" { 40 return fmt.Errorf("unsupported compose directive: hostname; consider using 'domainname' instead") 41 } 42 if len(svccfg.DNSSearch) != 0 { 43 return fmt.Errorf("unsupported compose directive: dns_search") 44 } 45 if len(svccfg.DNSOpts) != 0 { 46 warnf("unsupported compose directive: dns_opt") 47 } 48 if len(svccfg.DNS) != 0 { 49 return fmt.Errorf("unsupported compose directive: dns") 50 } 51 if len(svccfg.Devices) != 0 { 52 return fmt.Errorf("unsupported compose directive: devices") 53 } 54 if len(svccfg.DependsOn) != 0 { 55 warnf("unsupported compose directive: depends_on") 56 } 57 if len(svccfg.DeviceCgroupRules) != 0 { 58 return fmt.Errorf("unsupported compose directive: device_cgroup_rules") 59 } 60 if len(svccfg.Entrypoint) > 0 { 61 return fmt.Errorf("unsupported compose directive: entrypoint") 62 } 63 if len(svccfg.GroupAdd) > 0 { 64 return fmt.Errorf("unsupported compose directive: group_add") 65 } 66 if len(svccfg.Ipc) > 0 { 67 warnf("unsupported compose directive: ipc") 68 } 69 if len(svccfg.Uts) > 0 { 70 warnf("unsupported compose directive: uts") 71 } 72 if svccfg.Isolation != "" { 73 warnf("unsupported compose directive: isolation") 74 } 75 if svccfg.MacAddress != "" { 76 warnf("unsupported compose directive: mac_address") 77 } 78 if len(svccfg.Labels) > 0 { 79 warnf("unsupported compose directive: labels") // TODO: add support for labels 80 } 81 if len(svccfg.Links) > 0 { 82 warnf("unsupported compose directive: links") 83 } 84 if svccfg.Logging != nil { 85 warnf("unsupported compose directive: logging") 86 } 87 for name := range svccfg.Networks { 88 if _, ok := project.Networks[name]; !ok { 89 warnf("network %v used by service %v is not defined in the top-level networks section", name, svccfg.Name) 90 } 91 } 92 if len(svccfg.Volumes) > 0 { 93 warnf("unsupported compose directive: volumes") // TODO: add support for volumes 94 } 95 if len(svccfg.VolumesFrom) > 0 { 96 warnf("unsupported compose directive: volumes_from") // TODO: add support for volumes_from 97 } 98 if svccfg.Build != nil { 99 if svccfg.Build.Dockerfile != "" { 100 if filepath.IsAbs(svccfg.Build.Dockerfile) { 101 return fmt.Errorf("dockerfile path must be relative to the build context: %q", svccfg.Build.Dockerfile) 102 } 103 if strings.HasPrefix(svccfg.Build.Dockerfile, "../") { 104 return fmt.Errorf("dockerfile path must be inside the build context: %q", svccfg.Build.Dockerfile) 105 } 106 // Check if the dockerfile exists 107 dockerfilePath := filepath.Join(svccfg.Build.Context, svccfg.Build.Dockerfile) 108 if _, err := os.Stat(dockerfilePath); err != nil { 109 return fmt.Errorf("dockerfile not found: %q", dockerfilePath) 110 } 111 } 112 if svccfg.Build.SSH != nil { 113 return fmt.Errorf("unsupported compose directive: build ssh") 114 } 115 if len(svccfg.Build.Labels) != 0 { 116 warnf("unsupported compose directive: build labels") // TODO: add support for Kaniko --label 117 } 118 if len(svccfg.Build.CacheFrom) != 0 { 119 warnf("unsupported compose directive: build cache_from") 120 } 121 if len(svccfg.Build.CacheTo) != 0 { 122 warnf("unsupported compose directive: build cache_to") 123 } 124 if svccfg.Build.NoCache { 125 warnf("unsupported compose directive: build no_cache") 126 } 127 if len(svccfg.Build.ExtraHosts) != 0 { 128 return fmt.Errorf("unsupported compose directive: build extra_hosts") 129 } 130 if svccfg.Build.Isolation != "" { 131 warnf("unsupported compose directive: build isolation") 132 } 133 if svccfg.Build.Network != "" { 134 return fmt.Errorf("unsupported compose directive: build network") 135 } 136 if len(svccfg.Build.Secrets) != 0 { 137 return fmt.Errorf("unsupported compose directive: build secrets") // TODO: support build secrets 138 } 139 if len(svccfg.Build.Tags) != 0 { 140 return fmt.Errorf("unsupported compose directive: build tags") 141 } 142 if len(svccfg.Build.Platforms) != 0 { 143 return fmt.Errorf("unsupported compose directive: build platforms") 144 } 145 if svccfg.Build.Privileged { 146 return fmt.Errorf("unsupported compose directive: build privileged") 147 } 148 if svccfg.Build.DockerfileInline != "" { 149 return fmt.Errorf("unsupported compose directive: build dockerfile_inline") 150 } 151 } 152 for _, secret := range svccfg.Secrets { 153 if !pkg.IsValidSecretName(secret.Source) { 154 return fmt.Errorf("secret name is invalid: %q", secret.Source) 155 } 156 if secret.Target != "" { 157 return fmt.Errorf("unsupported compose directive: secret target") 158 } 159 if s, ok := project.Secrets[secret.Source]; !ok { 160 warnf("secret %q is not defined in the top-level secrets section", secret.Source) 161 } else if s.Name != "" && s.Name != secret.Source { 162 return fmt.Errorf("unsupported secret %q: cannot override name %q", secret.Source, s.Name) // TODO: support custom secret names 163 } else if !s.External { 164 warnf("unsupported secret %q: not marked external:true", secret.Source) // TODO: support secrets from environment/file 165 } 166 } 167 err := validatePorts(svccfg.Ports) 168 if err != nil { 169 return err 170 } 171 if svccfg.HealthCheck == nil || svccfg.HealthCheck.Disable { 172 // Show a warning when we have ingress ports but no explicit healthcheck 173 for _, port := range svccfg.Ports { 174 if port.Mode == "ingress" { 175 warnf("ingress port without healthcheck defaults to GET / HTTP/1.1") 176 break 177 } 178 } 179 } else { 180 timeout := 30 // default per compose spec 181 if svccfg.HealthCheck.Timeout != nil { 182 if *svccfg.HealthCheck.Timeout%1e9 != 0 { 183 warnf("healthcheck timeout must be a multiple of 1s") 184 } 185 timeout = int(*svccfg.HealthCheck.Timeout / 1e9) 186 } 187 interval := 30 // default per compose spec 188 if svccfg.HealthCheck.Interval != nil { 189 if *svccfg.HealthCheck.Interval%1e9 != 0 { 190 warnf("healthcheck interval must be a multiple of 1s") 191 } 192 interval = int(*svccfg.HealthCheck.Interval / 1e9) 193 } 194 // Technically this should test for <= but both interval and timeout have 30s as the default value 195 if interval < timeout || timeout <= 0 { 196 return fmt.Errorf("healthcheck timeout %ds must be positive and smaller than the interval %ds", timeout, interval) 197 } 198 if svccfg.HealthCheck.StartPeriod != nil { 199 warnf("unsupported compose directive: healthcheck start_period") 200 } 201 if svccfg.HealthCheck.StartInterval != nil { 202 warnf("unsupported compose directive: healthcheck start_interval") 203 } 204 } 205 if svccfg.Deploy != nil { 206 if svccfg.Deploy.Mode != "" && svccfg.Deploy.Mode != "replicated" { 207 return fmt.Errorf("unsupported compose directive: deploy mode: %q", svccfg.Deploy.Mode) 208 } 209 if len(svccfg.Deploy.Labels) > 0 { 210 warnf("unsupported compose directive: deploy labels") 211 } 212 if svccfg.Deploy.UpdateConfig != nil { 213 return fmt.Errorf("unsupported compose directive: deploy update_config") 214 } 215 if svccfg.Deploy.RollbackConfig != nil { 216 return fmt.Errorf("unsupported compose directive: deploy rollback_config") 217 } 218 if svccfg.Deploy.RestartPolicy != nil { 219 return fmt.Errorf("unsupported compose directive: deploy restart_policy") 220 } 221 if len(svccfg.Deploy.Placement.Constraints) != 0 || len(svccfg.Deploy.Placement.Preferences) != 0 || svccfg.Deploy.Placement.MaxReplicas != 0 { 222 warnf("unsupported compose directive: deploy placement") 223 } 224 if svccfg.Deploy.EndpointMode != "" { 225 return fmt.Errorf("unsupported compose directive: deploy endpoint_mode") 226 } 227 if svccfg.Deploy.Resources.Limits != nil && svccfg.Deploy.Resources.Reservations == nil { 228 warnf("no reservations specified; using limits as reservations") 229 } 230 reservations := getResourceReservations(svccfg.Deploy.Resources) 231 if reservations != nil && reservations.NanoCPUs != "" { 232 cpus, err := strconv.ParseFloat(reservations.NanoCPUs, 32) 233 if err != nil || cpus < 0 { // "0" just means "as small as possible" 234 return fmt.Errorf("invalid value for cpus: %q", reservations.NanoCPUs) 235 } 236 } 237 } 238 var reservations *compose.Resource 239 if svccfg.Deploy != nil { 240 reservations = getResourceReservations(svccfg.Deploy.Resources) 241 } 242 243 if svccfg.Deploy == nil || reservations == nil || reservations.MemoryBytes == 0 { 244 warnf("missing memory reservation; specify deploy.resources.reservations.memory to avoid out-of-memory errors") 245 } 246 247 if dnsRoleVal := svccfg.Extensions["x-defang-dns-role"]; dnsRoleVal != nil { 248 if _, ok := dnsRoleVal.(string); !ok { 249 return fmt.Errorf("x-defang-dns-role must be a string") 250 } 251 } 252 253 if staticFilesVal := svccfg.Extensions["x-defang-static-files"]; staticFilesVal != nil { 254 if _, ok := staticFilesVal.(string); !ok { 255 return fmt.Errorf("x-defang-static-files must be a string") 256 } 257 } 258 } 259 return nil 260 }