Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions osac-metering/adapters/cmd/echo-adapter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
"github.com/go-logr/stdr"

"github.com/osac-project/osac-metering/adapters"
"github.com/osac-project/osac-metering/adapters/envutil"
)

// echoAdapter logs every event to stdout, stores it in a ring buffer
Expand Down Expand Up @@ -86,15 +87,9 @@ func (a *echoAdapter) Close() error {
}

func main() {
brokers := os.Getenv("KAFKA_BROKERS")
if brokers == "" {
log.Fatal("KAFKA_BROKERS is required (comma-separated broker list)")
}
brokers := envutil.RequireEnv("KAFKA_BROKERS")

group := os.Getenv("KAFKA_CONSUMER_GROUP")
if group == "" {
group = "echo-adapter-smoke-test"
}
group := envutil.EnvOrDefault("KAFKA_CONSUMER_GROUP", "echo-adapter-smoke-test")

flushInterval := 5 * time.Second
if v := os.Getenv("FLUSH_INTERVAL"); v != "" {
Expand All @@ -105,10 +100,7 @@ func main() {
flushInterval = d
}

metricsAddr := os.Getenv("METRICS_ADDR")
if metricsAddr == "" {
metricsAddr = ":2112"
}
metricsAddr := envutil.EnvOrDefault("METRICS_ADDR", ":2112")

bufferSize := defaultMaxEvents
if v := os.Getenv("ECHO_BUFFER_SIZE"); v != "" {
Expand Down
56 changes: 9 additions & 47 deletions osac-metering/adapters/cmd/m360-adapter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import (
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"

"github.com/go-logr/stdr"
"github.com/osac-project/osac-metering/adapters"
"github.com/osac-project/osac-metering/adapters/envutil"
)

type m360Adapter struct {
Expand Down Expand Up @@ -57,22 +57,22 @@ func (a *m360Adapter) HealthCheck(ctx context.Context) error {
func (a *m360Adapter) Close() error { return nil }

func main() {
brokers := requireEnv("KAFKA_BROKERS")
m360URL := requireEnv("M360_API_URL")
apiKeyFile := requireEnv("M360_API_KEY_FILE")
brokers := envutil.RequireEnv("KAFKA_BROKERS")
m360URL := envutil.RequireEnv("M360_API_URL")
apiKeyFile := envutil.RequireEnv("M360_API_KEY_FILE")

apiKey := readFileOrFatal(apiKeyFile)
apiVersion := envOrDefault("M360_API_VERSION", "v1")
apiKey := envutil.ReadFileOrFatal(apiKeyFile)
apiVersion := envutil.EnvOrDefault("M360_API_VERSION", "v1")

topics := adapters.AllTopics
if v := os.Getenv("KAFKA_TOPICS"); v != "" {
topics = splitAndTrim(v, ",")
topics = envutil.SplitAndTrim(v, ",")
if len(topics) == 0 {
log.Fatal("KAFKA_TOPICS must contain at least one topic")
}
}

group := envOrDefault("KAFKA_CONSUMER_GROUP", "m360-adapter")
group := envutil.EnvOrDefault("KAFKA_CONSUMER_GROUP", "m360-adapter")

var flushInterval time.Duration
if v := os.Getenv("FLUSH_INTERVAL"); v != "" {
Expand All @@ -86,7 +86,7 @@ func main() {
flushInterval = d
}

metricsAddr := envOrDefault("METRICS_ADDR", ":2112")
metricsAddr := envutil.EnvOrDefault("METRICS_ADDR", ":2112")

// TLS defaults to true (!= "false") — safer for production.
// Note: echo-adapter uses == "true" (defaults off) since it runs in
Expand Down Expand Up @@ -157,41 +157,3 @@ func main() {

log.Print("m360 adapter shut down cleanly")
}

func requireEnv(key string) string {
v := os.Getenv(key)
if v == "" {
log.Fatalf("%s is required", key)
}
return v
}

func envOrDefault(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}

func readFileOrFatal(path string) string {
data, err := os.ReadFile(path)
if err != nil {
log.Fatalf("reading %s: %v", path, err)
}
trimmed := strings.TrimSpace(string(data))
if trimmed == "" {
log.Fatalf("%s is empty", path)
}
return trimmed
}

func splitAndTrim(s, sep string) []string {
parts := strings.Split(s, sep)
result := parts[:0]
for _, p := range parts {
if trimmed := strings.TrimSpace(p); trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
67 changes: 67 additions & 0 deletions osac-metering/adapters/envutil/envutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
Copyright (c) 2026 Red Hat, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0
*/

// Package envutil provides common environment variable and file-reading
// helpers for adapter main functions. All functions that encounter an
// error call log.Fatalf, making them suitable for use during process
// startup only.
package envutil

import (
"log"
"os"
"strings"
)

// RequireEnv returns the value of the named environment variable or
// terminates the process if it is empty or unset.
func RequireEnv(key string) string {
v := os.Getenv(key)
if v == "" {
log.Fatalf("%s is required", key)
}
return v
}

// EnvOrDefault returns the value of the named environment variable, or
// fallback if the variable is empty or unset.
func EnvOrDefault(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}

// ReadFileOrFatal reads the file at path, trims whitespace, and returns
// the result. It terminates the process if the file cannot be read or
// is empty after trimming.
func ReadFileOrFatal(path string) string {
data, err := os.ReadFile(path)
if err != nil {
log.Fatalf("reading %s: %v", path, err)
}
trimmed := strings.TrimSpace(string(data))
if trimmed == "" {
log.Fatalf("%s is empty", path)
}
return trimmed
}

// SplitAndTrim splits s by sep, trims whitespace from each part, and
// returns only the non-empty parts.
func SplitAndTrim(s, sep string) []string {
parts := strings.Split(s, sep)
var result []string
for _, p := range parts {
if trimmed := strings.TrimSpace(p); trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
76 changes: 76 additions & 0 deletions osac-metering/adapters/envutil/envutil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
Copyright (c) 2026 Red Hat, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0
*/

package envutil_test

import (
"os"
"path/filepath"
"testing"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] test-suite-organization

Every other test package in the adapters module places the Ginkgo suite bootstrap in a dedicated *_suite_test.go file. Here, the bootstrap is inlined alongside the specs.

Suggested fix: Extract the bootstrap into envutil_suite_test.go.


. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/osac-project/osac-metering/adapters/envutil"
)

func TestEnvutil(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Envutil Suite")
}

var _ = Describe("RequireEnv", func() {
It("returns the value when set", func() {
GinkgoT().Setenv("TEST_REQUIRE_ENV_KEY", "hello")
Expect(envutil.RequireEnv("TEST_REQUIRE_ENV_KEY")).To(Equal("hello"))
})
})

var _ = Describe("EnvOrDefault", func() {
It("returns the env value when set", func() {
GinkgoT().Setenv("TEST_ENV_OR_DEFAULT_KEY", "custom")
Expect(envutil.EnvOrDefault("TEST_ENV_OR_DEFAULT_KEY", "fallback")).To(Equal("custom"))
})

It("returns the fallback when unset", func() {
Expect(envutil.EnvOrDefault("TEST_ENV_OR_DEFAULT_UNSET", "fallback")).To(Equal("fallback"))
})

It("returns the fallback when empty", func() {
GinkgoT().Setenv("TEST_ENV_OR_DEFAULT_KEY", "")
Expect(envutil.EnvOrDefault("TEST_ENV_OR_DEFAULT_KEY", "fallback")).To(Equal("fallback"))
})
})

var _ = Describe("ReadFileOrFatal", func() {
It("reads and trims file content", func() {
dir := GinkgoT().TempDir()
path := filepath.Join(dir, "secret")
Expect(os.WriteFile(path, []byte(" my-secret\n"), 0o600)).To(Succeed())
Expect(envutil.ReadFileOrFatal(path)).To(Equal("my-secret"))
})
})

var _ = Describe("SplitAndTrim", func() {
It("splits and trims a comma-separated string", func() {
Expect(envutil.SplitAndTrim(" a , b , c ", ",")).To(Equal([]string{"a", "b", "c"}))
})

It("drops empty segments", func() {
Expect(envutil.SplitAndTrim("a,,b,", ",")).To(Equal([]string{"a", "b"}))
})

It("returns empty slice for whitespace-only input", func() {
Expect(envutil.SplitAndTrim(" , , ", ",")).To(BeEmpty())
})

It("handles a single value", func() {
Expect(envutil.SplitAndTrim("solo", ",")).To(Equal([]string{"solo"}))
})
})
Loading