Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

Add --feature-gates handling for kubelet and api server #2032

Merged
merged 9 commits into from
Jan 17, 2018
44 changes: 44 additions & 0 deletions examples/feature-gates/kubernetes-featuresgates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"apiVersion": "vlabs",
"properties": {
"orchestratorProfile": {
"orchestratorType": "Kubernetes",
"orchestratorRelease": "1.8",
"kubernetesConfig": {
"kubeletConfig" : {
"--feature-gates": "MountPropagation=true,DebugContainers=true"
},
"apiServerConfig" : {
"--feature-gates": "MountPropagation=true"
}
}
},
"masterProfile": {
"count": 1,
"dnsPrefix": "",
"vmSize": "Standard_D2_v2"
},
"agentPoolProfiles": [
{
"name": "agentpool1",
"count": 3,
"vmSize": "Standard_D2_v2",
"availabilityProfile": "AvailabilitySet"
}
],
"linuxProfile": {
"adminUsername": "azureuser",
"ssh": {
"publicKeys": [
{
"keyData": ""
}
]
}
},
"servicePrincipalProfile": {
"clientId": "",
"secret": ""
}
}
}
2 changes: 1 addition & 1 deletion parts/k8s/artifacts/1.5/kuberneteskubelet.service
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ ExecStart=/usr/bin/docker run \
--node-labels="${KUBELET_NODE_LABELS}" \
--hairpin-mode=promiscuous-bridge \
${KUBELET_CONFIG} \
--v=2 ${KUBELET_FEATURE_GATES}
--v=2

[Install]
WantedBy=multi-user.target
2 changes: 1 addition & 1 deletion parts/k8s/artifacts/kuberneteskubelet.service
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ ExecStart=/usr/bin/docker run \
--require-kubeconfig \
--enable-server \
--node-labels="${KUBELET_NODE_LABELS}" \
--v=2 ${KUBELET_FEATURE_GATES} \
--v=2 \
--non-masquerade-cidr=${KUBELET_NON_MASQUERADE_CIDR} \
--volume-plugin-dir=/etc/kubernetes/volumeplugins \
$KUBELET_CONFIG \
Expand Down
1 change: 0 additions & 1 deletion parts/k8s/kubernetesagentcustomdata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ write_files:
KUBELET_NODE_LABELS={{GetAgentKubernetesLabels . "',variables('labelResourceGroup'),'"}}
{{if IsKubernetesVersionGe "1.6.0"}}
KUBELET_NON_MASQUERADE_CIDR={{WrapAsVariable "kubernetesNonMasqueradeCidr"}}
KUBELET_FEATURE_GATES=--feature-gates=Accelerators=true
{{end}}

AGENT_ARTIFACTS_CONFIG_PLACEHOLDER
Expand Down
65 changes: 65 additions & 0 deletions pkg/acsengine/defaults-kubelet.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package acsengine

