db, types: add a nodes column for the ephemeral flag

The migration fills it from the key each node references.
This commit is contained in:
Kristoffer Dalby committed 2026-11-02 17:46:53 +00:00
1 parent 95efb8cef1
commit 5827e4b73c
6 files changed
+53 -2

No files matched your search

+37
View File
@@ -294,6 +294,43 @@ WHERE tags IS NOT NULL AND tags != '[]' AND tags != '' AND tags != 'null'
},
Rollback: func(db *gorm.DB) error { return nil },
},
{
// Store a node's ephemeral flag on the node, taken from the
// key it references, so a login with another key keeps it.
ID: "202610211300-node-ephemeral",
Migrate: func(tx *gorm.DB) error {
// The column and backfill commit together, before gormigrate
// records the migration; a stop in between leaves both done.
if tx.Migrator().HasColumn(&types.Node{}, "ephemeral") {
return nil
}
return tx.Transaction(func(tx *gorm.DB) error {
colType := "numeric"
if tx.Name() != sqlite.DriverName {
colType = "boolean"
}
err := tx.Exec(`ALTER TABLE nodes ADD COLUMN ephemeral ` + colType + ` DEFAULT false`).Error
if err != nil {
return fmt.Errorf("adding ephemeral to nodes: %w", err)
}
err = tx.Exec(`
UPDATE nodes SET ephemeral = true
WHERE auth_key_id IN (
SELECT id FROM credentials WHERE kind = ? AND ephemeral = true
);
`, types.CredentialPreAuthKey).Error
if err != nil {
return fmt.Errorf("backfilling ephemeral on nodes: %w", err)
}
return nil
})
},
Rollback: func(db *gorm.DB) error { return nil },
},
},
)
+1
View File
@@ -86,6 +86,7 @@ CREATE TABLE nodes(
register_method text,
tags text,
auth_key_id integer,
ephemeral numeric DEFAULT false,
last_seen datetime,
expiry datetime,
approved_routes text,
+2 -1
View File
@@ -1965,7 +1965,8 @@ var nodeFieldImpact = map[string]string{
"MachineKey": "payload", "NodeKey": "payload", "DiscoKey": "payload",
"Endpoints": "payload", "Hostname": "payload", "GivenName": "payload",
"RegisterMethod": "payload", "AuthKeyID": "payload", "AuthKey": "payload",
"Expiry": "election", "LastSeen": "payload", "CreatedAt": "payload",
"Ephemeral": "payload",
"Expiry": "election", "LastSeen": "payload", "CreatedAt": "payload",
"UpdatedAt": "payload", "DeletedAt": "payload",
"ActiveSessions": "payload", "SessionEpoch": "payload", "SessionStart": "election",
}
+5
View File
@@ -173,6 +173,11 @@ type Node struct {
AuthKeyID *uint64 `sql:"DEFAULT:NULL"`
AuthKey *Credential
// Ephemeral is taken from the key the node was created with. Logging in
// again with another key keeps it, as Tailscale does; a user claiming a
// tagged node clears it.
Ephemeral bool `gorm:"default:false"`
Expiry *time.Time
// LastSeen is when the node was last in contact with
+1
View File
@@ -99,6 +99,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
Tags Strings
AuthKeyID *uint64
AuthKey *Credential
Ephemeral bool
Expiry *time.Time
LastSeen *time.Time
ApprovedRoutes Prefixes
+7 -1
View File
@@ -236,7 +236,12 @@ func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) }
// authkey), which is what auth_key_id references.
func (v NodeView) AuthKeyID() views.ValuePointer[uint64] { return views.ValuePointerOf(v.ж.AuthKeyID) }
func (v NodeView) AuthKey() CredentialView { return v.ж.AuthKey.View() }
func (v NodeView) AuthKey() CredentialView { return v.ж.AuthKey.View() }
// Ephemeral is taken from the key the node was created with. Logging in
// again with another key keeps it, as Tailscale does; a user claiming a
// tagged node clears it.
func (v NodeView) Ephemeral() bool { return v.ж.Ephemeral }
func (v NodeView) Expiry() views.ValuePointer[time.Time] { return views.ValuePointerOf(v.ж.Expiry) }
// LastSeen is when the node was last in contact with
@@ -314,6 +319,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
Tags Strings
AuthKeyID *uint64
AuthKey *Credential
Ephemeral bool
Expiry *time.Time
LastSeen *time.Time
ApprovedRoutes Prefixes