Introduce `Normalize` and `Check` in compose model lifecycle

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2020-05-19 15:27:11 +02:00
parent a5a925173c
commit 08bd18231d
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
8 changed files with 311 additions and 42 deletions

42
ecs/pkg/amazon/check.go Normal file
View File

@ -0,0 +1,42 @@
package amazon
import (
"github.com/compose-spec/compose-go/types"
"github.com/docker/ecs-plugin/pkg/compose"
)
type Warning string
type Warnings []string
type CompatibilityChecker interface {
CheckService(service *types.ServiceConfig)
CheckCapAdd(service *types.ServiceConfig)
CheckDependsOn(service *types.ServiceConfig)
CheckDNS(service *types.ServiceConfig)
CheckDNSOpts(service *types.ServiceConfig)
CheckDNSSearch(service *types.ServiceConfig)
CheckDomainName(service *types.ServiceConfig)
CheckExtraHosts(service *types.ServiceConfig)
CheckHostname(service *types.ServiceConfig)
CheckIpc(service *types.ServiceConfig)
CheckLabels(service *types.ServiceConfig)
CheckLinks(service *types.ServiceConfig)
CheckLogging(service *types.ServiceConfig)
CheckMacAddress(service *types.ServiceConfig)
CheckNetworkMode(service *types.ServiceConfig)
CheckPid(service *types.ServiceConfig)
CheckSysctls(service *types.ServiceConfig)
CheckTmpfs(service *types.ServiceConfig)
CheckUserNSMode(service *types.ServiceConfig)
Errors() []error
}
// Check the compose model do not use unsupported features and inject sane defaults for ECS deployment
func Check(project *compose.Project) []error {
c := FargateCompatibilityChecker{}
for i, service := range project.Services {
c.CheckService(&service)
project.Services[i] = service
}
return c.errors
}

View File