import (
"bytes"
"fmt"
"sort"
"strconv"
"strings"

"github.com/Azure/acs-engine/pkg/api"
"github.com/Azure/acs-engine/pkg/helpers"
Expand Down Expand Up @@ -50,6 +54,7 @@ func setKubeletConfig(cs *api.ContainerService) {

// If no user-configurable kubelet config values exists, use the defaults
setMissingKubeletValues(o.KubernetesConfig, defaultKubeletConfig)
addDefaultFeatureGates(o.KubernetesConfig.KubeletConfig, o.OrchestratorVersion, "", "")

// Override default cloud-provider?
if helpers.IsTrueBoolPointer(o.KubernetesConfig.UseCloudControllerManager) {
Expand Down Expand Up @@ -84,15 +89,34 @@ func setKubeletConfig(cs *api.ContainerService) {
if cs.Properties.MasterProfile != nil {
if cs.Properties.MasterProfile.KubernetesConfig == nil {
cs.Properties.MasterProfile.KubernetesConfig = &api.KubernetesConfig{}
cs.Properties.MasterProfile.KubernetesConfig.KubeletConfig = copyMap(cs.Properties.MasterProfile.KubernetesConfig.KubeletConfig)
}
setMissingKubeletValues(cs.Properties.MasterProfile.KubernetesConfig, o.KubernetesConfig.KubeletConfig)
addDefaultFeatureGates(cs.Properties.MasterProfile.KubernetesConfig.KubeletConfig, o.OrchestratorVersion, "", "")

}
// Agent-specific kubelet config changes go here
for _, profile := range cs.Properties.AgentPoolProfiles {
if profile.KubernetesConfig == nil {
profile.KubernetesConfig = &api.KubernetesConfig{}
profile.KubernetesConfig.KubeletConfig = copyMap(profile.KubernetesConfig.KubeletConfig)
}
setMissingKubeletValues(profile.KubernetesConfig, o.KubernetesConfig.KubeletConfig)
addDefaultFeatureGates(profile.KubernetesConfig.KubeletConfig, o.OrchestratorVersion, "1.6.0", "Accelerators=true")
}
}

// combine user-provided --feature-gates vals with defaults
// a minimum k8s version may be declared as required for defaults assignment
func addDefaultFeatureGates(m map[string]string, version string, minVersion string, defaults string) {
if minVersion != "" {
if isKubernetesVersionGe(version, minVersion) {
m["--feature-gates"] = combineValues(m["--feature-gates"], defaults)
} else {
m["--feature-gates"] = combineValues(m["--feature-gates"], "")
}
} else {
m["--feature-gates"] = combineValues(m["--feature-gates"], defaults)
}
}

Expand All @@ -109,3 +133,44 @@ func setMissingKubeletValues(p *api.KubernetesConfig, d map[string]string) {
}
}
}
func copyMap(input map[string]string) map[string]string {
copy := map[string]string{}
for key, value := range input {
copy[key] = value
}
return copy
}
func combineValues(inputs ...string) string {
var valueMap map[string]string
valueMap = make(map[string]string)
for _, input := range inputs {
applyValueStringToMap(valueMap, input)
}
return mapToString(valueMap)
}

func applyValueStringToMap(valueMap map[string]string, input string) {
values := strings.Split(input, ",")
for index := 0; index < len(values); index++ {
// trim spaces (e.g. if the input was "foo=true, bar=true" - we want to drop the space after the comma)
value := strings.Trim(values[index], " ")
valueParts := strings.Split(value, "=")
if len(valueParts) == 2 {
valueMap[valueParts[0]] = valueParts[1]
}
}
}

func mapToString(valueMap map[string]string) string {
// Order by key for consistency
keys := []string{}
for key := range valueMap {
keys = append(keys, key)
}
sort.Strings(keys)
var buf bytes.Buffer
for _, key := range keys {
buf.WriteString(fmt.Sprintf("%s=%s,", key, valueMap[key]))
}
return strings.TrimSuffix(buf.String(), ",")
}
114 changes: 114 additions & 0 deletions pkg/acsengine/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,100 @@ func TestPointerToBool(t *testing.T) {
}
}

func TestKubeletFeatureGatesEnsureAcceleratorsNotSetFor1_5_0(t *testing.T) {
mockCS := getMockBaseContainerService("1.5.0")
properties := mockCS.Properties

// No KubernetesConfig.KubeletConfig set for MasterProfile or AgentProfile
// so they will inherit the top-level config
properties.OrchestratorProfile.KubernetesConfig = getKubernetesConfigWithFeatureGates("TopLevel=true")

setKubeletConfig(&mockCS)

// Verify that the Accelerators feature gate has not been applied to master or agents
agentFeatureGates := properties.AgentPoolProfiles[0].KubernetesConfig.KubeletConfig["--feature-gates"]
if agentFeatureGates != "TopLevel=true" {
t.Fatalf("setKubeletConfig modified the agent profile (version 1.5.0): expected 'TopLevel=true' got '%s'", agentFeatureGates)
}
masterFeatureFates := properties.MasterProfile.KubernetesConfig.KubeletConfig["--feature-gates"]
if masterFeatureFates != "TopLevel=true" {
t.Fatalf("setKubeletConfig modified feature gates for master profile: 'TopLevel=true' got '%s'", agentFeatureGates)
}
}
func TestKubeletFeatureGatesEnsureAcceleratorsOnAgentsFor1_6_0(t *testing.T) {
mockCS := getMockBaseContainerService("1.6.0")
properties := mockCS.Properties

// No KubernetesConfig.KubeletConfig set for MasterProfile or AgentProfile
// so they will inherit the top-level config
properties.OrchestratorProfile.KubernetesConfig = getKubernetesConfigWithFeatureGates("TopLevel=true")

setKubeletConfig(&mockCS)

agentFeatureGates := properties.AgentPoolProfiles[0].KubernetesConfig.KubeletConfig["--feature-gates"]
if agentFeatureGates != "Accelerators=true,TopLevel=true" {
t.Fatalf("setKubeletConfig did not add 'Accelerators=true' for agent profile: expected 'Accelerators=true;TopLevel=true' got '%s'", agentFeatureGates)
}

// Verify that the Accelerators feature gate override has only been applied to the agents
masterFeatureFates := properties.MasterProfile.KubernetesConfig.KubeletConfig["--feature-gates"]
if masterFeatureFates != "TopLevel=true" {
t.Fatalf("setKubeletConfig modified feature gates for master profile: expected 'TopLevel=true' got '%s'", agentFeatureGates)
}
}

