WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -19,7 +19,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck+
|
||||
💣 github.com/go4org/hashtriemap from tailscale.com/derp/derpserver
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
|
||||
@@ -41,7 +41,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json/jsontext+
|
||||
github.com/go-json-experiment/json/jsontext from tailscale.com/logtail+
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck+
|
||||
github.com/go-logr/logr from github.com/go-logr/logr/slogr+
|
||||
github.com/go-logr/logr/slogr from github.com/go-logr/zapr
|
||||
github.com/go-logr/zapr from sigs.k8s.io/controller-runtime/pkg/log/zap+
|
||||
|
||||
@@ -9,6 +9,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/types/logger
|
||||
github.com/munnerz/goautoneg from github.com/prometheus/common/expfmt
|
||||
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
|
||||
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
|
||||
|
||||
@@ -125,7 +125,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck+
|
||||
L 💣 github.com/godbus/dbus/v5 from fyne.io/systray+
|
||||
L github.com/godbus/dbus/v5/introspect from fyne.io/systray+
|
||||
L github.com/godbus/dbus/v5/prop from fyne.io/systray
|
||||
|
||||
@@ -15,6 +15,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/types/logger
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
|
||||
github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
|
||||
@@ -15,6 +15,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/types/logger
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
|
||||
github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
|
||||
@@ -30,7 +30,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck+
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
|
||||
@@ -38,7 +38,7 @@ func run(pass *analysis.Pass) (any, error) {
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
var seenName, hasFormat bool
|
||||
var seenName bool
|
||||
for opt := range strings.SplitSeq(tag, ",") {
|
||||
if !seenName {
|
||||
seenName = true
|
||||
@@ -116,13 +116,18 @@ func run(pass *analysis.Pass) (any, error) {
|
||||
report(pass, structType, fieldVar, StringOnNonNumericKind)
|
||||
}
|
||||
default:
|
||||
key, _, ok := strings.Cut(opt, ":")
|
||||
hasFormat = key == "format" && ok
|
||||
// The `format` tag option did not make the cut for
|
||||
// Go 1.27's encoding/json/v2. Its mere presence in a
|
||||
// struct tag makes both v1 and v2 marshaling fail at
|
||||
// runtime unless the caller passes the
|
||||
// jsonv2.ExperimentalSupportFormatTag option.
|
||||
// See https://go.dev/issue/71631 and
|
||||
// https://github.com/tailscale/tailscale/issues/20528.
|
||||
if key, _, ok := strings.Cut(opt, ":"); ok && key == "format" {
|
||||
report(pass, structType, fieldVar, FormatUnsupported)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasFormat && isTimeDuration(mayPointerElem(fieldVar.Type())) {
|
||||
report(pass, structType, fieldVar, FormatMissingOnTimeDuration)
|
||||
}
|
||||
}
|
||||
})
|
||||
return nil, nil
|
||||
@@ -152,26 +157,6 @@ func isString(t types.Type) bool {
|
||||
return ok && basic.Kind() == types.String
|
||||
}
|
||||
|
||||
// isTimeDuration reports whether t is a time.Duration type.
|
||||
func isTimeDuration(t types.Type) bool {
|
||||
return isNamed(t, "time", "Duration")
|
||||
}
|
||||
|
||||
// mayPointerElem returns the pointed-at type if t is a pointer,
|
||||
// otherwise it returns t as-is.
|
||||
func mayPointerElem(t types.Type) types.Type {
|
||||
if pointer, ok := t.(*types.Pointer); ok {
|
||||
return pointer.Elem()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// isNamed reports t is a named typed of the given path and name.
|
||||
func isNamed(t types.Type, path, name string) bool {
|
||||
gotPath, gotName := typeName(t)
|
||||
return gotPath == path && gotName == name
|
||||
}
|
||||
|
||||
// typeName reports the pkgPath and name of the type.
|
||||
// It recursively follows type aliases to get the underlying named type.
|
||||
func typeName(t types.Type) (pkgPath, name string) {
|
||||
|
||||
@@ -74,7 +74,7 @@ const (
|
||||
OmitEmptyShouldBeOmitZero ReportKind = "OmitEmptyShouldBeOmitZero"
|
||||
OmitEmptyShouldBeOmitZeroButHasIsZero ReportKind = "OmitEmptyShouldBeOmitZeroButHasIsZero"
|
||||
StringOnNonNumericKind ReportKind = "StringOnNonNumericKind"
|
||||
FormatMissingOnTimeDuration ReportKind = "FormatMissingOnTimeDuration"
|
||||
FormatUnsupported ReportKind = "FormatUnsupported"
|
||||
)
|
||||
|
||||
func (k ReportKind) message() string {
|
||||
@@ -89,8 +89,8 @@ func (k ReportKind) message() string {
|
||||
return "should probably use `omitzero` instead of `omitempty`"
|
||||
case StringOnNonNumericKind:
|
||||
return "must not use `string` on non-numeric types"
|
||||
case FormatMissingOnTimeDuration:
|
||||
return "must use an explicit `format` tag (e.g., `format:nano`) on a time.Duration type; see https://go.dev/issue/71631"
|
||||
case FormatUnsupported:
|
||||
return "must not use the `format` tag option; Go 1.27's encoding/json rejects it at runtime (see tailscale/tailscale#20528)"
|
||||
default:
|
||||
return string(k)
|
||||
}
|
||||
|
||||
+8
-1
@@ -186,7 +186,14 @@ type Logtail struct {
|
||||
// differently-shaped payloads in a single upload.
|
||||
type LogEntry[T any] struct {
|
||||
Logtail Logtail `json:"logtail,omitzero"`
|
||||
Value T `json:",inline"`
|
||||
// The `inline` tag option was renamed to `embed` in Go 1.27's
|
||||
// encoding/json/v2, but the pinned go-json-experiment module
|
||||
// (used on older Go versions) only knows `inline`. Each
|
||||
// implementation ignores the option it doesn't know, so specify
|
||||
// both until we require Go 1.27 and drop `inline`.
|
||||
//
|
||||
//lint:ignore SA5008 staticcheck doesn't know Go 1.27's `embed` option yet
|
||||
Value T `json:",inline,embed"`
|
||||
}
|
||||
|
||||
// UploadLogs uploads entries to the log server described by conf and returns
|
||||
|
||||
@@ -28,7 +28,7 @@ type Logtail struct {
|
||||
|
||||
type LogEntry[T any] struct {
|
||||
Logtail Logtail `json:"logtail,omitzero"`
|
||||
Value T `json:",inline"`
|
||||
Value T `json:",inline,embed"` // both options; see the non-omit variant in logtail.go
|
||||
}
|
||||
|
||||
func UploadLogs[T any](ctx context.Context, conf Config, entries iter.Seq[LogEntry[T]]) error {
|
||||
|
||||
@@ -24,7 +24,7 @@ const (
|
||||
// conduct the test.
|
||||
type config struct {
|
||||
Version int `json:"version"`
|
||||
TestDuration time.Duration `json:"time,format:nano"`
|
||||
TestDuration time.Duration `json:"time"` // int64 nanoseconds; no jsonv2 format tag (tailscale/tailscale#20528)
|
||||
Direction Direction `json:"direction"`
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -3088,7 +3088,12 @@ type SSHAction struct {
|
||||
|
||||
// SessionDuration, if non-zero, is how long the session can stay open
|
||||
// before being forcefully terminated.
|
||||
SessionDuration time.Duration `json:"sessionDuration,omitempty,format:nano"`
|
||||
// It is encoded as an int64 of nanoseconds (Go's time.Duration
|
||||
// wire format for encoding/json v1). It must not use a jsonv2
|
||||
// format tag; the mere presence of one makes Go 1.27's
|
||||
// encoding/json fail to decode the struct. See
|
||||
// https://github.com/tailscale/tailscale/issues/20528.
|
||||
SessionDuration time.Duration `json:"sessionDuration,omitempty"`
|
||||
|
||||
// AllowAgentForwarding, if true, allows accepted connections to forward
|
||||
// the ssh agent if requested.
|
||||
|
||||
@@ -1310,3 +1310,30 @@ func TestServiceActionTypeValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSHActionJSON verifies that SSHAction round-trips through
|
||||
// encoding/json with SessionDuration encoded as int64 nanoseconds.
|
||||
// It notably guards against jsonv2 `format` tag options in struct
|
||||
// tags, which Go 1.27's encoding/json rejects at runtime.
|
||||
// See https://github.com/tailscale/tailscale/issues/20528.
|
||||
func TestSSHActionJSON(t *testing.T) {
|
||||
a := SSHAction{
|
||||
Accept: true,
|
||||
SessionDuration: 5 * time.Second,
|
||||
}
|
||||
got, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal: %v", err)
|
||||
}
|
||||
const want = `{"accept":true,"sessionDuration":5000000000}`
|
||||
if string(got) != want {
|
||||
t.Errorf("Marshal = %s; want %s", got, want)
|
||||
}
|
||||
var back SSHAction
|
||||
if err := json.Unmarshal(got, &back); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(back, a) {
|
||||
t.Errorf("round trip = %+v; want %+v", back, a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2107,6 +2107,11 @@ func (v SSHActionView) Accept() bool { return v.ж.Accept }
|
||||
|
||||
// SessionDuration, if non-zero, is how long the session can stay open
|
||||
// before being forcefully terminated.
|
||||
// It is encoded as an int64 of nanoseconds (Go's time.Duration
|
||||
// wire format for encoding/json v1). It must not use a jsonv2
|
||||
// format tag; the mere presence of one makes Go 1.27's
|
||||
// encoding/json fail to decode the struct. See
|
||||
// https://github.com/tailscale/tailscale/issues/20528.
|
||||
func (v SSHActionView) SessionDuration() time.Duration { return v.ж.SessionDuration }
|
||||
|
||||
// AllowAgentForwarding, if true, allows accepted connections to forward
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
|
||||
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck+
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
|
||||
"context"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
jsonv1 "github.com/go-json-experiment/json/v1"
|
||||
"go4.org/mem"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/util/ctxkey"
|
||||
@@ -379,12 +381,13 @@ func AsJSON(v any) fmt.Formatter {
|
||||
type asJSONResult struct{ v any }
|
||||
|
||||
func (a asJSONResult) Format(s fmt.State, verb rune) {
|
||||
v, err := json.Marshal(a.v)
|
||||
// Write directly to s rather than going through json.Marshal's
|
||||
// returned []byte to avoid an allocation. The explicit v1
|
||||
// options keep the output identical to encoding/json.
|
||||
err := jsonv2.MarshalWrite(s, a.v, jsonv1.DefaultOptionsV1())
|
||||
if err != nil {
|
||||
fmt.Fprintf(s, "%%!JSON-ERROR:%v", err)
|
||||
return
|
||||
}
|
||||
s.Write(v)
|
||||
}
|
||||
|
||||
// TestLogger returns a logger that logs to tb.Logf
|
||||
|
||||
+7
-2
@@ -105,7 +105,11 @@ func (o Value[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
|
||||
if !o.set {
|
||||
return enc.WriteToken(jsontext.Null)
|
||||
}
|
||||
return jsonv2.MarshalEncode(enc, &o.value)
|
||||
// Pin v2 semantics so the wire format (e.g. nil slices as "[]",
|
||||
// nil maps as "{}") stays the same even when this method is
|
||||
// reached via encoding/json v1, which as of Go 1.27 dispatches
|
||||
// here with v1 options that would otherwise leak into this call.
|
||||
return jsonv2.MarshalEncode(enc, &o.value, jsonv2.DefaultOptionsV2())
|
||||
}
|
||||
|
||||
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
|
||||
@@ -116,7 +120,8 @@ func (o *Value[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
return err
|
||||
}
|
||||
o.set = true
|
||||
return jsonv2.UnmarshalDecode(dec, &o.value)
|
||||
// Pin v2 semantics; see MarshalJSONTo.
|
||||
return jsonv2.UnmarshalDecode(dec, &o.value, jsonv2.DefaultOptionsV2())
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
|
||||
+17
-3
@@ -52,7 +52,15 @@ type serializable[T any] struct {
|
||||
// when the preference has not been configured.
|
||||
Default T `json:",omitzero"`
|
||||
// Metadata is any additional type-agnostic preference metadata to be serialized.
|
||||
Metadata metadata `json:",inline"`
|
||||
//
|
||||
// The `inline` tag option was renamed to `embed` in Go 1.27's
|
||||
// encoding/json/v2, but the pinned go-json-experiment module
|
||||
// (used on older Go versions) only knows `inline`. Each
|
||||
// implementation ignores the option it doesn't know, so specify
|
||||
// both until we require Go 1.27 and drop `inline`.
|
||||
//
|
||||
//lint:ignore SA5008 staticcheck doesn't know Go 1.27's `embed` option yet
|
||||
Metadata metadata `json:",inline,embed"`
|
||||
}
|
||||
|
||||
// preference is an embeddable type that provides a common implementation for
|
||||
@@ -165,12 +173,18 @@ var (
|
||||
|
||||
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
||||
func (p preference[T]) MarshalJSONTo(out *jsontext.Encoder) error {
|
||||
return jsonv2.MarshalEncode(out, &p.s)
|
||||
// Pin v2 semantics so the wire format (notably the handling of
|
||||
// the `inline` and `omitzero` tag options in [serializable])
|
||||
// stays the same even when this method is reached via
|
||||
// encoding/json v1, which as of Go 1.27 dispatches here with v1
|
||||
// options that would otherwise leak into this call.
|
||||
return jsonv2.MarshalEncode(out, &p.s, jsonv2.DefaultOptionsV2())
|
||||
}
|
||||
|
||||
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
|
||||
func (p *preference[T]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
|
||||
return jsonv2.UnmarshalDecode(in, &p.s)
|
||||
// Pin v2 semantics; see MarshalJSONTo.
|
||||
return jsonv2.UnmarshalDecode(in, &p.s, jsonv2.DefaultOptionsV2())
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
|
||||
Reference in New Issue
Block a user