Просмотр исходного кода

Add /version endpoint and --version cli flag

Closes #18

Allow the server operator a way to view the current RAS version
Josh Knight 1 год назад
Родитель
Сommit
79b20ab11d
5 измененных файлов с 105 добавлено и 1 удалено
  1. 22 0
      api.yml
  2. 25 1
      cmd/server/main.go
  3. 6 0
      config/config.go
  4. 15 0
      server/http/mgmt_api.go
  5. 37 0
      server/http/mgmt_api_test.go

+ 22 - 0
api.yml

@@ -363,3 +363,25 @@ paths:
           description: Message sent successfully.
         '400':
           description: Bad request. Invalid input data.
+
+  /version:
+    get:
+      summary: Get build information of RAS.
+      description: Retrieve the build version, git commit, and build date of the running RAS binary.
+      responses:
+        '200':
+          description: Successful response containing the build information.
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  version:
+                    type: string
+                    description: The release version number.
+                  commit:
+                    type: string
+                    description: The latest git commit hash in this build.
+                  date:
+                    type: string
+                    description: The build date and timestamp in RFC3339 format.

+ 25 - 1
cmd/server/main.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"flag"
 	"fmt"
 	"log/slog"
 	"net"
@@ -18,7 +19,30 @@ import (
 	"github.com/kelseyhightower/envconfig"
 )
 
+// Default build fields are populated by GoReleaser
+var (
+	version = "dev"
+	commit  = "none"
+	date    = "unknown"
+)
+
 func main() {
+	bld := config.Build{
+		Version: version,
+		Commit:  commit,
+		Date:    date,
+	}
+	var showVersion bool
+	flag.BoolVar(&showVersion, "version", false, "Display build information")
+	flag.BoolVar(&showVersion, "v", false, "Display build information")
+	flag.Parse()
+	if showVersion {
+		fmt.Printf("%-10s %s\n", "version:", bld.Version)
+		fmt.Printf("%-10s %s\n", "commit:", bld.Commit)
+		fmt.Printf("%-10s %s\n", "date:", bld.Date)
+		os.Exit(0)
+	}
+
 	var cfg config.Config
 	err := envconfig.Process("", &cfg)
 	if err != nil {
@@ -47,7 +71,7 @@ func main() {
 	wg.Add(7)
 
 	go func() {
-		http.StartManagementAPI(cfg, feedbagStore, sessionManager, feedbagStore, feedbagStore, chatSessionManager, sessionManager, feedbagStore, feedbagStore, feedbagStore, feedbagStore, logger)
+		http.StartManagementAPI(bld, cfg, feedbagStore, sessionManager, feedbagStore, feedbagStore, chatSessionManager, sessionManager, feedbagStore, feedbagStore, feedbagStore, feedbagStore, logger)
 		wg.Done()
 	}()
 	go func(logger *slog.Logger) {

+ 6 - 0
config/config.go

@@ -18,3 +18,9 @@ type Config struct {
 	LogLevel    string `envconfig:"LOG_LEVEL" required:"true" val:"info" description:"Set logging granularity. Possible values: 'trace', 'debug', 'info', 'warn', 'error'."`
 	OSCARHost   string `envconfig:"OSCAR_HOST" required:"true" val:"127.0.0.1" description:"The hostname that AIM clients connect to in order to reach OSCAR services (auth, BOS, BUCP, etc). Make sure the hostname is reachable by all clients. For local development, the default loopback address should work provided the server and AIM client(s) are running on the same machine. For LAN-only clients, a private IP address (e.g. 192.168..) or hostname should suffice. For clients connecting over the Internet, specify your public IP address and ensure that TCP ports 5190-5196 are open on your firewall."`
 }
+
+type Build struct {
+	Version string `json:"version"`
+	Commit  string `json:"commit"`
+	Date    string `json:"date"`
+}

+ 15 - 0
server/http/mgmt_api.go

@@ -21,6 +21,7 @@ import (
 )
 
 func StartManagementAPI(
+	bld config.Build,
 	cfg config.Config,
 	userManager UserManager,
 	sessionRetriever SessionRetriever,
@@ -96,6 +97,11 @@ func StartManagementAPI(
 		postInstantMessageHandler(w, r, messageRelayer, logger)
 	})
 
+	// Handlers for '/version' route
+	mux.HandleFunc("GET /version", func(w http.ResponseWriter, r *http.Request) {
+		getVersionHandler(w, bld)
+	})
+
 	addr := net.JoinHostPort(cfg.ApiHost, cfg.ApiPort)
 	logger.Info("starting management API server", "addr", addr)
 	if err := http.ListenAndServe(addr, mux); err != nil {
@@ -569,3 +575,12 @@ func getUserAccountHandler(w http.ResponseWriter, r *http.Request, userManager U
 		return
 	}
 }
+
+// getVersionHandler handles the GET /version endpoint.
+func getVersionHandler(w http.ResponseWriter, bld config.Build) {
+	w.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(w).Encode(bld); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}

+ 37 - 0
server/http/mgmt_api_test.go

@@ -14,6 +14,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 
+	"github.com/mk6i/retro-aim-server/config"
 	"github.com/mk6i/retro-aim-server/state"
 	"github.com/mk6i/retro-aim-server/wire"
 )
@@ -1248,3 +1249,39 @@ func TestInstantMessageHandler_POST(t *testing.T) {
 		})
 	}
 }
+
+func TestVersionHandler_GET(t *testing.T) {
+	tt := []struct {
+		name       string
+		want       string
+		statusCode int
+		buildInfo  config.Build
+	}{
+		{
+			name:       "get ras version",
+			want:       `{"version":"13.3.7","commit":"asdfASDF12345678","date":"2024-03-01"}`,
+			statusCode: http.StatusOK,
+			buildInfo: config.Build{
+				Version: "13.3.7",
+				Commit:  "asdfASDF12345678",
+				Date:    "2024-03-01",
+			},
+		},
+	}
+
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			responseRecorder := httptest.NewRecorder()
+
+			getVersionHandler(responseRecorder, tc.buildInfo)
+
+			if responseRecorder.Code != tc.statusCode {
+				t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
+			}
+
+			if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
+				t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
+			}
+		})
+	}
+}