func TestKubeletFeatureGatesEnsureMasterAndAgentConfigUsedFor1_5_0(t *testing.T) {
mockCS := getMockBaseContainerService("1.5.0")
properties := mockCS.Properties

// Set MasterProfile and AgentProfiles KubernetesConfig.KubeletConfit ig values
// Verify that they are used instead of the top-level config
properties.OrchestratorProfile.KubernetesConfig = getKubernetesConfigWithFeatureGates("TopLevel=true")
properties.MasterProfile = &api.MasterProfile{KubernetesConfig: getKubernetesConfigWithFeatureGates("MasterLevel=true")}
properties.AgentPoolProfiles[0].KubernetesConfig = getKubernetesConfigWithFeatureGates("AgentLevel=true")

setKubeletConfig(&mockCS)

agentFeatureGates := properties.AgentPoolProfiles[0].KubernetesConfig.KubeletConfig["--feature-gates"]
if agentFeatureGates != "AgentLevel=true" {
t.Fatalf("setKubeletConfig agent profile: expected 'AgentLevel=true' got '%s'", agentFeatureGates)
}

// Verify that the Accelerators feature gate override has only been applied to the agents
masterFeatureFates := properties.MasterProfile.KubernetesConfig.KubeletConfig["--feature-gates"]
if masterFeatureFates != "MasterLevel=true" {
t.Fatalf("setKubeletConfig master profile: expected 'MasterLevel=true' got '%s'", agentFeatureGates)
}
}

func TestKubeletFeatureGatesEnsureMasterAndAgentConfigUsedFor1_6_0(t *testing.T) {
mockCS := getMockBaseContainerService("1.6.0")
properties := mockCS.Properties

// Set MasterProfile and AgentProfiles KubernetesConfig.KubeletConfig values
// Verify that they are used instead of the top-level config
properties.OrchestratorProfile.KubernetesConfig = getKubernetesConfigWithFeatureGates("TopLevel=true")
properties.MasterProfile = &api.MasterProfile{KubernetesConfig: getKubernetesConfigWithFeatureGates("MasterLevel=true")}
properties.AgentPoolProfiles[0].KubernetesConfig = getKubernetesConfigWithFeatureGates("AgentLevel=true")

setKubeletConfig(&mockCS)

agentFeatureGates := properties.AgentPoolProfiles[0].KubernetesConfig.KubeletConfig["--feature-gates"]
if agentFeatureGates != "Accelerators=true,AgentLevel=true" {
t.Fatalf("setKubeletConfig agent profile: expected 'Accelerators=true,AgentLevel=true' got '%s'", agentFeatureGates)
}

// Verify that the Accelerators feature gate override has only been applied to the agents
masterFeatureFates := properties.MasterProfile.KubernetesConfig.KubeletConfig["--feature-gates"]
if masterFeatureFates != "MasterLevel=true" {
t.Fatalf("setKubeletConfig master profile: expected 'MasterLevel=true' got '%s'", agentFeatureGates)
}
}

// TODO - tests:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these TODOs still operative? (we can merge with them in there if they mark gaps in test surface area that we want to fill in the future)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops - no, I've added the tests now. Just pushed a commit to remove the comments

// - check merging
// - check that master/agent overrides are used correctly

func getMockAddon(name string) api.KubernetesAddon {
return api.KubernetesAddon{
Name: name,
Expand All @@ -302,3 +396,23 @@ func getMockAddon(name string) api.KubernetesAddon {
},
}
}

func getMockBaseContainerService(orchestratorVersion string) api.ContainerService {
return api.ContainerService{
Properties: &api.Properties{
OrchestratorProfile: &api.OrchestratorProfile{
OrchestratorVersion: orchestratorVersion,
KubernetesConfig: &api.KubernetesConfig{},
},
MasterProfile: &api.MasterProfile{},
AgentPoolProfiles: []*api.AgentPoolProfile{
{},
},
},
}
}
func getKubernetesConfigWithFeatureGates(featureGates string) *api.KubernetesConfig {
return &api.KubernetesConfig{
KubeletConfig: map[string]string{"--feature-gates": featureGates},
}
}