Skip to content

Commit

Permalink
agent: add server_metadata
Browse files Browse the repository at this point in the history
Signed-off-by: Dan Bond <danbond@protonmail.com>
  • Loading branch information
loshz committed May 9, 2023
1 parent 5ef0eb6 commit ec03223
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 25 deletions.
41 changes: 16 additions & 25 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
Expand Down Expand Up @@ -4559,13 +4558,20 @@ func (a *Agent) persistServerLastSeen() {
// Reset the timer to the larger periodic interval.
t.Reset(1 * time.Hour)

// TODO: should we do this properly using binary encoding?
now := strconv.FormatInt(time.Now().Unix(), 10)
f, err := consul.OpenServerMetadata(file)
if err != nil {
a.logger.Error("failed to open existing server metadata: %w", err)
continue
}

if err := os.WriteFile(file, []byte(now), 0600); err != nil {
if err := consul.WriteServerMetadata(f); err != nil {
f.Close()
// TODO: should we exit if this has happened too many times?
a.logger.Error("failed to write last seen timestamp: %w", err)
a.logger.Error("failed to write server metadata: %w", err)
continue
}

f.Close()
case <-a.shutdownCh:
return
}
Expand All @@ -4581,30 +4587,15 @@ func (a *Agent) persistServerLastSeen() {
// Example: if the server recorded a last seen timestamp of now-7d, and we configure a max age
// of 3d, then we should prevent the server from rejoining.
func (a *Agent) checkServerLastSeen() error {
file := filepath.Join(a.config.DataDir, serverLastSeenFile)
filename := filepath.Join(a.config.DataDir, consul.ServerMetadataFile)

// Check if the last seen timestamp file exists, and return early if it doesn't
// as this indicates the server is starting for the first time.
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
return nil
}

// Read timestamp from last seen file.
b, err := os.ReadFile(file)
// Read server metadata file.
md, err := consul.ReadServerMetadata(filename)
if err != nil {
return fmt.Errorf("error reading server last seen timestamp: %w", err)
}

// TODO: again, we probably want to do this more efficiently using binary encoding.
i, _ := strconv.Atoi(string(b))
lastSeen := time.Unix(int64(i), 0)
maxAge := time.Now().Add(-a.config.ServerRejoinAgeMax)

if lastSeen.Before(maxAge) {
return fmt.Errorf("server has not been seen since %s, will not rejoin", lastSeen.Format(time.DateTime))
return fmt.Errorf("error reading server metadata: %w", err)
}

return nil
return md.CheckLastSeen(a.config.ServerRejoinAgeMax)
}

func listenerPortKey(svcID structs.ServiceID, checkID structs.CheckID) string {
Expand Down
69 changes: 69 additions & 0 deletions agent/consul/server_metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package consul

import (
"encoding/json"
"fmt"
"io"
"os"
"time"
)

const ServerMetadataFile = "server_metadata.json"

// ServerMetadata ...
type ServerMetadata struct {
LastSeenUnix int64 `json:"last_seen_unix"`
}

func (md *ServerMetadata) CheckLastSeen(d time.Duration) error {
lastSeen := time.Unix(md.LastSeenUnix, 0)
maxAge := time.Now().Add(-d)

if lastSeen.Before(maxAge) {
return fmt.Errorf("server is older than specified %s max age", d)
}

return nil
}

func OpenServerMetadata(filename string) (io.WriteCloser, error) {
return os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
}

func ReadServerMetadata(filename string) (*ServerMetadata, error) {
b, err := os.ReadFile(filename)
if err != nil {
// Return early if it doesn't as this indicates the server is starting for the first time.
if err == os.ErrNotExist {
return nil, nil
}
return nil, err
}

var md ServerMetadata
if err := json.Unmarshal(b, &md); err != nil {
return nil, err
}

return &md, nil
}

func WriteServerMetadata(w io.Writer) error {
md := &ServerMetadata{
LastSeenUnix: time.Now().Unix(),
}

b, err := json.Marshal(md)
if err != nil {
return err
}

if _, err := w.Write(b); err != nil {
return err
}

return nil
}
68 changes: 68 additions & 0 deletions agent/consul/server_metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package consul

import (
"bytes"
"errors"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

type mockServerMetadataWriter struct {
writeErr error
}

func (m *mockServerMetadataWriter) Write(p []byte) (n int, err error) {
if m.writeErr != nil {
return 0, m.writeErr
}

return 1, nil
}

func TestServerMetadata(t *testing.T) {
now := time.Now()

t.Run("TestCheckLastSeenError", func(t *testing.T) {
// Create a server that is 24 hours old.
md := &ServerMetadata{
LastSeenUnix: now.Add(-24 * time.Hour).Unix(),
}

err := md.CheckLastSeen(1 * time.Hour)
assert.Error(t, err)
})

t.Run("TestCheckLastSeenOK", func(t *testing.T) {
// Create a server that is 24 hours old.
md := &ServerMetadata{
LastSeenUnix: now.Add(-1 * time.Hour).Unix(),
}

err := md.CheckLastSeen(24 * time.Hour)
assert.NoError(t, err)
})
}

func TestWriteServerMetadata(t *testing.T) {
t.Run("TestWriteError", func(t *testing.T) {
m := &mockServerMetadataWriter{
writeErr: errors.New("write error"),
}

err := WriteServerMetadata(m)
assert.Error(t, err)
})

t.Run("TestOK", func(t *testing.T) {
b := new(bytes.Buffer)

err := WriteServerMetadata(b)
assert.NoError(t, err)
assert.True(t, b.Len() > 0)
})
}

0 comments on commit ec03223

Please sign in to comment.