@ -8,6 +8,6 @@ import (
func TestInvalidNetworkMode(t *testing.T) {
project := load(t, "testdata/invalid_network_mode.yaml")
err := Validate(project)
assert.Error(t, err, "ECS do not support NetworkMode \"bridge\"")
err := Check(project)
assert.Error(t, err[0], "'network_mode' \"bridge\" is not supported")
}

View File

@ -4,6 +4,8 @@ import (
"fmt"
"strings"
"github.com/sirupsen/logrus"
cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
@ -26,9 +28,9 @@ const (
// Convert a compose project into a CloudFormation template
func (c client) Convert(project *compose.Project) (*cloudformation.Template, error) {
err := Validate(project)
if err != nil {
return nil, err
warnings := Check(project)
for _, w := range warnings {
logrus.Warn(w)
}
template := cloudformation.NewTemplate()

View File

@ -0,0 +1,179 @@
package amazon
import (
"fmt"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/compose-spec/compose-go/types"
)
type FargateCompatibilityChecker struct {
errors []error
}
func (c *FargateCompatibilityChecker) error(message string, args ...interface{}) {
c.errors = append(c.errors, fmt.Errorf(message, args...))
}
func (c *FargateCompatibilityChecker) Errors() []error {
return c.errors
}
func (c *FargateCompatibilityChecker) CheckService(service *types.ServiceConfig) {
c.CheckCapAdd(service)
c.CheckDependsOn(service)
c.CheckDNS(service)
c.CheckDNSOpts(service)
c.CheckDNSSearch(service)
c.CheckDomainName(service)
c.CheckExtraHosts(service)
c.CheckHostname(service)
c.CheckIpc(service)
c.CheckLabels(service)
c.CheckLinks(service)
c.CheckLogging(service)
c.CheckMacAddress(service)
c.CheckNetworkMode(service)
c.CheckPid(service)
c.CheckSysctls(service)
c.CheckTmpfs(service)
c.CheckUserNSMode(service)
}
func (c *FargateCompatibilityChecker) CheckNetworkMode(service *types.ServiceConfig) {
if service.NetworkMode != "" && service.NetworkMode != ecs.NetworkModeAwsvpc {
c.error("'network_mode' %q is not supported", service.NetworkMode)
}
service.NetworkMode = ecs.NetworkModeAwsvpc
}
func (c *FargateCompatibilityChecker) CheckDependsOn(service *types.ServiceConfig) {
if len(service.DependsOn) != 0 {
c.error("'depends_on' is not supported")
service.DependsOn = nil
}
}
func (c *FargateCompatibilityChecker) CheckLinks(service *types.ServiceConfig) {
if len(service.Links) != 0 {
c.error("'links' is not supported")
service.Links = nil
}
}
func (c *FargateCompatibilityChecker) CheckLogging(service *types.ServiceConfig) {
c.CheckLoggingDriver(service)
}
func (c *FargateCompatibilityChecker) CheckLoggingDriver(service *types.ServiceConfig) {
if service.LogDriver != "" && service.LogDriver != ecs.LogDriverAwslogs {
c.error("'log_driver' %q is not supported", service.LogDriver)
service.LogDriver = ecs.LogDriverAwslogs
}
}
func (c *FargateCompatibilityChecker) CheckPid(service *types.ServiceConfig) {
if service.Pid != "" {
c.error("'pid' is not supported")
service.Pid = ""
}
}
func (c *FargateCompatibilityChecker) CheckUserNSMode(service *types.ServiceConfig) {
if service.UserNSMode != "" {
c.error("'userns_mode' is not supported")
service.UserNSMode = ""
}
}
func (c *FargateCompatibilityChecker) CheckIpc(service *types.ServiceConfig) {
if service.Ipc != "" {
c.error("'ipc' is not supported")
service.Ipc = ""
}
}
func (c *FargateCompatibilityChecker) CheckMacAddress(service *types.ServiceConfig) {
if service.MacAddress != "" {
c.error("'mac_address' is not supported")
service.MacAddress = ""
}
}
func (c *FargateCompatibilityChecker) CheckHostname(service *types.ServiceConfig) {
if service.Hostname != "" {
c.error("'hostname' is not supported")
service.Hostname = ""
}
}
func (c *FargateCompatibilityChecker) CheckDomainName(service *types.ServiceConfig) {
if service.DomainName != "" {
c.error("'domainname' is not supported")
service.DomainName = ""
}
}
func (c *FargateCompatibilityChecker) CheckDNSSearch(service *types.ServiceConfig) {
if len(service.DNSSearch) > 0 {
c.error("'dns_search' is not supported")
service.DNSSearch = nil
}
}
func (c *FargateCompatibilityChecker) CheckDNS(service *types.ServiceConfig) {
if len(service.DNS) > 0 {
c.error("'dns' is not supported")
service.DNS = nil
}
}
func (c *FargateCompatibilityChecker) CheckDNSOpts(service *types.ServiceConfig) {
if len(service.DNSOpts) > 0 {
c.error("'dns_opt' is not supported")
service.DNSOpts = nil
}
}
func (c *FargateCompatibilityChecker) CheckExtraHosts(service *types.ServiceConfig) {
if len(service.ExtraHosts) > 0 {
c.error("'extra_hosts' is not supported")
service.ExtraHosts = nil
}
}
func (c *FargateCompatibilityChecker) CheckCapAdd(service *types.ServiceConfig) {
for i, v := range service.CapAdd {
if v != "SYS_PTRACE" {
c.error("'cap_add' %s is not supported", v)
l := len(service.CapAdd)
service.CapAdd[i] = service.CapAdd[l-1]
service.CapAdd = service.CapAdd[:l-1]
}
}
}
func (c *FargateCompatibilityChecker) CheckTmpfs(service *types.ServiceConfig) {
if len(service.Tmpfs) > 0 {
c.error("'tmpfs' is not supported")
service.Tmpfs = nil
}
}
func (c *FargateCompatibilityChecker) CheckSysctls(service *types.ServiceConfig) {
if len(service.Sysctls) > 0 {
c.error("'sysctls' is not supported")
service.Sysctls = nil
}
}
func (c *FargateCompatibilityChecker) CheckLabels(service *types.ServiceConfig) {
for k, v := range service.Labels {
if v == "" {
c.error("'labels' with an empty value is not supported")
delete(service.Labels, k)
}
}
}
var _ CompatibilityChecker = &FargateCompatibilityChecker{}

View File

@ -10,6 +10,7 @@ import (
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
"github.com/awslabs/goformation/v4/cloudformation"
"github.com/awslabs/goformation/v4/cloudformation/ecs"
"github.com/awslabs/goformation/v4/cloudformation/tags"
"github.com/compose-spec/compose-go/types"
"github.com/docker/cli/opts"
"github.com/docker/ecs-plugin/pkg/compose"
@ -68,7 +69,7 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe
ResourceRequirements: nil,
StartTimeout: 0,
StopTimeout: durationToInt(service.StopGracePeriod),
SystemControls: nil,
SystemControls: toSystemControls(service.Sysctls),
Ulimits: toUlimits(service.Ulimits),
User: service.User,
VolumesFrom: nil,
@ -84,10 +85,32 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe
PlacementConstraints: toPlacementConstraints(service.Deploy),
ProxyConfiguration: nil,
RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate},
Tags: nil,
Tags: toTags(service.Labels),
}, nil
}
func toTags(labels types.Labels) []tags.Tag {
t := []tags.Tag{}
for n, v := range labels {
t = append(t, tags.Tag{
Key: n,
Value: v,
})
}
return t
}
func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl {
sys := []ecs.TaskDefinition_SystemControl{}
for k, v := range sysctls {
sys = append(sys, ecs.TaskDefinition_SystemControl{
Namespace: k,
Value: v,
})
}
return sys
}
func toLimits(service types.ServiceConfig) (string, string, error) {
// All possible cpu/mem values for Fargate
cpuToMem := map[int64][]types.UnitBytes{

View File

@ -1,35 +0,0 @@
package amazon
import (
"fmt"
"github.com/compose-spec/compose-go/types"
"github.com/docker/ecs-plugin/pkg/compose"
)
// Validate check the compose model do not use unsupported features and inject sane defaults for ECS deployment
func Validate(project *compose.Project) error {
if len(project.Networks) == 0 {
// Compose application model implies a default network if none is explicitly set.
// FIXME move this to compose-go
project.Networks["default"] = types.NetworkConfig{
Name: "default",
}
}
for i, service := range project.Services {
if len(service.Networks) == 0 {
// Service without explicit network attachment are implicitly exposed on default network
// FIXME move this to compose-go
service.Networks = map[string]*types.ServiceNetworkConfig{"default": nil}
project.Services[i] = service
}
if service.NetworkMode != "" && service.NetworkMode != "awsvpc" {
return fmt.Errorf("ECS do not support NetworkMode %q", service.NetworkMode)
}
}
// Here we can check for incompatible attributes, inject sane defaults, etc
return nil
}

View File

@ -0,0 +1,53 @@
package compose
import (
"fmt"
"github.com/compose-spec/compose-go/types"
"github.com/sirupsen/logrus"
)
// Normalize a compose-go model to move deprecated attributes to canonical position, and introduce implicit defaults
// FIXME move this to compose-go
func Normalize(model *types.Config) error {
if len(model.Networks) == 0 {
// Compose application model implies a default network if none is explicitly set.
model.Networks["default"] = types.NetworkConfig{
Name: "default",
}
}
for i, s := range model.Services {
if len(s.Networks) == 0 {
// Service without explicit network attachment are implicitly exposed on default network
s.Networks = map[string]*types.ServiceNetworkConfig{"default": nil}
}
if s.LogDriver != "" {
logrus.Warn("`log_driver` is deprecated. Use the `logging` attribute")
if s.Logging == nil {
s.Logging = &types.LoggingConfig{}
}
if s.Logging.Driver == "" {
s.Logging.Driver = s.LogDriver
} else {
return fmt.Errorf("can't use both 'log_driver' (deprecated) and 'logging.driver'")
}
}
if len(s.LogOpt) != 0 {
logrus.Warn("`log_opts` is deprecated. Use the `logging` attribute")
if s.Logging == nil {
s.Logging = &types.LoggingConfig{}
}
for k, v := range s.LogOpt {
if _, ok := s.Logging.Options[k]; !ok {
s.Logging.Options[k] = v
} else {
return fmt.Errorf("can't use both 'log_opt' (deprecated) and 'logging.options'")
}
}
}
model.Services[i] = s
}
return nil
}

View File

@ -25,6 +25,11 @@ func NewProject(config types.ConfigDetails, name string) (*Project, error) {
return nil, err
}
err = Normalize(model)
if err != nil {
return nil, err
}
p := Project{
Config: *model,
projectDir: config.WorkingDir,