diff --git a/cmd/root.go b/cmd/root.go index e950480..12e9ae2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -89,6 +89,7 @@ func initLogger() *zap.SugaredLogger { } log := zap.Must(logCfg.Build()) + _ = zap.ReplaceGlobals(log) // make the logger accessible globally by zap.L() (sugared with zap.S()) return log.Sugar() } diff --git a/internal/inventory/device_components.go b/internal/inventory/device_components.go index 27e34cf..52c1f67 100644 --- a/internal/inventory/device_components.go +++ b/internal/inventory/device_components.go @@ -3,6 +3,7 @@ package inventory import ( "context" "database/sql" + "encoding/json" "github.com/bmc-toolbox/common" rivets "github.com/metal-toolbox/rivets/types" @@ -10,6 +11,7 @@ import ( "github.com/volatiletech/null/v8" "github.com/volatiletech/sqlboiler/v4/boil" "github.com/volatiletech/sqlboiler/v4/queries/qm" + "go.uber.org/zap" "github.com/metal-toolbox/fleetdb/internal/dbtools" "github.com/metal-toolbox/fleetdb/internal/metrics" @@ -211,10 +213,21 @@ func componentsFromDatabase(ctx context.Context, exec boil.ContextExecutor, var comps []*rivets.Component + var ute *json.UnmarshalTypeError for _, rec := range records { // attributes/firmware/status might not be stored because it was missing in the original data. attr, err := retrieveComponentAttributes(ctx, exec, rec.ID, getAttributeNamespace(inband)) - if err != nil { + switch { + case err == nil, errors.Is(err, sql.ErrNoRows): + case errors.As(err, &ute): + // attributes are a bit of the wild-west. if the JSON we stored doesn't deserialize + // cleanly into an attributes structure, just complain about it but don't stop. + zap.L().With( + zap.String("server.id", deviceID), + zap.String("component.id", rec.ID), + zap.String("component.type", rec.Name.String), + ).Warn("bad json attributes") + default: return nil, errors.Wrap(err, "retrieving "+rec.Name.String+"-"+rec.ID+" attributes"+":"+err.Error()) } diff --git a/internal/inventory/device_components_test.go b/internal/inventory/device_components_test.go index d97a7b9..9759d96 100644 --- a/internal/inventory/device_components_test.go +++ b/internal/inventory/device_components_test.go @@ -4,6 +4,7 @@ package inventory import ( "context" + "encoding/json" "testing" "github.com/bmc-toolbox/common" @@ -277,4 +278,70 @@ func TestComposeComponentRecords(t *testing.T) { require.NoError(t, err) require.Len(t, comps, 1) }) + t.Run("nonconforming existing data", func(t *testing.T) { + // write an attribute record that doesn't have rivets.ComponentAttribute as a basis + srvUUID := mustCreateServerRecord(t, db, "bad-attribute-json") + + var inband bool + attributeNS := getAttributeNamespace(inband) + + slug := common.SlugBIOS + + orig := &rivets.Component{ + // this can be any real slug, but *must* be a real slug, otherwise + // we will panic on the slug -> type-id lookup. + Name: slug, + Vendor: "the-vendor", + Serial: "some-serial-number", + Firmware: &common.Firmware{ + Installed: "old-version", + }, + Status: &common.Status{ + State: "OK", + Health: "decent", + }, + } + + tx := db.MustBegin() + err := composeRecords(context.TODO(), tx, orig, inband, srvUUID.String()) + require.NoError(t, err) + _ = tx.Commit() + + // add the crazy attribute record + compRecs, err := models.ServerComponents( + models.ServerComponentWhere.Name.EQ(null.StringFrom(slug)), + models.ServerComponentWhere.ServerID.EQ(srvUUID.String()), + ).All(context.TODO(), db) + + require.NoError(t, err) + require.Len(t, compRecs, 1) + // get id and inject attributes + compID := compRecs[0].ID + badData := []map[string]string{ + { + "msg": "this is not a rivets component attributes structure", + }, + { + "msg": "this is also not a rivets component attributes structure", + }, + } + payload, err := json.Marshal(badData) + require.NoError(t, err) + + err = updateAnyAttribute(context.TODO(), db, false, compID, attributeNS, payload) + require.NoError(t, err) + + // be pedantic and validate that there is an attribute record + attrs, err := models.Attributes( + models.AttributeWhere.ServerComponentID.EQ(null.StringFrom(compID)), + models.AttributeWhere.Namespace.EQ(attributeNS), + ).All(context.TODO(), db) + require.NoError(t, err) + require.Len(t, attrs, 1) + + // now ask for this component via the API + comps, err := componentsFromDatabase(context.TODO(), db, inband, srvUUID.String()) + require.NoError(t, err, "received error with type %T", err) + require.Len(t, comps, 1) + }) }