From d9aeaa504b7af9bdc5ffc9d42413b8114eb92092 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 3 Jul 2026 08:04:33 +0000 Subject: [PATCH] ipn/conffile: add ErrNoConfig for absent config sources Load wraps read-phase failures with it so callers can distinguish a missing config from an invalid one. Updates #1866 Signed-off-by: Kristoffer Dalby --- ipn/conffile/conffile.go | 15 ++++++++- ipn/conffile/conffile_test.go | 60 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 ipn/conffile/conffile_test.go diff --git a/ipn/conffile/conffile.go b/ipn/conffile/conffile.go index c221a3d8c..8586c574e 100644 --- a/ipn/conffile/conffile.go +++ b/ipn/conffile/conffile.go @@ -8,6 +8,7 @@ package conffile import ( "bytes" "encoding/json" + "errors" "fmt" "os" "runtime" @@ -40,6 +41,15 @@ func (c *Config) WantRunning() bool { // from the VM's metadata service's user-data field. const VMUserDataPath = "vm:user-data" +// ErrNoConfig is returned (wrapped) by Load when the config source is absent or +// unreadable: a missing file, an EC2 instance with no user-data, an unreachable +// metadata service, or a build without the relevant cloud support. It reports +// "no config was provided", as distinct from "a config was provided but is +// invalid" (which Load returns as an unwrapped parse/validate error). Callers +// that want to boot unconfigured when no config is present (e.g. tailscaled's +// "optional:" -config prefix) can check for it with errors.Is. +var ErrNoConfig = errors.New("no config present") + // hujsonStandardize is set to hujson.Standardize by conffile_hujson.go on // platforms that support config files. var hujsonStandardize func([]byte) ([]byte, error) @@ -62,7 +72,10 @@ func Load(path string) (*Config, error) { c.Raw, err = os.ReadFile(path) } if err != nil { - return nil, err + // Read-phase failure: the config source is absent or unreadable. + // Wrap ErrNoConfig so callers can distinguish this from a config + // that was read but failed to parse below. + return nil, fmt.Errorf("%w: %v", ErrNoConfig, err) } if buildfeatures.HasHuJSONConf && hujsonStandardize != nil { c.Std, err = hujsonStandardize(c.Raw) diff --git a/ipn/conffile/conffile_test.go b/ipn/conffile/conffile_test.go new file mode 100644 index 000000000..4a233a0c5 --- /dev/null +++ b/ipn/conffile/conffile_test.go @@ -0,0 +1,60 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package conffile + +import ( + "errors" + "os" + "path/filepath" + "testing" +) + +// TestLoadAbsentIsErrNoConfig verifies that an absent config source (here a +// missing file, the same read-phase seam the "vm:user-data" 404 goes through) +// is reported as ErrNoConfig, so callers using tailscaled's "optional:" prefix +// can boot unconfigured instead of failing. +func TestLoadAbsentIsErrNoConfig(t *testing.T) { + _, err := Load(filepath.Join(t.TempDir(), "does-not-exist.json")) + if !errors.Is(err, ErrNoConfig) { + t.Fatalf("Load(missing) error = %v; want it to wrap ErrNoConfig", err) + } +} + +// TestLoadInvalidIsNotErrNoConfig verifies that a config that is present but +// invalid is NOT ErrNoConfig, so it stays fatal even in optional mode. +func TestLoadInvalidIsNotErrNoConfig(t *testing.T) { + for name, data := range map[string]string{ + "malformed": `{"Version": "alpha0"`, // truncated JSON + "unknown-version": `{"Version": "bogus9"}`, + "unknown-field": `{"Version": "alpha0", "Bogus": true}`, + } { + t.Run(name, func(t *testing.T) { + p := filepath.Join(t.TempDir(), "bad.json") + if err := os.WriteFile(p, []byte(data), 0600); err != nil { + t.Fatal(err) + } + _, err := Load(p) + if err == nil { + t.Fatalf("Load(%q) succeeded; want an error", data) + } + if errors.Is(err, ErrNoConfig) { + t.Fatalf("Load(%q) error = %v; want a parse error, not ErrNoConfig", data, err) + } + }) + } +} + +func TestLoadValid(t *testing.T) { + p := filepath.Join(t.TempDir(), "ok.json") + if err := os.WriteFile(p, []byte(`{"Version": "alpha0"}`), 0600); err != nil { + t.Fatal(err) + } + c, err := Load(p) + if err != nil { + t.Fatalf("Load(valid) error = %v; want nil", err) + } + if c.Version != "alpha0" { + t.Fatalf("Version = %q; want alpha0", c.Version) + } +}