Skip to content

Commit 44d13d3

Browse files
authored
Merge pull request kubernetes#73726 from wk8/wk8/gmsa_alpha
Kubelet changes for Windows GMSA support
2 parents 5a2d587 + 0d392ff commit 44d13d3

10 files changed

+734
-4
lines changed

pkg/features/kube_features.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,12 @@ const (
411411
//
412412
// Implement support for limiting pids in nodes
413413
SupportNodePidsLimit utilfeature.Feature = "SupportNodePidsLimit"
414+
415+
// owner: @wk8
416+
// alpha: v1.14
417+
//
418+
// Enables GMSA support for Windows workloads.
419+
WindowsGMSA utilfeature.Feature = "WindowsGMSA"
414420
)
415421

416422
func init() {

pkg/kubelet/dockershim/BUILD

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ go_library(
77
"doc.go",
88
"docker_checkpoint.go",
99
"docker_container.go",
10+
"docker_container_unsupported.go",
11+
"docker_container_windows.go",
1012
"docker_image.go",
1113
"docker_image_linux.go",
1214
"docker_image_unsupported.go",
@@ -73,6 +75,7 @@ go_library(
7375
"@io_bazel_rules_go//go/platform:windows": [
7476
"//pkg/kubelet/apis:go_default_library",
7577
"//pkg/kubelet/winstats:go_default_library",
78+
"//vendor/golang.org/x/sys/windows/registry:go_default_library",
7679
],
7780
"//conditions:default": [],
7881
}),
@@ -84,6 +87,7 @@ go_test(
8487
"convert_test.go",
8588
"docker_checkpoint_test.go",
8689
"docker_container_test.go",
90+
"docker_container_windows_test.go",
8791
"docker_image_test.go",
8892
"docker_sandbox_test.go",
8993
"docker_service_test.go",
@@ -118,6 +122,9 @@ go_test(
118122
"@io_bazel_rules_go//go/platform:linux": [
119123
"//staging/src/k8s.io/api/core/v1:go_default_library",
120124
],
125+
"@io_bazel_rules_go//go/platform:windows": [
126+
"//vendor/golang.org/x/sys/windows/registry:go_default_library",
127+
],
121128
"//conditions:default": [],
122129
}),
123130
)

