Skip to content

Commit 6be41bc

Browse files
authored
Add hvsock service config annotation (microsoft#2056)
Allow specifying the hyper-v configuration for specific service GUIDs via an annotation to allow dedicated hvsocket communication from the host to the guest Signed-off-by: Hamza El-Saawy <[email protected]>
1 parent 02a899c commit 6be41bc

8 files changed

Lines changed: 253 additions & 60 deletions

File tree

internal/annotations/annotations.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
1-
// This package contains annotations that are not exposed to end users and mainly for
2-
// testing and debugging purposes.
1+
// This package contains annotations that are not exposed to end users and are either:
2+
// 1. intended for testing and debugging purposes; or
3+
// 2. rely on undocumented Windows APIs that are subject to change.
34
//
45
// Do not rely on these annotations to customize production workload behavior.
56
package annotations
67

78
// uVM specific annotations
89

910
const (
11+
// UVMHyperVSocketConfigPrefix is the prefix of an annotation to map a [hyper-v socket] service GUID
12+
// to a JSON-encoded string of its [configuration].
13+
//
14+
// The service GUID should be part of the annotation.
15+
// For example:
16+
//
17+
// "io.microsoft.virtualmachine.hv-socket.service-table.00000000-0000-0000-0000-000000000000" =
18+
// "{\"AllowWildcardBinds\": true, \"BindSecurityDescriptor\": \"D:P(A;;FA;;;WD)\"}"
19+
//
20+
// If multiple annotations with the same GUID are present, then it is undefined which configuration will
21+
// take precedence.
22+
//
23+
// For LCOW, it is preferred to use [ExtraVSockPorts], as vsock ports specified there will take precedence.
24+
//
25+
// # Warning
26+
//
27+
// Setting the configuration for special services (e.g., the GCS) can cause catastrophic failures.
28+
//
29+
// [hyper-v socket]: https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service
30+
// [configuration]: https://learn.microsoft.com/en-us/virtualization/api/hcs/schemareference#HvSocketServiceConfig
31+
UVMHyperVSocketConfigPrefix = "io.microsoft.virtualmachine.hv-socket.service-table."
32+
1033
// AdditionalRegistryValues specifies additional registry keys and their values to set in the WCOW UVM.
1134
// The format is a JSON-encoded string of an array containing [HCS RegistryValue] objects.
1235
//

internal/oci/annotations.go

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import (
44
"context"
55
"encoding/json"
66
"errors"
7+
"fmt"
78
"slices"
89
"strconv"
910
"strings"
1011

1112
"github.com/opencontainers/runtime-spec/specs-go"
1213
"github.com/sirupsen/logrus"
1314

15+
"github.com/Microsoft/go-winio/pkg/guid"
1416
iannotations "github.com/Microsoft/hcsshim/internal/annotations"
1517
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
1618
"github.com/Microsoft/hcsshim/internal/log"
@@ -66,6 +68,7 @@ func ParseAnnotationsDisableGMSA(ctx context.Context, s *specs.Spec) bool {
6668
}
6769

6870
// parseAdditionalRegistryValues extracts the additional registry values to set from annotations.
71+
//
6972
// Like the [parseAnnotation*] functions, this logs errors but does not return them.
7073
func parseAdditionalRegistryValues(ctx context.Context, a map[string]string) []hcsschema.RegistryValue {
7174
// rather than have users deal with nil vs []hcsschema.RegistryValue as returns, always
@@ -81,7 +84,7 @@ func parseAdditionalRegistryValues(ctx context.Context, a map[string]string) []h
8184

8285
t := []hcsschema.RegistryValue{}
8386
if err := json.Unmarshal([]byte(v), &t); err != nil {
84-
logAnnotationParseError(ctx, k, v, "JSON string", err)
87+
logAnnotationValueParseError(ctx, k, v, fmt.Sprintf("%T", t), err)
8588
return []hcsschema.RegistryValue{}
8689
}
8790

@@ -182,20 +185,63 @@ func parseAdditionalRegistryValues(ctx context.Context, a map[string]string) []h
182185
return slices.Clip(rvs)
183186
}
184187

188+
// parseHVSocketServiceTable extracts any additional Hyper-V socket service configurations from annotations.
189+
//
190+
// Like the [parseAnnotation*] functions, this logs errors but does not return them.
191+
func parseHVSocketServiceTable(ctx context.Context, a map[string]string) map[string]hcsschema.HvSocketServiceConfig {
192+
sc := make(map[string]hcsschema.HvSocketServiceConfig)
193+
// TODO(go1.23) use range over functions to implement a functional `filter | map $ a`
194+
for k, v := range a {
195+
sGUID, found := strings.CutPrefix(k, iannotations.UVMHyperVSocketConfigPrefix)
196+
if !found {
197+
continue
198+
}
199+
200+
entry := log.G(ctx).WithFields(logrus.Fields{
201+
logfields.OCIAnnotation: k,
202+
logfields.Value: v,
203+
"guid": sGUID,
204+
})
205+
206+
g, err := guid.FromString(sGUID)
207+
if err != nil {
208+
entry.WithError(err).Warn("invalid GUID string for Hyper-V socket service configuration annotation")
209+
continue
210+
}
211+
sGUID = g.String() // overwrite the GUID string to standardize format (capitalization)
212+
213+
conf := hcsschema.HvSocketServiceConfig{}
214+
if err := json.Unmarshal([]byte(v), &conf); err != nil {
215+
logAnnotationValueParseError(ctx, k, v, fmt.Sprintf("%T", conf), err)
216+
continue
217+
}
218+
219+
if _, found := sc[sGUID]; found {
220+
entry.WithFields(logrus.Fields{
221+
"guid": sGUID,
222+
}).Warn("overwritting existing Hyper-V socket service configuration")
223+
}
224+
225+
if entry.Logger.IsLevelEnabled(logrus.TraceLevel) {
226+
entry.WithField("configuration", log.Format(ctx, conf)).Trace("found Hyper-V socket service configuration annotation")
227+
}
228+
sc[sGUID] = conf
229+
}
230+
231+
return sc
232+
}
233+
185234
// general annotation parsing
186235

187236
// ParseAnnotationsBool searches `a` for `key` and if found verifies that the
188237
// value is `true` or `false` in any case. If `key` is not found returns `def`.
189238
func ParseAnnotationsBool(ctx context.Context, a map[string]string, key string, def bool) bool {
190239
if v, ok := a[key]; ok {
191-
switch strings.ToLower(v) {
192-
case "true":
193-
return true
194-
case "false":
195-
return false
196-
default:
197-
logAnnotationParseError(ctx, key, v, logfields.Bool, nil)
240+
b, err := strconv.ParseBool(v)
241+
if err == nil {
242+
return b
198243
}
244+
logAnnotationValueParseError(ctx, key, v, logfields.Bool, err)
199245
}
200246
return def
201247
}
@@ -206,17 +252,11 @@ func ParseAnnotationsBool(ctx context.Context, a map[string]string, key string,
206252
// the value they point at.
207253
func ParseAnnotationsNullableBool(ctx context.Context, a map[string]string, key string) *bool {
208254
if v, ok := a[key]; ok {
209-
switch strings.ToLower(v) {
210-
case "true":
211-
_bool := true
212-
return &_bool
213-
case "false":
214-
_bool := false
215-
return &_bool
216-
default:
217-
err := errors.New("boolean fields must be 'true', 'false', or not set")
218-
logAnnotationParseError(ctx, key, v, logfields.Bool, err)
255+
b, err := strconv.ParseBool(v)
256+
if err == nil {
257+
return &b
219258
}
259+
logAnnotationValueParseError(ctx, key, v, logfields.Bool, err)
220260
}
221261
return nil
222262
}
@@ -230,7 +270,7 @@ func ParseAnnotationsInt32(ctx context.Context, a map[string]string, key string,
230270
v := int32(countu)
231271
return v
232272
}
233-
logAnnotationParseError(ctx, key, v, logfields.Int32, err)
273+
logAnnotationValueParseError(ctx, key, v, logfields.Int32, err)
234274
}
235275
return def
236276
}
@@ -244,7 +284,7 @@ func ParseAnnotationsUint32(ctx context.Context, a map[string]string, key string
244284
v := uint32(countu)
245285
return v
246286
}
247-
logAnnotationParseError(ctx, key, v, logfields.Uint32, err)
287+
logAnnotationValueParseError(ctx, key, v, logfields.Uint32, err)
248288
}
249289
return def
250290
}
@@ -257,15 +297,15 @@ func ParseAnnotationsUint64(ctx context.Context, a map[string]string, key string
257297
if err == nil {
258298
return countu
259299
}
260-
logAnnotationParseError(ctx, key, v, logfields.Uint64, err)
300+
logAnnotationValueParseError(ctx, key, v, logfields.Uint64, err)
261301
}
262302
return def
263303
}
264304

265-
// ParseAnnotationCommaSeparated searches `annotations` for `annotation` corresponding to a
266-
// list of comma separated strings
267-
func ParseAnnotationCommaSeparatedUint32(ctx context.Context, annotations map[string]string, annotation string, def []uint32) []uint32 {
268-
cs, ok := annotations[annotation]
305+
// ParseAnnotationCommaSeparated searches `a` for `annotation` corresponding to a
306+
// list of comma separated strings.
307+
func ParseAnnotationCommaSeparatedUint32(_ context.Context, a map[string]string, key string, def []uint32) []uint32 {
308+
cs, ok := a[key]
269309
if !ok || cs == "" {
270310
return def
271311
}
@@ -289,18 +329,18 @@ func ParseAnnotationsString(a map[string]string, key string, def string) string
289329
return def
290330
}
291331

292-
// ParseAnnotationCommaSeparated searches `annotations` for `annotation` corresponding to a
293-
// list of comma separated strings
294-
func ParseAnnotationCommaSeparated(annotation string, annotations map[string]string) []string {
295-
cs, ok := annotations[annotation]
332+
// ParseAnnotationCommaSeparated searches `a` for `key` corresponding to a
333+
// list of comma separated strings.
334+
func ParseAnnotationCommaSeparated(key string, a map[string]string) []string {
335+
cs, ok := a[key]
296336
if !ok || cs == "" {
297337
return nil
298338
}
299339
results := strings.Split(cs, ",")
300340
return results
301341
}
302342

303-
func logAnnotationParseError(ctx context.Context, k, v, et string, err error) {
343+
func logAnnotationValueParseError(ctx context.Context, k, v, et string, err error) {
304344
entry := log.G(ctx).WithFields(logrus.Fields{
305345
logfields.OCIAnnotation: k,
306346
logfields.Value: v,
@@ -309,5 +349,5 @@ func logAnnotationParseError(ctx context.Context, k, v, et string, err error) {
309349
if err != nil {
310350
entry = entry.WithError(err)
311351
}
312-
entry.Warning("annotation could not be parsed")
352+
entry.Warning("annotation value could not be parsed")
313353
}

internal/oci/annotations_test.go

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package oci
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
57
"errors"
68
"fmt"
9+
"maps"
710
"strings"
811
"testing"
912

@@ -225,7 +228,9 @@ func TestParseAdditionalRegistryValues(t *testing.T) {
225228
t.Logf("registry values:\n%s", tt.give)
226229
v := strings.ReplaceAll(tt.give, "\n", "")
227230
rvs := parseAdditionalRegistryValues(ctx, map[string]string{
228-
iannotations.AdditionalRegistryValues: v,
231+
"some-random-annotation": "random",
232+
"not-microsoft.virtualmachine.wcow.additional-reg-keys": "this is fake",
233+
iannotations.AdditionalRegistryValues: v,
229234
})
230235
want := tt.want
231236
if want == nil {
@@ -237,3 +242,112 @@ func TestParseAdditionalRegistryValues(t *testing.T) {
237242
})
238243
}
239244
}
245+
246+
func TestParseHVSocketServiceTable(t *testing.T) {
247+
ctx := context.Background()
248+
249+
toString := func(t *testing.T, v hcsschema.HvSocketServiceConfig) string {
250+
t.Helper()
251+
252+
buf := &bytes.Buffer{}
253+
enc := json.NewEncoder(buf)
254+
enc.SetEscapeHTML(false)
255+
enc.SetIndent("", "")
256+
257+
if err := enc.Encode(v); err != nil {
258+
t.Fatalf("encode %v to JSON: %v", v, err)
259+
}
260+
261+
return strings.TrimSpace(buf.String())
262+
}
263+
264+
g1 := "0b52781f-b24d-5685-ddf6-69830ed40ec3"
265+
g2 := "00000000-0000-0000-0000-000000000000"
266+
267+
defaultConfig := hcsschema.HvSocketServiceConfig{
268+
AllowWildcardBinds: true,
269+
BindSecurityDescriptor: "D:P(A;;FA;;;WD)",
270+
}
271+
defaultConfigStr := toString(t, defaultConfig)
272+
273+
disabledConfig := hcsschema.HvSocketServiceConfig{
274+
Disabled: true,
275+
}
276+
disabledConfigStr := toString(t, disabledConfig)
277+
278+
for _, tt := range []struct {
279+
name string
280+
give map[string]string
281+
want map[string]hcsschema.HvSocketServiceConfig
282+
}{
283+
{
284+
name: "empty",
285+
},
286+
{
287+
name: "single",
288+
give: map[string]string{
289+
iannotations.UVMHyperVSocketConfigPrefix + g1: defaultConfigStr,
290+
},
291+
want: map[string]hcsschema.HvSocketServiceConfig{
292+
g1: defaultConfig,
293+
},
294+
},
295+
{
296+
name: "invalid guid",
297+
give: map[string]string{
298+
iannotations.UVMHyperVSocketConfigPrefix + "not-a-guid": defaultConfigStr,
299+
},
300+
},
301+
{
302+
name: "invalid config",
303+
give: map[string]string{
304+
iannotations.UVMHyperVSocketConfigPrefix + g1: `["not", "a", "valid", "config"]`,
305+
},
306+
},
307+
{
308+
name: "override",
309+
give: map[string]string{
310+
iannotations.UVMHyperVSocketConfigPrefix + g1: defaultConfigStr,
311+
iannotations.UVMHyperVSocketConfigPrefix + strings.ToUpper(g1): defaultConfigStr,
312+
},
313+
want: map[string]hcsschema.HvSocketServiceConfig{
314+
g1: defaultConfig,
315+
},
316+
},
317+
{
318+
name: "multiple",
319+
give: map[string]string{
320+
iannotations.UVMHyperVSocketConfigPrefix + strings.ToUpper(g1): defaultConfigStr,
321+
iannotations.UVMHyperVSocketConfigPrefix + g2: disabledConfigStr,
322+
323+
iannotations.UVMHyperVSocketConfigPrefix + g1: `["not", "a", "valid", "config"]`,
324+
iannotations.UVMHyperVSocketConfigPrefix + "not-a-guid": defaultConfigStr,
325+
"also.not-a-guid": disabledConfigStr,
326+
},
327+
want: map[string]hcsschema.HvSocketServiceConfig{
328+
g1: defaultConfig,
329+
g2: disabledConfig,
330+
},
331+
},
332+
} {
333+
t.Run(tt.name, func(t *testing.T) {
334+
annots := map[string]string{
335+
"some-random-annotation": "random",
336+
"io.microsoft.virtualmachine.hv-socket.service-table": "should be ignored",
337+
"not-microsoft.virtualmachine.hv-socket.service-table": "this is fake",
338+
}
339+
maps.Copy(annots, tt.give)
340+
t.Logf("annotations:\n%v", annots)
341+
342+
rvs := parseHVSocketServiceTable(ctx, annots)
343+
t.Logf("got %v", rvs)
344+
want := tt.want
345+
if want == nil {
346+
want = map[string]hcsschema.HvSocketServiceConfig{}
347+
}
348+
if diff := cmp.Diff(want, rvs); diff != "" {
349+
t.Fatal(diff)
350+
}
351+
})
352+
}
353+
}

0 commit comments

Comments
 (0)