feat: add optional basic auth support for Loki client
Some Loki deployments (e.g., behind a reverse proxy or Grafana Cloud) require HTTP Basic Authentication. This adds optional --loki-username and --loki-password flags (and corresponding env vars) to the lab-monitoring server, along with NixOS module options for secure credential management via systemd LoadCredential. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,7 @@ func setupTestServer(t *testing.T, promHandler, amHandler http.HandlerFunc, loki
|
||||
var lokiSrv *httptest.Server
|
||||
if len(lokiHandler) > 0 && lokiHandler[0] != nil {
|
||||
lokiSrv = httptest.NewServer(lokiHandler[0])
|
||||
loki = NewLokiClient(lokiSrv.URL)
|
||||
loki = NewLokiClient(LokiClientOptions{BaseURL: lokiSrv.URL})
|
||||
}
|
||||
|
||||
RegisterHandlers(server, prom, am, loki, HandlerOptions{EnableSilences: true})
|
||||
|
||||
@@ -11,16 +11,27 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// LokiClientOptions configures the Loki client.
|
||||
type LokiClientOptions struct {
|
||||
BaseURL string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// LokiClient is an HTTP client for the Loki API.
|
||||
type LokiClient struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewLokiClient creates a new Loki API client.
|
||||
func NewLokiClient(baseURL string) *LokiClient {
|
||||
func NewLokiClient(opts LokiClientOptions) *LokiClient {
|
||||
return &LokiClient{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
baseURL: strings.TrimRight(opts.BaseURL, "/"),
|
||||
username: opts.Username,
|
||||
password: opts.Password,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
@@ -94,6 +105,10 @@ func (c *LokiClient) get(ctx context.Context, path string, params url.Values) (j
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
if c.username != "" {
|
||||
req.SetBasicAuth(c.username, c.password)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestLokiClient_QueryRange(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(srv.URL)
|
||||
client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
|
||||
start := time.Unix(0, 1234567890000000000)
|
||||
end := time.Unix(0, 1234567899000000000)
|
||||
data, err := client.QueryRange(context.Background(), `{job="varlogs"}`, start, end, 10, "backward")
|
||||
@@ -78,7 +78,7 @@ func TestLokiClient_QueryRangeError(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(srv.URL)
|
||||
client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
|
||||
_, err := client.QueryRange(context.Background(), "invalid{", time.Now().Add(-time.Hour), time.Now(), 100, "backward")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
@@ -102,7 +102,7 @@ func TestLokiClient_Labels(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(srv.URL)
|
||||
client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
|
||||
labels, err := client.Labels(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -130,7 +130,7 @@ func TestLokiClient_LabelValues(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(srv.URL)
|
||||
client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
|
||||
values, err := client.LabelValues(context.Background(), "job")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -144,6 +144,65 @@ func TestLokiClient_LabelValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLokiClient_BasicAuth(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
t.Error("expected basic auth to be set")
|
||||
}
|
||||
if user != "myuser" {
|
||||
t.Errorf("expected username=myuser, got %s", user)
|
||||
}
|
||||
if pass != "mypass" {
|
||||
t.Errorf("expected password=mypass, got %s", pass)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"status": "success",
|
||||
"data": ["job"]
|
||||
}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(LokiClientOptions{
|
||||
BaseURL: srv.URL,
|
||||
Username: "myuser",
|
||||
Password: "mypass",
|
||||
})
|
||||
labels, err := client.Labels(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(labels) != 1 || labels[0] != "job" {
|
||||
t.Errorf("unexpected labels: %v", labels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLokiClient_NoAuthWhenNoCredentials(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, _, ok := r.BasicAuth(); ok {
|
||||
t.Error("expected no basic auth header, but it was set")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"status": "success",
|
||||
"data": ["job"]
|
||||
}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
|
||||
labels, err := client.Labels(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(labels) != 1 || labels[0] != "job" {
|
||||
t.Errorf("unexpected labels: %v", labels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLokiClient_HTTPError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
@@ -151,7 +210,7 @@ func TestLokiClient_HTTPError(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(srv.URL)
|
||||
client := NewLokiClient(LokiClientOptions{BaseURL: srv.URL})
|
||||
_, err := client.QueryRange(context.Background(), `{job="test"}`, time.Now().Add(-time.Hour), time.Now(), 100, "backward")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
|
||||
Reference in New Issue
Block a user