pkg/kubelet/dockershim/docker_container.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,9 @@ func (ds *dockerService) CreateContainer(_ context.Context, r *runtimeapi.Create
114114
if iSpec := config.GetImage(); iSpec != nil {
115115
image = iSpec.Image
116116
}
117+
containerName := makeContainerName(sandboxConfig, config)
117118
createConfig := dockertypes.ContainerCreateConfig{
118-
Name: makeContainerName(sandboxConfig, config),
119+
Name: containerName,
119120
Config: &dockercontainer.Config{
120121
// TODO: set User.
121122
Entrypoint: dockerstrslice.StrSlice(config.Command),
@@ -162,15 +163,25 @@ func (ds *dockerService) CreateContainer(_ context.Context, r *runtimeapi.Create
162163

163164
hc.SecurityOpt = append(hc.SecurityOpt, securityOpts...)
164165

165-
createResp, err := ds.client.CreateContainer(createConfig)
166+
cleanupInfo, err := ds.applyPlatformSpecificDockerConfig(r, &createConfig)
166167
if err != nil {
167-
createResp, err = recoverFromCreationConflictIfNeeded(ds.client, createConfig, err)
168+
return nil, err
169+
}
170+
defer func() {
171+
for _, err := range ds.performPlatformSpecificContainerCreationCleanup(cleanupInfo) {
172+
klog.Warningf("error when cleaning up after container %v's creation: %v", containerName, err)
173+
}
174+
}()
175+
176+
createResp, createErr := ds.client.CreateContainer(createConfig)
177+
if createErr != nil {
178+
createResp, createErr = recoverFromCreationConflictIfNeeded(ds.client, createConfig, createErr)
168179
}
169180

170181
if createResp != nil {
171182
return &runtimeapi.CreateContainerResponse{ContainerId: createResp.ID}, nil
172183
}
173-
return nil, err
184+
return nil, createErr
174185
}
175186

176187
// getContainerLogPath returns the container log path specified by kubelet and the real
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// +build !windows
2+
3+
/*
4+
Copyright 2019 The Kubernetes Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package dockershim
20+
21+
import (
22+
dockertypes "github.com/docker/docker/api/types"
23+
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2"
24+
)
25+
26+
type containerCreationCleanupInfo struct{}
27+
28+
// applyPlatformSpecificDockerConfig applies platform-specific configurations to a dockertypes.ContainerCreateConfig struct.
29+
// The containerCreationCleanupInfo struct it returns will be passed as is to performPlatformSpecificContainerCreationCleanup
30+
// after the container has been created.
31+
func (ds *dockerService) applyPlatformSpecificDockerConfig(*runtimeapi.CreateContainerRequest, *dockertypes.ContainerCreateConfig) (*containerCreationCleanupInfo, error) {
32+
return nil, nil
33+
}
34+
35+
// performPlatformSpecificContainerCreationCleanup is responsible for doing any platform-specific cleanup
36+
// after a container creation. Any errors it returns are simply logged, but do not fail the container
37+
// creation.
38+
func (ds *dockerService) performPlatformSpecificContainerCreationCleanup(cleanupInfo *containerCreationCleanupInfo) (errors []error) {
39+
return
40+
}
41+
42+
// platformSpecificContainerCreationInitCleanup is called when dockershim
43+
// is starting, and is meant to clean up any cruft left by previous runs
44+
// creating containers.
45+
// Errors are simply logged, but don't prevent dockershim from starting.
46+
func (ds *dockerService) platformSpecificContainerCreationInitCleanup() (errors []error) {
47+
return
48+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// +build windows
2+
3+
/*
4+
Copyright 2019 The Kubernetes Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package dockershim
20+
21+
import (
22+
"crypto/rand"
23+
"encoding/hex"
24+
"fmt"
25+
"regexp"
26+
27+
"golang.org/x/sys/windows/registry"
28+
29+
dockertypes "github.com/docker/docker/api/types"
30+
dockercontainer "github.com/docker/docker/api/types/container"
31+
32+
runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/runtime/v1alpha2"
33+
"k8s.io/kubernetes/pkg/kubelet/kuberuntime"
34+
)
35+
36+
type containerCreationCleanupInfo struct {
37+
gMSARegistryValueName string
38+
}
39+
40+
// applyPlatformSpecificDockerConfig applies platform-specific configurations to a dockertypes.ContainerCreateConfig struct.
41+
// The containerCreationCleanupInfo struct it returns will be passed as is to performPlatformSpecificContainerCreationCleanup
42+
// after the container has been created.
43+
func (ds *dockerService) applyPlatformSpecificDockerConfig(request *runtimeapi.CreateContainerRequest, createConfig *dockertypes.ContainerCreateConfig) (*containerCreationCleanupInfo, error) {
44+
cleanupInfo := &containerCreationCleanupInfo{}
45+
46+
if err := applyGMSAConfig(request.GetConfig(), createConfig, cleanupInfo); err != nil {
47+
return nil, err
48+
}
49+
50+
return cleanupInfo, nil
51+
}
52+
53+
// applyGMSAConfig looks at the kuberuntime.GMSASpecContainerAnnotationKey container annotation; if present,
54+
// it copies its contents to a unique registry value, and sets a SecurityOpt on the config pointing to that registry value.
55+
// We use registry values instead of files since their location cannot change - as opposed to credential spec files,
56+
// whose location could potentially change down the line, or even be unknown (eg if docker is not installed on the
57+
// C: drive)
58+
// When docker supports passing a credential spec's contents directly, we should switch to using that
59+
// as it will avoid cluttering the registry - there is a moby PR out for this:
60+
// https://github.com/moby/moby/pull/38777
61+
func applyGMSAConfig(config *runtimeapi.ContainerConfig, createConfig *dockertypes.ContainerCreateConfig, cleanupInfo *containerCreationCleanupInfo) error {
62+
credSpec := config.Annotations[kuberuntime.GMSASpecContainerAnnotationKey]
63+
if credSpec == "" {
64+
return nil
65+
}
66+
67+
valueName, err := copyGMSACredSpecToRegistryValue(credSpec)
68+
if err != nil {
69+
return err
70+
}
71+
72+
if createConfig.HostConfig == nil {
73+
createConfig.HostConfig = &dockercontainer.HostConfig{}
74+
}
75+
76+
createConfig.HostConfig.SecurityOpt = append(createConfig.HostConfig.SecurityOpt, "credentialspec=registry://"+valueName)
77+
cleanupInfo.gMSARegistryValueName = valueName
78+
79+
return nil
80+
}
81+
82+
const (
83+
// same as https://github.com/moby/moby/blob/93d994e29c9cc8d81f1b0477e28d705fa7e2cd72/daemon/oci_windows.go#L23
84+
credentialSpecRegistryLocation = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs`
85+
// the prefix for the registry values we write GMSA cred specs to
86+
gMSARegistryValueNamePrefix = "k8s-cred-spec-"
87+
// the number of random bytes to generate suffixes for registry value names
88+
gMSARegistryValueNameSuffixRandomBytes = 40
89+
)
90+
91+
// registryKey is an interface wrapper around `registry.Key`,
92+
// listing only the methods we care about here.
93+
// It's mainly useful to easily allow mocking the registry in tests.
94+
type registryKey interface {
95+
SetStringValue(name, value string) error
96+
DeleteValue(name string) error
97+
ReadValueNames(n int) ([]string, error)
98+
Close() error
99+
}
100+
101+
var registryCreateKeyFunc = func(baseKey registry.Key, path string, access uint32) (registryKey, bool, error) {
102+
return registry.CreateKey(baseKey, path, access)
103+
}
104+
105+
// randomReader is only meant to ever be overridden for testing purposes,
106+
// same idea as for `registryKey` above
107+
var randomReader = rand.Reader
108+
109+
// gMSARegistryValueNamesRegex is the regex used to detect gMSA cred spec
110+
// registry values in `removeAllGMSARegistryValues` below.
111+
var gMSARegistryValueNamesRegex = regexp.MustCompile(fmt.Sprintf("^%s[0-9a-f]{%d}$", gMSARegistryValueNamePrefix, 2*gMSARegistryValueNameSuffixRandomBytes))
112+
113+
// copyGMSACredSpecToRegistryKey copies the credential specs to a unique registry value, and returns its name.
114+
func copyGMSACredSpecToRegistryValue(credSpec string) (string, error) {
115+
valueName, err := gMSARegistryValueName()
116+
if err != nil {
117+
return "", err
118+
}
119+
120+
// write to the registry
121+
key, _, err := registryCreateKeyFunc(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.SET_VALUE)
122+
if err != nil {
123+
return "", fmt.Errorf("unable to open registry key %q: %v", credentialSpecRegistryLocation, err)
124+
}
125+
defer key.Close()
126+
if err = key.SetStringValue(valueName, credSpec); err != nil {
127+
return "", fmt.Errorf("unable to write into registry value %q/%q: %v", credentialSpecRegistryLocation, valueName, err)
128+
}
129+
130+
return valueName, nil
131+
}
132+
133+
// gMSARegistryValueName computes the name of the registry value where to store the GMSA cred spec contents.
134+
// The value's name is a purely random suffix appended to `gMSARegistryValueNamePrefix`.
135+
func gMSARegistryValueName() (string, error) {
136+
randomSuffix, err := randomString(gMSARegistryValueNameSuffixRandomBytes)
137+
138+
if err != nil {
139+
return "", fmt.Errorf("error when generating gMSA registry value name: %v", err)
140+
}
141+
142+
return gMSARegistryValueNamePrefix + randomSuffix, nil
143+
}
144+
145+
// randomString returns a random hex string.
146+
func randomString(length int) (string, error) {
147+
randBytes := make([]byte, length)
148+
149+
if n, err := randomReader.Read(randBytes); err != nil || n != length {
150+
if err == nil {
151+
err = fmt.Errorf("only got %v random bytes, expected %v", n, length)
152+
}
153+
return "", fmt.Errorf("unable to generate random string: %v", err)
154+
}
155+
156+
return hex.EncodeToString(randBytes), nil
157+
}
158+
159+
// performPlatformSpecificContainerCreationCleanup is responsible for doing any platform-specific cleanup
160+
// after a container creation. Any errors it returns are simply logged, but do not fail the container
161+
// creation.
162+
func (ds *dockerService) performPlatformSpecificContainerCreationCleanup(cleanupInfo *containerCreationCleanupInfo) (errors []error) {
163+
if err := removeGMSARegistryValue(cleanupInfo); err != nil {
164+
errors = append(errors, err)
165+
}
166+
167+
return
168+
}
169+
170+
func removeGMSARegistryValue(cleanupInfo *containerCreationCleanupInfo) error {
171+
if cleanupInfo == nil || cleanupInfo.gMSARegistryValueName == "" {
172+
return nil
173+
}
174+
175+
key, _, err := registryCreateKeyFunc(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.SET_VALUE)
176+
if err != nil {
177+
return fmt.Errorf("unable to open registry key %q: %v", credentialSpecRegistryLocation, err)
178+
}
179+
defer key.Close()
180+
if err = key.DeleteValue(cleanupInfo.gMSARegistryValueName); err != nil {
181+
return fmt.Errorf("unable to remove registry value %q/%q: %v", credentialSpecRegistryLocation, cleanupInfo.gMSARegistryValueName, err)
182+
}
183+
184+
return nil
185+
}
186+
187+
// platformSpecificContainerCreationInitCleanup is called when dockershim
188+
// is starting, and is meant to clean up any cruft left by previous runs
189+
// creating containers.
190+
// Errors are simply logged, but don't prevent dockershim from starting.
191+
func (ds *dockerService) platformSpecificContainerCreationInitCleanup() (errors []error) {
192+
return removeAllGMSARegistryValues()
193+
}
194+
195+
func removeAllGMSARegistryValues() (errors []error) {
196+
key, _, err := registryCreateKeyFunc(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.SET_VALUE)
197+
if err != nil {
198+
return []error{fmt.Errorf("unable to open registry key %q: %v", credentialSpecRegistryLocation, err)}
199+
}
200+
defer key.Close()
201+
202+
valueNames, err := key.ReadValueNames(0)
203+
if err != nil {
204+
return []error{fmt.Errorf("unable to list values under registry key %q: %v", credentialSpecRegistryLocation, err)}
205+
}
206+
207+
for _, valueName := range valueNames {
208+
if gMSARegistryValueNamesRegex.MatchString(valueName) {
209+
if err = key.DeleteValue(valueName); err != nil {
210+
errors = append(errors, fmt.Errorf("unable to remove registry value %q/%q: %v", credentialSpecRegistryLocation, valueName, err))
211+
}
212+
}
213+
}
214+
215+
return
216+
}

0 commit comments

Comments
 (0)