forked from LaconicNetwork/kompose
Add support for placement preferences docker-compose v3.3+ (#1425)
This commit is contained in:
parent
c921643705
commit
8cb1b0599e
@ -216,6 +216,7 @@ type Volumes struct {
|
|||||||
type Placement struct {
|
type Placement struct {
|
||||||
PositiveConstraints map[string]string
|
PositiveConstraints map[string]string
|
||||||
NegativeConstraints map[string]string
|
NegativeConstraints map[string]string
|
||||||
|
Preferences []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigMapKeyFromMeta ...
|
// GetConfigMapKeyFromMeta ...
|
||||||
|
|||||||
@ -595,6 +595,11 @@ func TestCheckPlacementCustomLabels(t *testing.T) {
|
|||||||
"node.labels.something == anything",
|
"node.labels.something == anything",
|
||||||
"node.labels.monitor != xxx",
|
"node.labels.monitor != xxx",
|
||||||
},
|
},
|
||||||
|
Preferences: []types.PlacementPreferences{
|
||||||
|
{Spread: "node.labels.zone"},
|
||||||
|
{Spread: "foo"},
|
||||||
|
{Spread: "node.labels.ssd"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
output := loadV3Placement(placement)
|
output := loadV3Placement(placement)
|
||||||
|
|
||||||
@ -605,10 +610,22 @@ func TestCheckPlacementCustomLabels(t *testing.T) {
|
|||||||
NegativeConstraints: map[string]string{
|
NegativeConstraints: map[string]string{
|
||||||
"monitor": "xxx",
|
"monitor": "xxx",
|
||||||
},
|
},
|
||||||
|
Preferences: []string{
|
||||||
|
"zone", "ssd",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
checkConstraints(t, "positive", output.PositiveConstraints, expected.PositiveConstraints)
|
checkConstraints(t, "positive", output.PositiveConstraints, expected.PositiveConstraints)
|
||||||
checkConstraints(t, "negative", output.NegativeConstraints, expected.NegativeConstraints)
|
checkConstraints(t, "negative", output.NegativeConstraints, expected.NegativeConstraints)
|
||||||
|
|
||||||
|
if len(output.Preferences) != len(expected.Preferences) {
|
||||||
|
t.Errorf("preferences len is not equal, expected %d, got %d", len(expected.Preferences), len(output.Preferences))
|
||||||
|
}
|
||||||
|
for i := range output.Preferences {
|
||||||
|
if output.Preferences[i] != expected.Preferences[i] {
|
||||||
|
t.Errorf("preference is not equal, expected %s, got %s", expected.Preferences[i], output.Preferences[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkConstraints(t *testing.T, caseName string, output, expected map[string]string) {
|
func checkConstraints(t *testing.T, caseName string, output, expected map[string]string) {
|
||||||
|
|||||||
@ -137,9 +137,11 @@ func loadV3Placement(placement types.Placement) kobject.Placement {
|
|||||||
komposePlacement := kobject.Placement{
|
komposePlacement := kobject.Placement{
|
||||||
PositiveConstraints: make(map[string]string),
|
PositiveConstraints: make(map[string]string),
|
||||||
NegativeConstraints: make(map[string]string),
|
NegativeConstraints: make(map[string]string),
|
||||||
|
Preferences: make([]string, 0, len(placement.Preferences)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert constraints
|
||||||
equal, notEqual := " == ", " != "
|
equal, notEqual := " == ", " != "
|
||||||
errMsg := " constraints in placement is not supported, only 'node.hostname', 'engine.labels.operatingsystem' and 'node.labels.xxx' (ex: node.labels.something == anything) is supported as a constraint "
|
|
||||||
for _, j := range placement.Constraints {
|
for _, j := range placement.Constraints {
|
||||||
operator := equal
|
operator := equal
|
||||||
if strings.Contains(j, notEqual) {
|
if strings.Contains(j, notEqual) {
|
||||||
@ -147,19 +149,13 @@ func loadV3Placement(placement types.Placement) kobject.Placement {
|
|||||||
}
|
}
|
||||||
p := strings.Split(j, operator)
|
p := strings.Split(j, operator)
|
||||||
if len(p) < 2 {
|
if len(p) < 2 {
|
||||||
log.Warn(p[0], errMsg)
|
log.Warnf("Failed to parse placement constraints %s, the correct format is 'label == xxx'", j)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var key string
|
key, err := convertDockerLabel(p[0])
|
||||||
if p[0] == "node.hostname" {
|
if err != nil {
|
||||||
key = "kubernetes.io/hostname"
|
log.Warn("Ignore placement constraints: ", err.Error())
|
||||||
} else if p[0] == "engine.labels.operatingsystem" {
|
|
||||||
key = "beta.kubernetes.io/os"
|
|
||||||
} else if strings.HasPrefix(p[0], "node.labels.") {
|
|
||||||
key = strings.TrimPrefix(p[0], "node.labels.")
|
|
||||||
} else {
|
|
||||||
log.Warn(p[0], errMsg)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,9 +165,36 @@ func loadV3Placement(placement types.Placement) kobject.Placement {
|
|||||||
komposePlacement.NegativeConstraints[key] = p[1]
|
komposePlacement.NegativeConstraints[key] = p[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert preferences
|
||||||
|
for _, p := range placement.Preferences {
|
||||||
|
// Spread is the only supported strategy currently
|
||||||
|
label, err := convertDockerLabel(p.Spread)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Ignore placement preferences: ", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
komposePlacement.Preferences = append(komposePlacement.Preferences, label)
|
||||||
|
}
|
||||||
return komposePlacement
|
return komposePlacement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert docker label to k8s label
|
||||||
|
func convertDockerLabel(dockerLabel string) (string, error) {
|
||||||
|
switch dockerLabel {
|
||||||
|
case "node.hostname":
|
||||||
|
return "kubernetes.io/hostname", nil
|
||||||
|
case "engine.labels.operatingsystem":
|
||||||
|
return "kubernetes.io/os", nil
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(dockerLabel, "node.labels.") {
|
||||||
|
return strings.TrimPrefix(dockerLabel, "node.labels."), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errMsg := fmt.Sprint(dockerLabel, " is not supported, only 'node.hostname', 'engine.labels.operatingsystem' and 'node.labels.xxx' (ex: node.labels.something == anything) is supported")
|
||||||
|
return "", errors.New(errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
// Convert the Docker Compose v3 volumes to []string (the old way)
|
// Convert the Docker Compose v3 volumes to []string (the old way)
|
||||||
// TODO: Check to see if it's a "bind" or "volume". Ignore for now.
|
// TODO: Check to see if it's a "bind" or "volume". Ignore for now.
|
||||||
// TODO: Refactor it similar to loadV3Ports
|
// TODO: Refactor it similar to loadV3Ports
|
||||||
|
|||||||
@ -534,6 +534,7 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic
|
|||||||
template.Spec.Volumes = append(template.Spec.Volumes, volumes...)
|
template.Spec.Volumes = append(template.Spec.Volumes, volumes...)
|
||||||
}
|
}
|
||||||
template.Spec.Affinity = ConfigAffinity(service)
|
template.Spec.Affinity = ConfigAffinity(service)
|
||||||
|
template.Spec.TopologySpreadConstraints = ConfigTopologySpreadConstraints(service)
|
||||||
// Configure the HealthCheck
|
// Configure the HealthCheck
|
||||||
template.Spec.Containers[0].LivenessProbe = configProbe(service.HealthChecks.Liveness)
|
template.Spec.Containers[0].LivenessProbe = configProbe(service.HealthChecks.Liveness)
|
||||||
template.Spec.Containers[0].ReadinessProbe = configProbe(service.HealthChecks.Readiness)
|
template.Spec.Containers[0].ReadinessProbe = configProbe(service.HealthChecks.Readiness)
|
||||||
|
|||||||
@ -29,8 +29,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/tools/godoc/util"
|
|
||||||
|
|
||||||
"github.com/fatih/structs"
|
"github.com/fatih/structs"
|
||||||
"github.com/kubernetes/kompose/pkg/kobject"
|
"github.com/kubernetes/kompose/pkg/kobject"
|
||||||
"github.com/kubernetes/kompose/pkg/loader/compose"
|
"github.com/kubernetes/kompose/pkg/loader/compose"
|
||||||
@ -40,6 +38,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
|
"golang.org/x/tools/godoc/util"
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
api "k8s.io/api/core/v1"
|
api "k8s.io/api/core/v1"
|
||||||
networkingv1 "k8s.io/api/networking/v1"
|
networkingv1 "k8s.io/api/networking/v1"
|
||||||
@ -1073,22 +1072,52 @@ func ConfigEnvs(service kobject.ServiceConfig, opt kobject.ConvertOptions) ([]ap
|
|||||||
|
|
||||||
// ConfigAffinity configures the Affinity.
|
// ConfigAffinity configures the Affinity.
|
||||||
func ConfigAffinity(service kobject.ServiceConfig) *api.Affinity {
|
func ConfigAffinity(service kobject.ServiceConfig) *api.Affinity {
|
||||||
|
var affinity *api.Affinity
|
||||||
|
// Config constraints
|
||||||
|
// Convert constraints to requiredDuringSchedulingIgnoredDuringExecution
|
||||||
positiveConstraints := configConstrains(service.Placement.PositiveConstraints, api.NodeSelectorOpIn)
|
positiveConstraints := configConstrains(service.Placement.PositiveConstraints, api.NodeSelectorOpIn)
|
||||||
negativeConstraints := configConstrains(service.Placement.NegativeConstraints, api.NodeSelectorOpNotIn)
|
negativeConstraints := configConstrains(service.Placement.NegativeConstraints, api.NodeSelectorOpNotIn)
|
||||||
if len(positiveConstraints) == 0 && len(negativeConstraints) == 0 {
|
if len(positiveConstraints) != 0 || len(negativeConstraints) != 0 {
|
||||||
return nil
|
affinity = &api.Affinity{
|
||||||
}
|
NodeAffinity: &api.NodeAffinity{
|
||||||
return &api.Affinity{
|
RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{
|
||||||
NodeAffinity: &api.NodeAffinity{
|
NodeSelectorTerms: []api.NodeSelectorTerm{
|
||||||
RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{
|
{
|
||||||
NodeSelectorTerms: []api.NodeSelectorTerm{
|
MatchExpressions: append(positiveConstraints, negativeConstraints...),
|
||||||
{
|
},
|
||||||
MatchExpressions: append(positiveConstraints, negativeConstraints...),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
return affinity
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigTopologySpreadConstraints configures the TopologySpreadConstraints.
|
||||||
|
func ConfigTopologySpreadConstraints(service kobject.ServiceConfig) []api.TopologySpreadConstraint {
|
||||||
|
preferencesLen := len(service.Placement.Preferences)
|
||||||
|
constraints := make([]api.TopologySpreadConstraint, 0, preferencesLen)
|
||||||
|
|
||||||
|
// Placement preferences are ignored for global services
|
||||||
|
if service.DeployMode == "global" {
|
||||||
|
log.Warnf("Ignore placement preferences for global service %s", service.Name)
|
||||||
|
return constraints
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range service.Placement.Preferences {
|
||||||
|
constraints = append(constraints, api.TopologySpreadConstraint{
|
||||||
|
// According to the order of preferences, the MaxSkew decreases in order
|
||||||
|
// The minimum value is 1
|
||||||
|
MaxSkew: int32(preferencesLen - i),
|
||||||
|
TopologyKey: p,
|
||||||
|
WhenUnsatisfiable: api.ScheduleAnyway,
|
||||||
|
LabelSelector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: transformer.ConfigLabels(service.Name),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
func configConstrains(constrains map[string]string, operator api.NodeSelectorOperator) []api.NodeSelectorRequirement {
|
func configConstrains(constrains map[string]string, operator api.NodeSelectorOperator) []api.NodeSelectorRequirement {
|
||||||
@ -1404,6 +1433,7 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject.
|
|||||||
ResourcesLimits(service),
|
ResourcesLimits(service),
|
||||||
ResourcesRequests(service),
|
ResourcesRequests(service),
|
||||||
TerminationGracePeriodSeconds(name, service),
|
TerminationGracePeriodSeconds(name, service),
|
||||||
|
TopologySpreadConstraints(service),
|
||||||
)
|
)
|
||||||
|
|
||||||
if serviceAccountName, ok := service.Labels[compose.LabelServiceAccountName]; ok {
|
if serviceAccountName, ok := service.Labels[compose.LabelServiceAccountName]; ok {
|
||||||
|
|||||||
@ -668,6 +668,51 @@ func TestConfigAffinity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigTopologySpreadConstraints(t *testing.T) {
|
||||||
|
serviceName := "app"
|
||||||
|
testCases := map[string]struct {
|
||||||
|
service kobject.ServiceConfig
|
||||||
|
result []api.TopologySpreadConstraint
|
||||||
|
}{
|
||||||
|
"ConfigTopologySpreadConstraint": {
|
||||||
|
service: kobject.ServiceConfig{
|
||||||
|
Name: serviceName,
|
||||||
|
Placement: kobject.Placement{
|
||||||
|
Preferences: []string{
|
||||||
|
"zone", "ssd",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: []api.TopologySpreadConstraint{
|
||||||
|
{
|
||||||
|
MaxSkew: 2,
|
||||||
|
TopologyKey: "zone",
|
||||||
|
WhenUnsatisfiable: api.ScheduleAnyway,
|
||||||
|
LabelSelector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: transformer.ConfigLabels(serviceName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MaxSkew: 1,
|
||||||
|
TopologyKey: "ssd",
|
||||||
|
WhenUnsatisfiable: api.ScheduleAnyway,
|
||||||
|
LabelSelector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: transformer.ConfigLabels(serviceName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range testCases {
|
||||||
|
t.Log("Test case:", name)
|
||||||
|
result := ConfigTopologySpreadConstraints(test.service)
|
||||||
|
if !reflect.DeepEqual(result, test.result) {
|
||||||
|
t.Errorf("Not expected result for ConfigTopologySpreadConstraints")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMultipleContainersInPod(t *testing.T) {
|
func TestMultipleContainersInPod(t *testing.T) {
|
||||||
groupName := "pod_group"
|
groupName := "pod_group"
|
||||||
|
|
||||||
|
|||||||
@ -306,6 +306,12 @@ func ServiceAccountName(serviceAccountName string) PodSpecOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TopologySpreadConstraints(service kobject.ServiceConfig) PodSpecOption {
|
||||||
|
return func(podSpec *PodSpec) {
|
||||||
|
podSpec.TopologySpreadConstraints = ConfigTopologySpreadConstraints(service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (podSpec *PodSpec) Append(ops ...PodSpecOption) *PodSpec {
|
func (podSpec *PodSpec) Append(ops ...PodSpecOption) *PodSpec {
|
||||||
for _, option := range ops {
|
for _, option := range ops {
|
||||||
option(podSpec)
|
option(podSpec)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
version: "3"
|
version: "3.3"
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis
|
||||||
@ -10,4 +10,8 @@ services:
|
|||||||
- node.hostname == machine
|
- node.hostname == machine
|
||||||
- engine.labels.operatingsystem == ubuntu 14.04
|
- engine.labels.operatingsystem == ubuntu 14.04
|
||||||
- node.labels.foo != bar
|
- node.labels.foo != bar
|
||||||
- baz != qux
|
- baz != qux
|
||||||
|
preferences:
|
||||||
|
- spread: node.labels.zone
|
||||||
|
- spread: foo
|
||||||
|
- spread: node.labels.ssd
|
||||||
@ -76,24 +76,52 @@
|
|||||||
{
|
{
|
||||||
"key": "kubernetes.io/hostname",
|
"key": "kubernetes.io/hostname",
|
||||||
"operator": "In",
|
"operator": "In",
|
||||||
"values": ["machine"]
|
"values": [
|
||||||
|
"machine"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "beta.kubernetes.io/os",
|
"key": "kubernetes.io/os",
|
||||||
"operator": "In",
|
"operator": "In",
|
||||||
"values": ["ubuntu 14.04"]
|
"values": [
|
||||||
|
"ubuntu 14.04"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "foo",
|
"key": "foo",
|
||||||
"operator": "NotIn",
|
"operator": "NotIn",
|
||||||
"values": ["bar"]
|
"values": [
|
||||||
|
"bar"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"topologySpreadConstraints": [
|
||||||
|
{
|
||||||
|
"maxSkew": 2,
|
||||||
|
"topologyKey": "zone",
|
||||||
|
"whenUnsatisfiable": "ScheduleAnyway",
|
||||||
|
"labelSelector": {
|
||||||
|
"matchLabels": {
|
||||||
|
"io.kompose.service": "redis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maxSkew": 1,
|
||||||
|
"topologyKey": "ssd",
|
||||||
|
"whenUnsatisfiable": "ScheduleAnyway",
|
||||||
|
"labelSelector": {
|
||||||
|
"matchLabels": {
|
||||||
|
"io.kompose.service": "redis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"strategy": {}
|
"strategy": {}
|
||||||
|
|||||||
@ -96,24 +96,52 @@
|
|||||||
{
|
{
|
||||||
"key": "kubernetes.io/hostname",
|
"key": "kubernetes.io/hostname",
|
||||||
"operator": "In",
|
"operator": "In",
|
||||||
"values": ["machine"]
|
"values": [
|
||||||
|
"machine"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "beta.kubernetes.io/os",
|
"key": "kubernetes.io/os",
|
||||||
"operator": "In",
|
"operator": "In",
|
||||||
"values": ["ubuntu 14.04"]
|
"values": [
|
||||||
|
"ubuntu 14.04"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "foo",
|
"key": "foo",
|
||||||
"operator": "NotIn",
|
"operator": "NotIn",
|
||||||
"values": ["bar"]
|
"values": [
|
||||||
|
"bar"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"topologySpreadConstraints": [
|
||||||
|
{
|
||||||
|
"maxSkew": 2,
|
||||||
|
"topologyKey": "zone",
|
||||||
|
"whenUnsatisfiable": "ScheduleAnyway",
|
||||||
|
"labelSelector": {
|
||||||
|
"matchLabels": {
|
||||||
|
"io.kompose.service": "redis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maxSkew": 1,
|
||||||
|
"topologyKey": "ssd",
|
||||||
|
"whenUnsatisfiable": "ScheduleAnyway",
|
||||||
|
"labelSelector": {
|
||||||
|
"matchLabels": {
|
||||||
|
"io.kompose.service": "redis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -89,7 +89,7 @@
|
|||||||
"values": ["machine"]
|
"values": ["machine"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "beta.kubernetes.io/os",
|
"key": "kubernetes.io/oss",
|
||||||
"operator": "In",
|
"operator": "In",
|
||||||
"values": ["ubuntu 14.04"]
|
"values": ["ubuntu 14.04"]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -104,7 +104,7 @@
|
|||||||
"values": ["machine"]
|
"values": ["machine"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "beta.kubernetes.io/os",
|
"key": "kubernetes.io/os",
|
||||||
"operator": "In",
|
"operator": "In",
|
||||||
"values": ["ubuntu 14.04"]
|
"values": ["ubuntu 14.04"]
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user