Sfoglia il codice sorgente

Merge commit '4d117aabe116695ddd11100497827983b1892959' into feature/kubemodel

# Conflicts:
#	core/pkg/exporter/encoder.go
#	core/pkg/model/kubemodel/device.go
#	core/pkg/model/kubemodel/device_usage.go
#	core/pkg/model/kubemodel/kubemodel.go
#	core/pkg/model/kubemodel/kubemodel_codecs.go
#	core/pkg/model/kubemodel/owner.go
#	core/pkg/model/kubemodel/service.go
#	core/pkg/opencost/exporter/controllers.go
Sean Holcomb 1 giorno fa
parent
commit
07848b7166
74 ha cambiato i file con 4700 aggiunte e 537 eliminazioni
  1. 1 0
      Dockerfile.cross
  2. 30 0
      Dockerfile.debug
  3. 18 0
      README.md
  4. 22 0
      THIRD_PARTY_LICENSES.txt
  5. 17 0
      configs/stackit.json
  6. 12 0
      core/pkg/autocomplete/filter.go
  7. 22 0
      core/pkg/autocomplete/filter_test.go
  8. 4 0
      core/pkg/autocomplete/normalize.go
  9. 5 1
      core/pkg/autocomplete/parse.go
  10. 20 0
      core/pkg/autocomplete/request_test.go
  11. 1 1
      core/pkg/exporter/controller.go
  12. 100 2
      core/pkg/exporter/decoder_test.go
  13. 90 6
      core/pkg/exporter/encoder.go
  14. 30 0
      core/pkg/exporter/exporter.go
  15. 106 0
      core/pkg/exporter/exporter_test.go
  16. 1 1
      core/pkg/model/kubemodel/kubemodel.go
  17. 390 149
      core/pkg/model/kubemodel/kubemodel_codecs.go
  18. 5 5
      core/pkg/model/kubemodel/namespace.go
  19. 1 0
      core/pkg/model/kubemodel/owner.go
  20. 19 0
      core/pkg/model/kubemodel/service.go
  21. 16 3
      core/pkg/opencost/allocation.go
  22. 76 0
      core/pkg/opencost/allocation_test.go
  23. 5 0
      core/pkg/opencost/assetprops.go
  24. 33 4
      core/pkg/opencost/exporter/controllers.go
  25. 5 0
      core/pkg/opencost/exporter/exporter_test.go
  26. 138 0
      core/pkg/opencost/exporter/exporters.go
  27. 355 133
      core/pkg/opencost/opencost_codecs.go
  28. 26 0
      core/pkg/storage/azurestorage.go
  29. 25 0
      core/pkg/storage/bucketstorage.go
  30. 32 0
      core/pkg/storage/clusterstorage.go
  31. 112 0
      core/pkg/storage/clusterstorage_test.go
  32. 19 0
      core/pkg/storage/filestorage.go
  33. 6 0
      core/pkg/storage/filestorage_test.go
  34. 10 0
      core/pkg/storage/gcsstorage.go
  35. 44 0
      core/pkg/storage/memorystorage.go
  36. 5 0
      core/pkg/storage/memorystorage_test.go
  37. 5 0
      core/pkg/storage/prefixedbucketstorage.go
  38. 10 0
      core/pkg/storage/prefixedbucketstorage_test.go
  39. 29 1
      core/pkg/storage/s3storage.go
  40. 4 0
      core/pkg/storage/storage.go
  41. 84 0
      core/pkg/storage/test.go
  42. 273 82
      core/pkg/util/buffer.go
  43. 45 0
      core/pkg/util/buffer_test.go
  44. 110 8
      core/pkg/util/bufferhelper.go
  45. 24 0
      core/pkg/util/fileutil/locks_unix.go
  46. 15 0
      core/pkg/util/fileutil/locks_windows.go
  47. 54 0
      core/pkg/util/fileutil/writer.go
  48. 6 4
      go.mod
  49. 12 8
      go.sum
  50. 295 101
      modules/collector-source/pkg/metric/metric_codecs.go
  51. 1 0
      modules/prometheus-source/pkg/prom/clustermap.go
  52. 1 1
      pkg/allocation/autocompletequeryservice.go
  53. 2 0
      pkg/allocation/autocompletequeryservice_test.go
  54. 1 1
      pkg/asset/autocompletequeryservice.go
  55. 2 0
      pkg/asset/autocompletequeryservice_test.go
  56. 40 0
      pkg/cloud/config/configurations.go
  57. 6 0
      pkg/cloud/config/statuses.go
  58. 51 8
      pkg/cloud/gcp/provider.go
  59. 56 3
      pkg/cloud/gcp/provider_test.go
  60. 23 4
      pkg/cloud/provider/provider.go
  61. 92 0
      pkg/cloud/stackit/costconfiguration.go
  62. 228 0
      pkg/cloud/stackit/costintegration.go
  63. 109 0
      pkg/cloud/stackit/costintegration_test.go
  64. 321 0
      pkg/cloud/stackit/pim.go
  65. 247 0
      pkg/cloud/stackit/pim_test.go
  66. 390 0
      pkg/cloud/stackit/provider.go
  67. 192 0
      pkg/cloud/stackit/provider_test.go
  68. 7 1
      pkg/cloudcost/ingestor.go
  69. 47 0
      pkg/cloudcost/ingestor_test.go
  70. 5 0
      pkg/cloudcost/integration.go
  71. 20 3
      pkg/cmd/costmodel/costmodel.go
  72. 1 0
      pkg/env/costmodel.go
  73. 47 7
      pkg/mcp/server.go
  74. 44 0
      pkg/mcp/server_test.go

+ 1 - 0
Dockerfile.cross

@@ -24,6 +24,7 @@ ADD --chmod=500 ./configs/gcp.json /models/gcp.json
 ADD --chmod=500 ./configs/alibaba.json /models/alibaba.json
 ADD --chmod=500 ./configs/oracle.json /models/oracle.json
 ADD --chmod=500 ./configs/otc.json /models/otc.json
+ADD --chmod=500 ./configs/stackit.json /models/stackit.json
 RUN chown -R 1001:1001 /models
 
 COPY ${binarypath} /go/bin/app

+ 30 - 0
Dockerfile.debug

@@ -0,0 +1,30 @@
+# This dockerfile is for development purposes only; do not use this for production deployments
+FROM golang:alpine
+# The prebuilt binary path. This Dockerfile assumes the binary will be built
+# outside of Docker.
+ARG binary_path
+
+LABEL org.opencontainers.image.description="Cross-cloud cost allocation models for Kubernetes workloads"
+LABEL org.opencontainers.image.documentation=https://opencost.io/docs/
+LABEL org.opencontainers.image.licenses=Apache-2.0
+LABEL org.opencontainers.image.source=https://github.com/opencost/opencost
+LABEL org.opencontainers.image.title=kubecost-cost-model
+LABEL org.opencontainers.image.url=https://opencost.io
+
+WORKDIR /app
+RUN apk add --update --no-cache ca-certificates
+RUN go install github.com/go-delve/delve/cmd/dlv@latest
+
+ADD --chmod=644 ./THIRD_PARTY_LICENSES.txt /THIRD_PARTY_LICENSES.txt
+ADD --chmod=644 ./configs/default.json /models/default.json
+ADD --chmod=644 ./configs/azure.json /models/azure.json
+ADD --chmod=644 ./configs/aws.json /models/aws.json
+ADD --chmod=644 ./configs/gcp.json /models/gcp.json
+ADD --chmod=644 ./configs/alibaba.json /models/alibaba.json
+ADD --chmod=644 ./configs/oracle.json /models/oracle.json
+ADD --chmod=644 ./configs/otc.json /models/otc.json
+
+COPY ${binary_path} main
+
+ENTRYPOINT ["/go/bin/dlv exec --listen=:40000 --api-version=2 --headless=true --accept-multiclient --log --continue /app/main"]
+EXPOSE 9003 40000

+ 18 - 0
README.md

@@ -223,6 +223,16 @@ Retrieve cloud cost data with provider, service, and region filtering.
 - `region` (optional): Filter by region (e.g., "us-west-1", "us-central1")
 - `accountID` (optional): Filter by account ID
 
+#### `get_efficiency`
+Retrieve resource efficiency metrics with rightsizing recommendations and cost savings analysis.
+
+**Parameters:**
+- `window` (required): Time window (e.g., "7d", "1h", "30m")
+- `aggregate` (optional): Aggregation properties (e.g., "pod", "namespace", "controller")
+- `filter` (optional): Filter expression for allocations
+- `buffer_multiplier` (optional): Buffer multiplier for recommendations (default: 1.2 for 20% headroom)
+- `step` (optional): Query step size (e.g., "1h", "6h"); smaller steps reduce peak memory by batching large windows, but may increase query time/requests
+
 ### Supported Asset Types
 
 - **Node**: Compute instances with CPU, RAM, GPU details
@@ -257,6 +267,14 @@ const cloudCosts = await mcpClient.callTool('get_cloud_costs', {
   accumulate: 'day',
   filter: 'regionID:"us-west-1"'
 });
+
+// Get efficiency metrics with rightsizing recommendations
+const efficiency = await mcpClient.callTool('get_efficiency', {
+  window: '7d',
+  aggregate: 'namespace,controller',
+  step: '6h',
+  buffer_multiplier: 1.2
+});
 ```
 
 For detailed setup instructions and advanced configuration, see the [Helm chart documentation](https://github.com/opencost/opencost-helm-chart/blob/main/charts/opencost/README.md#mcp-server).

+ 22 - 0
THIRD_PARTY_LICENSES.txt

@@ -4741,6 +4741,28 @@ Copyright 2019 Scaleway.
 
 --------------------------------- (separator) ----------------------------------
 
+== Dependency
+github.com/stackitcloud/stackit-sdk-go/core
+
+== License Type
+SPDX:Apache-2.0
+
+== Copyright
+Copyright 2023 STACKIT GmbH & Co. KG
+
+--------------------------------- (separator) ----------------------------------
+
+== Dependency
+github.com/stackitcloud/stackit-sdk-go/services/cost
+
+== License Type
+SPDX:Apache-2.0
+
+== Copyright
+Copyright 2023 STACKIT GmbH & Co. KG
+
+--------------------------------- (separator) ----------------------------------
+
 == Dependency
 github.com/shopspring/decimal
 

+ 17 - 0
configs/stackit.json

@@ -0,0 +1,17 @@
+{
+    "provider": "STACKIT",
+    "currencyCode": "EUR",
+    "description": "STACKIT cloud prices derived from g2i.1 (1vCPU/4GB) at 36.83 EUR/month (compute only)",
+    "CPU": "0.02523",
+    "spotCPU": "0.02523",
+    "RAM": "0.00631",
+    "spotRAM": "0.00631",
+    "GPU": "0",
+    "storage": "0.0000712",
+    "zoneNetworkEgress": "0.01",
+    "regionNetworkEgress": "0.01",
+    "internetNetworkEgress": "0.12",
+    "natGatewayEgress": "0.045",
+    "natGatewayIngress": "0.045",
+    "defaultLBPrice": "0"
+}

+ 12 - 0
core/pkg/autocomplete/filter.go

@@ -0,0 +1,12 @@
+package autocomplete
+
+import (
+	"github.com/opencost/opencost/core/pkg/filter"
+	"github.com/opencost/opencost/core/pkg/filter/ast"
+)
+
+// HasFilter reports whether the request carries a user-provided filter.
+// Omitted or empty filters normalize to VoidOp and return false.
+func HasFilter(f filter.Filter) bool {
+	return f != nil && f.Op() != ast.FilterOpVoid
+}

+ 22 - 0
core/pkg/autocomplete/filter_test.go

@@ -0,0 +1,22 @@
+package autocomplete
+
+import (
+	"testing"
+
+	"github.com/opencost/opencost/core/pkg/filter/ast"
+)
+
+func TestHasFilter(t *testing.T) {
+	if HasFilter(nil) {
+		t.Fatal("expected nil filter to be treated as no filter")
+	}
+	if HasFilter(&ast.VoidOp{}) {
+		t.Fatal("expected void filter to be treated as no filter")
+	}
+	if !HasFilter(&ast.EqualOp{
+		Left:  ast.Identifier{Field: ast.NewField("cluster")},
+		Right: "c1",
+	}) {
+		t.Fatal("expected non-void filter to be treated as active filter")
+	}
+}

+ 4 - 0
core/pkg/autocomplete/normalize.go

@@ -3,6 +3,7 @@ package autocomplete
 import (
 	"fmt"
 
+	"github.com/opencost/opencost/core/pkg/filter/ast"
 	"github.com/opencost/opencost/core/pkg/opencost"
 )
 
@@ -57,6 +58,9 @@ func NormalizeRequest(req *Request, validateField FieldValidator, opts Normalize
 	req.Field = field
 	req.Search = SanitizeSearch(req.Search)
 	req.Limit = limit
+	if req.Filter == nil {
+		req.Filter = &ast.VoidOp{}
+	}
 	if opts.EnsureLabelConfig && req.LabelConfig == nil {
 		req.LabelConfig = opencost.NewLabelConfig()
 	}

+ 5 - 1
core/pkg/autocomplete/parse.go

@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/opencost/opencost/core/pkg/filter"
+	"github.com/opencost/opencost/core/pkg/filter/ast"
 	"github.com/opencost/opencost/core/pkg/opencost"
 	"github.com/opencost/opencost/core/pkg/util/httputil"
 )
@@ -53,7 +54,7 @@ func ParseRequest(qp httputil.QueryParams, opts ParseOptions, validateField Fiel
 	}
 
 	filterString := qp.Get("filter", "")
-	var parsedFilter filter.Filter
+	var parsedFilter filter.Filter = &ast.VoidOp{}
 	if filterString != "" {
 		if parseFilter == nil {
 			return nil, fmt.Errorf("%w: invalid 'filter' parameter: filter parser is required", ErrBadRequest)
@@ -62,6 +63,9 @@ func ParseRequest(qp httputil.QueryParams, opts ParseOptions, validateField Fiel
 		if err != nil {
 			return nil, fmt.Errorf("%w: invalid 'filter' parameter: %w", ErrBadRequest, err)
 		}
+		if parsedFilter == nil {
+			parsedFilter = &ast.VoidOp{}
+		}
 	}
 
 	tenantID := qp.Get("tenantId", opts.DefaultTenantID)

+ 20 - 0
core/pkg/autocomplete/request_test.go

@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/opencost/opencost/core/pkg/filter"
+	"github.com/opencost/opencost/core/pkg/filter/ast"
 	"github.com/opencost/opencost/core/pkg/opencost"
 	"github.com/opencost/opencost/core/pkg/util/httputil"
 )
@@ -42,6 +43,19 @@ func TestNormalizeRequest(t *testing.T) {
 		t.Fatal("expected default label config")
 	}
 
+	nilFilterReq := &Request{
+		TenantID: "t1",
+		Field:    "label",
+		Window:   opencost.NewClosedWindow(start, start.Add(24*time.Hour)),
+	}
+	_, err = NormalizeRequest(nilFilterReq, validateTestField, NormalizeOptions{RequireTenantID: true})
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if nilFilterReq.Filter == nil || nilFilterReq.Filter.Op() != ast.FilterOpVoid {
+		t.Fatalf("expected nil filter normalized to void op, got %+v", nilFilterReq.Filter)
+	}
+
 	_, err = NormalizeRequest(nil, validateTestField, NormalizeOptions{})
 	if err == nil || !errors.Is(err, ErrBadRequest) {
 		t.Fatalf("expected nil request error, got %v", err)
@@ -96,6 +110,9 @@ func TestParseRequest(t *testing.T) {
 	if got.Field != "cluster" || got.Search != "ns" || got.TenantID != "t1" {
 		t.Fatalf("unexpected request: %+v", got)
 	}
+	if got.Filter == nil || got.Filter.Op() != ast.FilterOpVoid {
+		t.Fatalf("expected void filter when filter param omitted, got %+v", got.Filter)
+	}
 
 	_, err = ParseRequest(httputil.NewQueryParams(map[string][]string{"field": {"cluster"}}), ParseOptions{}, validateTestField, nil)
 	if err == nil || !errors.Is(err, ErrBadRequest) {
@@ -155,4 +172,7 @@ func TestParseRequest(t *testing.T) {
 	if got.Field != "cluster" {
 		t.Fatalf("unexpected request: %+v", got)
 	}
+	if got.Filter == nil || got.Filter.Op() != ast.FilterOpVoid {
+		t.Fatalf("expected nil parse result normalized to void filter, got %+v", got.Filter)
+	}
 }

+ 1 - 1
core/pkg/exporter/controller.go

@@ -115,7 +115,7 @@ func NewComputeExportController[T any](
 		source:     source,
 		resolution: resolution,
 		exporter:   exporter,
-		typeName:   reflect.TypeOf((*T)(nil)).Elem().String(),
+		typeName:   reflect.TypeFor[T]().String(),
 	}
 }
 

+ 100 - 2
core/pkg/exporter/decoder_test.go

@@ -1,6 +1,7 @@
 package exporter
 
 import (
+	"compress/gzip"
 	"reflect"
 	"testing"
 	"time"
@@ -10,6 +11,7 @@ import (
 	"github.com/opencost/opencost/core/pkg/model"
 	"github.com/opencost/opencost/core/pkg/model/pb"
 	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/storage"
 	"github.com/opencost/opencost/core/pkg/util"
 	"github.com/opencost/opencost/core/pkg/util/json"
 	"google.golang.org/protobuf/proto"
@@ -196,12 +198,12 @@ func TestGzipDecoder(t *testing.T) {
 	if err != nil {
 		t.Errorf("failed to marshal diagnostic: %s", err.Error())
 	}
-	diagCompressed, err := gZipEncode(diagRaw)
+	diagCompressed, err := gZipEncode(diagRaw, gzip.BestSpeed)
 	if err != nil {
 		t.Errorf("failed to compress diagnostic: %s", err.Error())
 	}
 
-	badCompressed, err := gZipEncode(badBytes)
+	badCompressed, err := gZipEncode(badBytes, gzip.BestSpeed)
 	if err != nil {
 		t.Errorf("failed to compress bad bytes: %s", err.Error())
 	}
@@ -289,6 +291,102 @@ func TestProtobufDecoder(t *testing.T) {
 	testProtoBufDecoder(t, ProtobufDecoder, labelsResponseTests)
 }
 
+func TestProtobufEncoderDecoderRoundTrip(t *testing.T) {
+	badBytes := generateBadBytes()
+
+	now := time.Now().UTC().Truncate(24 * time.Hour)
+	start := now.Add(-(24 * 5) * time.Hour)
+
+	store := storage.NewMemoryStorage()
+	writer, err := store.WriteStream("test.pb")
+	if err != nil {
+		t.Fatalf("failed to open writer: %s", err)
+		return
+	}
+
+	customCostSet := model.GenerateMockCustomCostSet(start, start.Add(24*time.Hour))
+
+	enc := NewProtobufEncoder[pb.CustomCostResponse]()
+	err = enc.EncodeTo(writer, customCostSet)
+	if err != nil {
+		_ = writer.Close()
+		t.Fatalf("Failed to encode to writer: %s", err)
+		return
+	}
+
+	if err = writer.Close(); err != nil {
+		t.Fatalf("failed to flush/close the writer; %s", err)
+		return
+	}
+
+	// load raw bytes from memory file system
+	customCostSetRaw, err := store.Read("test.pb")
+	if err != nil {
+		t.Errorf("failed to load custom cost set raw binary from memory disk: %s", err)
+		return
+	}
+
+	customCostTests := []decoderTestCase[pb.CustomCostResponse]{
+		{
+			name:    "custom cost valid",
+			data:    customCostSetRaw,
+			want:    customCostSet,
+			wantErr: false,
+		},
+		{
+			name:    "custom cost invalid",
+			data:    badBytes,
+			want:    nil,
+			wantErr: true,
+		},
+	}
+
+	testProtoBufDecoder(t, ProtobufDecoder, customCostTests)
+
+	labelsResponse := model.GenerateMockLabelResponse(start, pb.Resolution_RESOLUTION_1D)
+	labelsEnc := NewProtobufEncoder[pb.LabelsResponse]()
+	labelsWriter, err := store.WriteStream("test-labels.pb")
+	if err != nil {
+		t.Fatalf("failed to open labels writer: %s", err)
+		return
+	}
+
+	err = labelsEnc.EncodeTo(labelsWriter, labelsResponse)
+	if err != nil {
+		_ = labelsWriter.Close()
+		t.Fatalf("Failed to encode to labels writer: %s", err)
+		return
+	}
+
+	if err = labelsWriter.Close(); err != nil {
+		t.Fatalf("failed to flush/close the labels writer; %s", err)
+		return
+	}
+
+	labelsResponseRaw, err := store.Read("test-labels.pb")
+	if err != nil {
+		t.Fatalf("failed to marshal labels response: %s", err)
+		return
+	}
+
+	labelsResponseTests := []decoderTestCase[pb.LabelsResponse]{
+		{
+			name:    "labels response valid",
+			data:    labelsResponseRaw,
+			want:    labelsResponse,
+			wantErr: false,
+		},
+		{
+			name:    "labels response invalid",
+			data:    badBytes,
+			want:    nil,
+			wantErr: true,
+		},
+	}
+
+	testProtoBufDecoder(t, ProtobufDecoder, labelsResponseTests)
+}
+
 func testProtoBufDecoder[T any, U ProtoMessagePtr[T]](t *testing.T, decoder Decoder[T], testCases []decoderTestCase[T]) {
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {

+ 90 - 6
core/pkg/exporter/encoder.go

@@ -5,6 +5,7 @@ import (
 	"compress/gzip"
 	"encoding"
 	"fmt"
+	"io"
 
 	"github.com/opencost/opencost/core/pkg/util/json"
 	"google.golang.org/protobuf/encoding/protojson"
@@ -22,6 +23,10 @@ const (
 type Encoder[T any] interface {
 	Encode(*T) ([]byte, error)
 
+	// EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
+	// binary encoding.
+	EncodeTo(io.Writer, *T) error
+
 	// FileExt returns the file extension for the encoded data. This can be used by a pathing strategy
 	// to append the file extension when exporting the data. Returning an empty string will typically
 	// omit the file extension completely.
@@ -32,6 +37,7 @@ type Encoder[T any] interface {
 // encoding.BinaryMarshaler and are pointers to T.
 type BinaryMarshalerPtr[T any] interface {
 	encoding.BinaryMarshaler
+	MarshalBinaryTo(io.Writer) error
 	*T
 }
 
@@ -63,6 +69,13 @@ func (b *BingenEncoder[T, U]) Encode(data *T) ([]byte, error) {
 	return bingenData.MarshalBinary()
 }
 
+// EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
+// binary encoding.
+func (b *BingenEncoder[T, U]) EncodeTo(writer io.Writer, data *T) error {
+	var bingenData U = data
+	return bingenData.MarshalBinaryTo(writer)
+}
+
 // FileExt returns the configured file extension for the encoded data. This may be an empty
 // string when no file extension is configured, or a non-empty value such as "bingen".
 func (b *BingenEncoder[T, U]) FileExt() string {
@@ -83,6 +96,13 @@ func (j *JSONEncoder[T]) Encode(data *T) ([]byte, error) {
 	return json.Marshal(data)
 }
 
+// EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
+// binary encoding.
+func (j *JSONEncoder[T]) EncodeTo(writer io.Writer, data *T) error {
+	jsonWriter := json.NewEncoder(writer)
+	return jsonWriter.Encode(data)
+}
+
 // FileExt returns the file extension for the encoded data. In this case, it returns "json" to indicate
 // that the data is in JSON format.
 func (j *JSONEncoder[T]) FileExt() string {
@@ -91,13 +111,21 @@ func (j *JSONEncoder[T]) FileExt() string {
 
 type GZipEncoder[T any] struct {
 	encoder Encoder[T]
+	level   int
 }
 
 // NewGZipEncoder creates a new GZip encoder which wraps the provided encoder.
 // The encoder is used to encode the data before compressing it with GZip.
 func NewGZipEncoder[T any](encoder Encoder[T]) Encoder[T] {
+	return NewGZipEncoderWithLevel(encoder, gzip.DefaultCompression)
+}
+
+// NewGZipEncoderWithLevel creates a new GZip encoder which wraps the provided encoder,
+// and uses the specified encoding level when gzipping.
+func NewGZipEncoderWithLevel[T any](encoder Encoder[T], level int) Encoder[T] {
 	return &GZipEncoder[T]{
 		encoder: encoder,
+		level:   level,
 	}
 }
 
@@ -108,23 +136,43 @@ func (gz *GZipEncoder[T]) Encode(data *T) ([]byte, error) {
 		return nil, fmt.Errorf("GZipEncoder: nested encode failure: %w", err)
 	}
 
-	compressed, err := gZipEncode(encoded)
+	compressed, err := gZipEncode(encoded, gz.level)
 	if err != nil {
 		return nil, fmt.Errorf("GZipEncoder: failed to compress encoded data: %w", err)
 	}
 	return compressed, nil
 }
 
-func gZipEncode(data []byte) ([]byte, error) {
+// EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
+// binary encoding.
+func (gz *GZipEncoder[T]) EncodeTo(writer io.Writer, data *T) error {
+	gzWriter, err := gzip.NewWriterLevel(writer, gz.level)
+	if err != nil {
+		return fmt.Errorf("failed to create gzip writer: %w", err)
+	}
+	if err := gz.encoder.EncodeTo(gzWriter, data); err != nil {
+		_ = gzWriter.Close()
+		return fmt.Errorf("failed to encode to gzip writer: %w", err)
+	}
+
+	return gzWriter.Close()
+}
+
+func gZipEncode(data []byte, level int) ([]byte, error) {
 	var buf bytes.Buffer
 
-	gzWriter, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
+	gzWriter, err := gzip.NewWriterLevel(&buf, level)
 	if err != nil {
 		return nil, err
 	}
 
-	gzWriter.Write(data)
-	gzWriter.Close()
+	if _, err := gzWriter.Write(data); err != nil {
+		_ = gzWriter.Close()
+		return nil, err
+	}
+	if err := gzWriter.Close(); err != nil {
+		return nil, err
+	}
 
 	return buf.Bytes(), nil
 }
@@ -132,7 +180,11 @@ func gZipEncode(data []byte) ([]byte, error) {
 // FileExt returns the file extension for the encoded data. In this case, it returns the wrapped encoder's
 // file extension with ".gz" appended to indicate that the data is compressed with GZip.
 func (gz *GZipEncoder[T]) FileExt() string {
-	return fmt.Sprintf("%s.%s", gz.encoder.FileExt(), GZipExt)
+	prev := gz.encoder.FileExt()
+	if prev == "" {
+		return GZipExt
+	}
+	return fmt.Sprintf("%s.%s", prev, GZipExt)
 }
 
 // ProtoMessagePtr [T] is a generic constraint to ensure types passed to the encoder implement
@@ -162,6 +214,21 @@ func (p *ProtobufEncoder[T, U]) Encode(data *T) ([]byte, error) {
 	return raw, nil
 }
 
+// EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
+// binary encoding.
+func (p *ProtobufEncoder[T, U]) EncodeTo(writer io.Writer, data *T) error {
+	var message U = data
+	bytes, err := proto.Marshal(message)
+	if err != nil {
+		return fmt.Errorf("failed to encode protobuf message: %w", err)
+	}
+	if _, err = writer.Write(bytes); err != nil {
+		return fmt.Errorf("failed to write encoded message to writer: %w", err)
+	}
+
+	return nil
+}
+
 // FileExt returns the file extension for the encoded data. In this case, it returns an empty string
 // to indicate that there is no specific file extension for the binary encoded data.
 func (p *ProtobufEncoder[T, U]) FileExt() string {
@@ -188,6 +255,23 @@ func (p *ProtoJsonEncoder[T, U]) Encode(data *T) ([]byte, error) {
 	return raw, nil
 }
 
+// EncodeTo performs a streaming write to an io.Writer instead of writing and returning the full
+// binary encoding.
+func (p *ProtoJsonEncoder[T, U]) EncodeTo(writer io.Writer, data *T) error {
+	var message U = data
+	// protojson doesn't have a way to marshal directly to an io.Writer, so we'll encode as normal,
+	// and write the resulting data out to the writer
+	bytes, err := protojson.Marshal(message)
+	if err != nil {
+		return fmt.Errorf("failed to marshal protojson: %w", err)
+	}
+	_, err = writer.Write(bytes)
+	if err != nil {
+		return fmt.Errorf("failed to write encoded protojson to writer: %w", err)
+	}
+	return nil
+}
+
 // FileExt returns the file extension for the encoded data. In this case, it returns an empty string
 // to indicate that there is no specific file extension for the binary encoded data.
 func (p *ProtoJsonEncoder[T, U]) FileExt() string {

+ 30 - 0
core/pkg/exporter/exporter.go

@@ -73,6 +73,7 @@ type ComputeStorageExporter[T any] struct {
 	encoder    Encoder[T]
 	storage    storage.Storage
 	validator  validator.ExportValidator[T]
+	streaming  bool
 }
 
 // NewComputeStorageExporter creates a new ComputeStorageExporter instance, which is responsible for exporting
@@ -84,12 +85,14 @@ func NewComputeStorageExporter[T any](
 	encoder Encoder[T],
 	storage storage.Storage,
 	validator validator.ExportValidator[T],
+	streaming bool,
 ) ComputeExporter[T] {
 	return &ComputeStorageExporter[T]{
 		paths:     paths,
 		encoder:   encoder,
 		storage:   storage,
 		validator: validator,
+		streaming: streaming,
 	}
 }
 
@@ -115,6 +118,16 @@ func (se *ComputeStorageExporter[T]) Export(window opencost.Window, data *T) err
 		return nil
 	}
 
+	// stream the data structure to the storage path if we select streaming
+	if se.streaming {
+		return se.streamingUpload(path, data)
+	}
+
+	// otherwise, just encode and write the encoded result directly
+	return se.encodeAndUpload(path, data)
+}
+
+func (se *ComputeStorageExporter[T]) encodeAndUpload(path string, data *T) error {
 	bin, err := se.encoder.Encode(data)
 	if err != nil {
 		return fmt.Errorf("failed to encode data: %w", err)
@@ -128,3 +141,20 @@ func (se *ComputeStorageExporter[T]) Export(window opencost.Window, data *T) err
 
 	return nil
 }
+
+func (se *ComputeStorageExporter[T]) streamingUpload(path string, data *T) error {
+	writer, err := se.storage.WriteStream(path)
+	if err != nil {
+		return fmt.Errorf("failed to create streaming storage writer: %w", err)
+	}
+
+	if err = se.encoder.EncodeTo(writer, data); err != nil {
+		_ = writer.Close()
+		return fmt.Errorf("failed to stream encoding for exporter: %w", err)
+	}
+
+	if err = writer.Close(); err != nil {
+		return fmt.Errorf("failed to flush and close writer after write: %w", err)
+	}
+	return nil
+}

+ 106 - 0
core/pkg/exporter/exporter_test.go

@@ -1,6 +1,8 @@
 package exporter
 
 import (
+	"bytes"
+	"compress/gzip"
 	"testing"
 	"time"
 
@@ -70,12 +72,61 @@ func TestStorageExporters(t *testing.T) {
 			t.Fatalf("failed to create path formatter: %v", err)
 		}
 
+		encoder := NewBingenEncoder[opencost.AllocationSet]()
+		export := NewComputeStorageExporter(
+			p,
+			encoder,
+			store,
+			validator.NewSetValidator[opencost.AllocationSet](24*time.Hour),
+			false,
+		)
+
+		start := time.Now().UTC().Truncate(res)
+		end := start.Add(res)
+
+		toExport := opencost.GenerateMockAllocationSet(start)
+		err = export.Export(opencost.NewClosedWindow(start, end), toExport)
+		if err != nil {
+			t.Fatalf("failed to export data: %v", err)
+		}
+
+		expectedPath := p.ToFullPath("", opencost.NewClosedWindow(start, end), "")
+
+		data, err := store.Read(expectedPath)
+		if err != nil {
+			t.Fatalf("failed to read data from store: %v", err)
+		}
+
+		if len(data) == 0 {
+			t.Fatalf("expected data to be non-empty, got empty")
+		}
+
+		var as *opencost.AllocationSet = new(opencost.AllocationSet)
+		err = as.UnmarshalBinary(data)
+		if err != nil {
+			t.Fatalf("failed to unmarshal data: %v", err)
+		}
+
+		if as.IsEmpty() {
+			t.Fatalf("expected allocation set to be non-empty, got empty")
+		}
+	})
+
+	t.Run("test streaming compute storage exporter", func(t *testing.T) {
+		res := 24 * time.Hour
+		store := storage.NewMemoryStorage()
+		p, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.AllocationPipelineName, &res)
+		if err != nil {
+			t.Fatalf("failed to create path formatter: %v", err)
+		}
+
 		encoder := NewBingenEncoder[opencost.AllocationSet]()
 		export := NewComputeStorageExporter[opencost.AllocationSet](
 			p,
 			encoder,
 			store,
 			validator.NewSetValidator[opencost.AllocationSet](24*time.Hour),
+			true,
 		)
 
 		start := time.Now().UTC().Truncate(res)
@@ -108,4 +159,59 @@ func TestStorageExporters(t *testing.T) {
 			t.Fatalf("expected allocation set to be non-empty, got empty")
 		}
 	})
+
+	t.Run("test compressed streaming compute storage exporter", func(t *testing.T) {
+		res := 24 * time.Hour
+		store := storage.NewMemoryStorage()
+		p, err := pathing.NewDefaultStoragePathFormatter(TestClusterId, pipelines.AllocationPipelineName, &res)
+		if err != nil {
+			t.Fatalf("failed to create path formatter: %v", err)
+		}
+
+		encoder := NewGZipEncoderWithLevel(NewBingenEncoder[opencost.AllocationSet](), gzip.BestSpeed)
+		export := NewComputeStorageExporter(
+			p,
+			encoder,
+			store,
+			validator.NewSetValidator[opencost.AllocationSet](24*time.Hour),
+			true,
+		)
+
+		start := time.Now().UTC().Truncate(res)
+		end := start.Add(res)
+
+		toExport := opencost.GenerateMockAllocationSet(start)
+		err = export.Export(opencost.NewClosedWindow(start, end), toExport)
+		if err != nil {
+			t.Fatalf("failed to export data: %v", err)
+		}
+
+		expectedPath := p.ToFullPath("", opencost.NewClosedWindow(start, end), "gz")
+		t.Logf("Reading from path: %s\n", expectedPath)
+
+		data, err := store.Read(expectedPath)
+		if err != nil {
+			t.Fatalf("failed to read data from store: %v", err)
+		}
+
+		if len(data) == 0 {
+			t.Fatalf("expected data to be non-empty, got empty")
+		}
+
+		reader, err := gzip.NewReader(bytes.NewReader(data))
+		if err != nil {
+			t.Fatalf("failed to create gzip reader")
+		}
+		defer reader.Close()
+
+		var as *opencost.AllocationSet = new(opencost.AllocationSet)
+		err = as.UnmarshalBinaryFromReader(reader)
+		if err != nil {
+			t.Fatalf("failed to unmarshal data: %v", err)
+		}
+
+		if as.IsEmpty() {
+			t.Fatalf("expected allocation set to be non-empty, got empty")
+		}
+	})
 }

+ 1 - 1
core/pkg/model/kubemodel/kubemodel.go

@@ -5,7 +5,7 @@ import (
 	"time"
 )
 
-// @bingen:generate[stringtable,streamable]:KubeModelSet
+// @bingen:generate[streamable,stringtable]:KubeModelSet
 type KubeModelSet struct {
 	Metadata               *Metadata                         `json:"meta"`              // @bingen:field[version=1]
 	Window                 Window                            `json:"window"`            // @bingen:field[version=1]

File diff suppressed because it is too large
+ 390 - 149
core/pkg/model/kubemodel/kubemodel_codecs.go


+ 5 - 5
core/pkg/model/kubemodel/namespace.go

@@ -11,11 +11,11 @@ type Namespace struct {
 	// This field was included in the initial bigen codec but was never populated. If you need to add a new string
 	// field to this structure rename this field and delete this comment
 	bingenPlaceHolder string            // @bingen:field[version=1]
-	Name              string            `json:"name"`            // @bingen:field[version=1]
-	Labels            map[string]string `json:"labels"`          // @bingen:field[version=1]
-	Annotations       map[string]string `json:"annotations"`     // @bingen:field[version=1]
-	Start             time.Time         `json:"start,omitempty"` // @bingen:field[version=1]
-	End               time.Time         `json:"end,omitempty"`   // @bingen:field[version=1]
+	Name              string            `json:"name"`        // @bingen:field[version=1]
+	Labels            map[string]string `json:"labels"`      // @bingen:field[version=1]
+	Annotations       map[string]string `json:"annotations"` // @bingen:field[version=1]
+	Start             time.Time         `json:"start"`       // @bingen:field[version=1]
+	End               time.Time         `json:"end"`         // @bingen:field[version=1]
 }
 
 func (n *Namespace) ValidateNamespace(window Window) error {

+ 1 - 0
core/pkg/model/kubemodel/owner.go

@@ -39,6 +39,7 @@ func ParseOwnerKind(kind string) OwnerKind {
 }
 
 // @bingen:generate:Owner
+// Owner represents a Kubernetes resource owner (workload controller)
 type Owner struct {
 	UID        string    `json:"uid"`
 	Controller bool      `json:"controller"`

+ 19 - 0
core/pkg/model/kubemodel/service.go

@@ -34,6 +34,25 @@ func ParseServiceType(serviceType string) ServiceType {
 }
 
 // @bingen:generate:Service
+// Service represents a Kubernetes Service with network traffic tracking for cost allocation.
+//
+// Network Cost Allocation Strategy:
+// Services expose applications and route traffic, incurring costs for:
+// 1. Load Balancers (LoadBalancer type) - Cloud provider LB hourly cost + data transfer
+// 2. Data Transfer - Egress charges based on NetworkTransferBytes
+// 3. Public IPs (for LoadBalancer/NodePort with external IPs)
+//
+// Cost Attribution Flow:
+// - LoadBalancer Services: Direct cloud resource cost (e.g., AWS ELB, GCP LB) allocated to service
+// - Data Transfer: NetworkTransferBytes × cloud provider egress rate (varies by region/destination)
+// - NetworkReceiveBytes: Typically free (ingress), tracked for visibility
+// - Use Selector to map service costs to backing pods/containers proportionally
+//
+// Example: AWS Application Load Balancer
+// - Fixed hourly cost: $0.0225/hour
+// - LCU cost: $0.008/hour per LCU (based on connections, requests, bandwidth)
+// - Data transfer: $0.09/GB for internet egress
+// Total Service Cost = (LB hours × hourly rate) + (LCU hours × LCU rate) + (NetworkTransferBytes × transfer rate)
 type Service struct {
 	UID          string      `json:"uid"`
 	NamespaceUID string      `json:"namespaceUid"`

+ 16 - 3
core/pkg/opencost/allocation.go

@@ -148,6 +148,19 @@ func (orig *GPUAllocation) Clone() *GPUAllocation {
 	}
 }
 
+// ptrValueEqual reports whether two pointers are both nil, or both non-nil
+// and pointing to equal values. Plain == on pointer fields compares
+// addresses, which made equal-valued GPUAllocations (e.g. binary
+// roundtrips) compare unequal (#3846). NaN values compare unequal per Go ==
+// semantics; SanitizeNaN normalizes NaN pointers to nil before comparisons
+// where that matters.
+func ptrValueEqual[T comparable](a, b *T) bool {
+	if a == nil || b == nil {
+		return a == b
+	}
+	return *a == *b
+}
+
 func (orig *GPUAllocation) Equal(that *GPUAllocation) bool {
 	if orig == nil && that == nil {
 		return true
@@ -159,9 +172,9 @@ func (orig *GPUAllocation) Equal(that *GPUAllocation) bool {
 	return orig.GPUDevice == that.GPUDevice &&
 		orig.GPUModel == that.GPUModel &&
 		orig.GPUUUID == that.GPUUUID &&
-		orig.IsGPUShared == that.IsGPUShared &&
-		orig.GPUUsageAverage == that.GPUUsageAverage &&
-		orig.GPURequestAverage == that.GPURequestAverage
+		ptrValueEqual(orig.IsGPUShared, that.IsGPUShared) &&
+		ptrValueEqual(orig.GPUUsageAverage, that.GPUUsageAverage) &&
+		ptrValueEqual(orig.GPURequestAverage, that.GPURequestAverage)
 
 }
 

+ 76 - 0
core/pkg/opencost/allocation_test.go

@@ -3956,3 +3956,79 @@ func checkAllFloat64sForNaN(t *testing.T, v reflect.Value, testCaseName string)
 		}
 	}
 }
+
+// TestGPUAllocation_Equal verifies value semantics for the pointer fields:
+// two independently constructed GPUAllocations with equal contents must be
+// equal, regardless of pointer identity. Regression test for #3846.
+func TestGPUAllocation_Equal(t *testing.T) {
+	makeGPUAllocation := func() *GPUAllocation {
+		shared := true
+		usage := 0.5
+		request := 1.0
+		return &GPUAllocation{
+			GPUDevice:         "nvidia0",
+			GPUModel:          "Tesla T4",
+			GPUUUID:           "GPU-1",
+			IsGPUShared:       &shared,
+			GPUUsageAverage:   &usage,
+			GPURequestAverage: &request,
+		}
+	}
+
+	cases := map[string]struct {
+		a, b *GPUAllocation
+		want bool
+	}{
+		"both nil": {nil, nil, true},
+		"one nil":  {makeGPUAllocation(), nil, false},
+		"identical values, distinct pointers": {
+			makeGPUAllocation(), makeGPUAllocation(), true,
+		},
+		"different usage value": {
+			makeGPUAllocation(),
+			func() *GPUAllocation { g := makeGPUAllocation(); v := 0.9; g.GPUUsageAverage = &v; return g }(),
+			false,
+		},
+		"different shared value": {
+			makeGPUAllocation(),
+			func() *GPUAllocation { g := makeGPUAllocation(); v := false; g.IsGPUShared = &v; return g }(),
+			false,
+		},
+		"nil vs set pointer field": {
+			makeGPUAllocation(),
+			func() *GPUAllocation { g := makeGPUAllocation(); g.GPURequestAverage = nil; return g }(),
+			false,
+		},
+		"different device identity": {
+			makeGPUAllocation(),
+			func() *GPUAllocation { g := makeGPUAllocation(); g.GPUUUID = "GPU-2"; return g }(),
+			false,
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			if got := tc.a.Equal(tc.b); got != tc.want {
+				t.Errorf("Equal() = %v, want %v", got, tc.want)
+			}
+			if got := tc.b.Equal(tc.a); got != tc.want {
+				t.Errorf("Equal() reversed = %v, want %v", got, tc.want)
+			}
+		})
+	}
+
+	t.Run("binary roundtrip equals original", func(t *testing.T) {
+		orig := makeGPUAllocation()
+		bs, err := orig.MarshalBinary()
+		if err != nil {
+			t.Fatalf("MarshalBinary: %s", err)
+		}
+		decoded := new(GPUAllocation)
+		if err := decoded.UnmarshalBinary(bs); err != nil {
+			t.Fatalf("UnmarshalBinary: %s", err)
+		}
+		if !orig.Equal(decoded) {
+			t.Errorf("roundtrip-decoded GPUAllocation not Equal to original: %+v vs %+v", orig, decoded)
+		}
+	})
+}

+ 5 - 0
core/pkg/opencost/assetprops.go

@@ -196,6 +196,9 @@ const DigitalOceanProvider = "DigitalOcean"
 // OVHProvider describes the provider OVH
 const OVHProvider = "OVH"
 
+// STACKITProvider describes the provider STACKIT
+const STACKITProvider = "STACKIT"
+
 // NilProvider describes unknown provider
 const NilProvider = "-"
 
@@ -220,6 +223,8 @@ func ParseProvider(str string) string {
 		return DigitalOceanProvider
 	case "ovh", "ovhcloud", "ovh-mks":
 		return OVHProvider
+	case "stackit", "ske":
+		return STACKITProvider
 	default:
 		return NilProvider
 	}

+ 33 - 4
core/pkg/opencost/exporter/controllers.go

@@ -38,6 +38,8 @@ type PipelinesExportConfig struct {
 	AssetPipelineResolutons           []time.Duration
 	NetworkInsightPipelineResolutions []time.Duration
 	KubeModelPipelineResolutions      []time.Duration
+	Streaming                         bool
+	Compression                       ExportCompressionLevel
 }
 
 // defaultPipelineExportResolutions returns the default export configuration for the pipeline
@@ -60,6 +62,8 @@ func NewPipelinesExportConfig(appName, clusterUID, clusterName string) Pipelines
 		AssetPipelineResolutons:           defaultPipelineExportResolutions(),
 		NetworkInsightPipelineResolutions: defaultPipelineExportResolutions(),
 		KubeModelPipelineResolutions:      defaultPipelineExportResolutions(),
+		Streaming:                         false,
+		Compression:                       ExportCompressionLevelNone,
 	}
 }
 
@@ -93,7 +97,13 @@ func NewPipelineExportControllers(store storage.Storage, cm ComputePipelineSourc
 		}
 
 		// Use ClusterName for "clusterId" here to maintain legacy pattern
-		allocController, err := NewComputePipelineExportController(config.ClusterName, store, allocSource, res)
+		var allocController *export.ComputeExportController[opencost.AllocationSet]
+		var err error
+		if config.Streaming {
+			allocController, err = NewStreamingComputePipelineExportController(config.ClusterName, store, allocSource, res, config.Compression)
+		} else {
+			allocController, err = NewComputePipelineExportController(config.ClusterName, store, allocSource, res)
+		}
 		if err != nil {
 			log.Errorf("Failed to create allocation export controller for resolution: %s - %v", timeutil.DurationString(res), err)
 			continue
@@ -113,7 +123,13 @@ func NewPipelineExportControllers(store storage.Storage, cm ComputePipelineSourc
 		}
 
 		// Use ClusterName for "clusterId" here to maintain legacy pattern
-		assetController, err := NewComputePipelineExportController(config.ClusterName, store, assetSource, res)
+		var assetController *export.ComputeExportController[opencost.AssetSet]
+		var err error
+		if config.Streaming {
+			assetController, err = NewStreamingComputePipelineExportController(config.ClusterName, store, assetSource, res, config.Compression)
+		} else {
+			assetController, err = NewComputePipelineExportController(config.ClusterName, store, assetSource, res)
+		}
 		if err != nil {
 			log.Errorf("Failed to create asset export controller for resolution: %s - %v", timeutil.DurationString(res), err)
 			continue
@@ -133,7 +149,13 @@ func NewPipelineExportControllers(store storage.Storage, cm ComputePipelineSourc
 		}
 
 		// Use ClusterName for "clusterId" here to maintain legacy pattern
-		networkInsightController, err := NewComputePipelineExportController(config.ClusterName, store, networkInsightSource, res)
+		var networkInsightController *export.ComputeExportController[opencost.NetworkInsightSet]
+		var err error
+		if config.Streaming {
+			networkInsightController, err = NewStreamingComputePipelineExportController(config.ClusterName, store, networkInsightSource, res, config.Compression)
+		} else {
+			networkInsightController, err = NewComputePipelineExportController(config.ClusterName, store, networkInsightSource, res)
+		}
 		if err != nil {
 			log.Errorf("Failed to create network insight export controller for resolution: %s - %v", timeutil.DurationString(res), err)
 			continue
@@ -152,7 +174,14 @@ func NewPipelineExportControllers(store storage.Storage, cm ComputePipelineSourc
 			continue
 		}
 
-		kubeModelController, err := NewKubeModelComputePipelineExportController(config.AppName, config.ClusterUID, store, kubeModelSource, res)
+		var kubeModelController *export.ComputeExportController[kubemodel.KubeModelSet]
+		var err error
+		if config.Streaming {
+			kubeModelController, err = NewStreamingKubeModelComputePipelineExportController(config.AppName, config.ClusterUID, store, kubeModelSource, res, config.Compression)
+		} else {
+			kubeModelController, err = NewKubeModelComputePipelineExportController(config.AppName, config.ClusterUID, store, kubeModelSource, res)
+		}
+
 		if err != nil {
 			log.Errorf("Failed to create KubeModel export controller for resolution: %s - %v", timeutil.DurationString(res), err)
 			continue

+ 5 - 0
core/pkg/opencost/exporter/exporter_test.go

@@ -1,6 +1,7 @@
 package exporter
 
 import (
+	"io"
 	"testing"
 	"time"
 
@@ -141,6 +142,10 @@ type UnknownSet struct{}
 func (u *UnknownSet) MarshalBinary() ([]byte, error) {
 	return []byte{}, nil
 }
+func (u *UnknownSet) MarshalBinaryTo(writer io.Writer) error {
+	return nil
+}
+
 func (u *UnknownSet) UnmarshalBinary(data []byte) error {
 	return nil
 }

+ 138 - 0
core/pkg/opencost/exporter/exporters.go

@@ -1,6 +1,7 @@
 package exporter
 
 import (
+	"compress/gzip"
 	"fmt"
 	"time"
 
@@ -13,6 +14,28 @@ import (
 	"github.com/opencost/opencost/core/pkg/util/typeutil"
 )
 
+// ExportCompressionLevel is an enumeration value for allowing a streaming compute exporter to enable
+// compression at specific gzip levels.
+type ExportCompressionLevel int
+
+// IsValid returns false when the integer value of the `ExportCompressionLevel` isn't a valid input.
+func (ecl ExportCompressionLevel) IsValid() bool {
+	// level is default or none
+	if ecl == ExportCompressionLevelNone || ecl == ExportCompressionLevelDefault {
+		return true
+	}
+
+	// level is within 1-9 bounds
+	return ecl >= ExportCompressionLevelBestSpeed && ecl <= ExportCompressionLevelBestCompression
+}
+
+const (
+	ExportCompressionLevelNone            ExportCompressionLevel = gzip.NoCompression
+	ExportCompressionLevelBestSpeed       ExportCompressionLevel = gzip.BestSpeed
+	ExportCompressionLevelBestCompression ExportCompressionLevel = gzip.BestCompression
+	ExportCompressionLevelDefault         ExportCompressionLevel = gzip.DefaultCompression
+)
+
 // NewComputePipelineExporter creates a new `ComputeExporter[T]` instance which is used to export computed data
 // by window for a specific pipeline.
 func NewComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
@@ -38,6 +61,45 @@ func NewComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validat
 		encoder,
 		store,
 		validator.NewSetValidator[T, S](resolution),
+		false,
+	), nil
+}
+
+// NewStreamingComputePipelineExporter creates a new `ComputeExporter[T]` instance which is used to export computed data
+// by window for a specific pipeline.
+func NewStreamingComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
+	clusterId string,
+	resolution time.Duration,
+	store storage.Storage,
+	compressionLevel ExportCompressionLevel,
+) (export.ComputeExporter[T], error) {
+	pipelineName := pipelines.NameFor[T]()
+	if pipelineName == "" {
+		return nil, fmt.Errorf("failed to extract pipeline name for type: %s", typeutil.TypeOf[T]())
+	}
+
+	pathing, err := pathing.NewDefaultStoragePathFormatter(clusterId, pipelineName, &resolution)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create path formatter: %w", err)
+	}
+
+	if !compressionLevel.IsValid() {
+		return nil, fmt.Errorf("invalid compression level passed: %d is not a valid compression level", int(compressionLevel))
+	}
+
+	var encoder export.Encoder[T]
+	if compressionLevel != ExportCompressionLevelNone {
+		encoder = export.NewGZipEncoderWithLevel(export.NewBingenEncoder[T, U](), int(compressionLevel))
+	} else {
+		encoder = export.NewBingenEncoder[T, U]()
+	}
+
+	return export.NewComputeStorageExporter(
+		pathing,
+		encoder,
+		store,
+		validator.NewSetValidator[T, S](resolution),
+		true,
 	), nil
 }
 
@@ -57,6 +119,23 @@ func NewComputePipelineExportController[T any, U export.BinaryMarshalerPtr[T], S
 	return export.NewComputeExportController(source, exporter, resolution), nil
 }
 
+// NewStreamingComputePipelineExportController creates a new `ComputeExportController[T]` instance which is used to stream/export the
+// computed data using the provided source, storage, resolution, and source resolution.
+func NewStreamingComputePipelineExportController[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
+	clusterId string,
+	store storage.Storage,
+	source export.ComputeSource[T],
+	resolution time.Duration,
+	compressionLevel ExportCompressionLevel,
+) (*export.ComputeExportController[T], error) {
+	exporter, err := NewStreamingComputePipelineExporter[T, U, S](clusterId, resolution, store, compressionLevel)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create compute exporter: %w", err)
+	}
+
+	return export.NewComputeExportController(source, exporter, resolution), nil
+}
+
 // NewKubeModelComputePipelineExporter creates a new `ComputeExporter[T]` instance which is used to export computed data
 // by window for a specific pipeline.
 func NewKubeModelComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
@@ -82,6 +161,47 @@ func NewKubeModelComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T],
 		encoder,
 		store,
 		validator.NewSetValidator[T, S](resolution),
+		false,
+	), nil
+}
+
+// NewStreamingComputePipelineExporter creates a new `ComputeExporter[T]` instance which is used to export computed data
+// by window for a specific pipeline.
+func NewStreamingKubeModelComputePipelineExporter[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
+	appName string,
+	clusterId string,
+	resolution time.Duration,
+	store storage.Storage,
+	compressionLevel ExportCompressionLevel,
+) (export.ComputeExporter[T], error) {
+	pipelineName := pipelines.NameFor[T]()
+	if pipelineName == "" {
+		return nil, fmt.Errorf("failed to extract pipeline name for type: %s", typeutil.TypeOf[T]())
+	}
+
+	res := timeutil.FormatStoreResolution(resolution)
+	pathing, err := pathing.NewKubeModelStoragePathFormatter(appName, clusterId, res)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create path formatter: %w", err)
+	}
+
+	if !compressionLevel.IsValid() {
+		return nil, fmt.Errorf("invalid compression level passed: %d is not a valid compression level", int(compressionLevel))
+	}
+
+	var encoder export.Encoder[T]
+	if compressionLevel != ExportCompressionLevelNone {
+		encoder = export.NewGZipEncoderWithLevel(export.NewBingenEncoder[T, U](), int(compressionLevel))
+	} else {
+		encoder = export.NewBingenEncoder[T, U]()
+	}
+
+	return export.NewComputeStorageExporter(
+		pathing,
+		encoder,
+		store,
+		validator.NewSetValidator[T, S](resolution),
+		true,
 	), nil
 }
 
@@ -101,3 +221,21 @@ func NewKubeModelComputePipelineExportController[T any, U export.BinaryMarshaler
 
 	return export.NewComputeExportController(source, exporter, resolution), nil
 }
+
+// NewStreamingComputePipelineExportController creates a new `ComputeExportController[T]` instance which is used to stream/export the
+// computed data using the provided source, storage, resolution, and source resolution.
+func NewStreamingKubeModelComputePipelineExportController[T any, U export.BinaryMarshalerPtr[T], S validator.SetConstraint[T]](
+	appName string,
+	clusterId string,
+	store storage.Storage,
+	source export.ComputeSource[T],
+	resolution time.Duration,
+	compressionLevel ExportCompressionLevel,
+) (*export.ComputeExportController[T], error) {
+	exporter, err := NewStreamingKubeModelComputePipelineExporter[T, U, S](appName, clusterId, resolution, store, compressionLevel)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create compute exporter: %w", err)
+	}
+
+	return export.NewComputeExportController(source, exporter, resolution), nil
+}

File diff suppressed because it is too large
+ 355 - 133
core/pkg/opencost/opencost_codecs.go


+ 26 - 0
core/pkg/storage/azurestorage.go

@@ -337,6 +337,32 @@ func (b *AzureStorage) Write(name string, data []byte) error {
 	return nil
 }
 
+// WriteStream uses the relative path of the storage combined with the provided path
+// to write a new file or overwrite an existing file. The returned `io.WriteCloser` _must_
+// be closed to complete the write.
+func (b *AzureStorage) WriteStream(name string) (io.WriteCloser, error) {
+	name = trimLeading(name)
+	ctx := context.Background()
+
+	log.Debugf("AzureStorage::WriteStream::HTTPS(%s)", name)
+
+	r, w := io.Pipe()
+	blobClient := b.containerClient.NewBlockBlobClient(name)
+	doneCh := make(chan error, 1)
+
+	go func() {
+		_, err := blobClient.UploadStream(ctx, r, &blockblob.UploadStreamOptions{
+			BlockSize:   4 * 1024 * 1024,
+			Concurrency: 4,
+		})
+		wrapped := errors.Wrapf(err, "cannot upload Azure blob, address: %s", name)
+		r.CloseWithError(wrapped)
+		doneCh <- wrapped
+	}()
+
+	return newAsyncPipeWriter(w, doneCh), nil
+}
+
 // Remove uses the relative path of the storage combined with the provided path to
 // remove a file from storage permanently.
 func (b *AzureStorage) Remove(name string) error {

+ 25 - 0
core/pkg/storage/bucketstorage.go

@@ -2,6 +2,7 @@ package storage
 
 import (
 	"fmt"
+	"io"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -65,6 +66,30 @@ func NewBucketStorage(config []byte) (Storage, error) {
 	return storage, nil
 }
 
+// asyncPipeWriter wraps *io.PipeWriter so that Close() blocks until the
+// background upload goroutine finishes and surfaces any upload error to the caller.
+type asyncPipeWriter struct {
+	*io.PipeWriter
+	done <-chan error
+}
+
+// newAsyncPipeWriter creates a new async pipe writer that implements io.WriteCloser that
+// handles asynchronous closing of an io.PipeWriter
+func newAsyncPipeWriter(writer *io.PipeWriter, done <-chan error) *asyncPipeWriter {
+	return &asyncPipeWriter{
+		PipeWriter: writer,
+		done:       done,
+	}
+}
+
+// Close propagates any errors that were received on the pipewriter or reader.
+func (apw *asyncPipeWriter) Close() error {
+	if err := apw.PipeWriter.Close(); err != nil {
+		return err
+	}
+	return <-apw.done
+}
+
 // trimLeading removes a leading / from the file name
 func trimLeading(file string) string {
 	if len(file) == 0 {

+ 32 - 0
core/pkg/storage/clusterstorage.go

@@ -339,6 +339,38 @@ func (c *ClusterStorage) Write(path string, data []byte) error {
 	return nil
 }
 
+func (c *ClusterStorage) WriteStream(path string) (io.WriteCloser, error) {
+	log.Debugf("ClusterStorage::WriteStream::%s(%s)", strings.ToUpper(c.scheme()), path)
+
+	fn := func(resp *http.Response) error {
+		return nil
+	}
+
+	args := map[string]string{
+		"path": path,
+	}
+
+	r, w := io.Pipe()
+	doneCh := make(chan error, 1)
+
+	go func() {
+		err := c.makeRequest(
+			http.MethodPut,
+			c.getURL("clusterStorage/write", args),
+			r,
+			fn,
+		)
+		var uploadErr error
+		if err != nil {
+			uploadErr = fmt.Errorf("ClusterStorage: WriteStream: %w", err)
+			r.CloseWithError(uploadErr)
+		}
+		doneCh <- uploadErr
+	}()
+
+	return newAsyncPipeWriter(w, doneCh), nil
+}
+
 func (c *ClusterStorage) Remove(path string) error {
 	log.Debugf("ClusterStorage::Remove::%s(%s)", strings.ToUpper(c.scheme()), path)
 

+ 112 - 0
core/pkg/storage/clusterstorage_test.go

@@ -1,6 +1,7 @@
 package storage
 
 import (
+	"bytes"
 	"crypto/tls"
 	"encoding/json"
 	"io"
@@ -171,3 +172,114 @@ func TestClusterStorage_ReadStream(t *testing.T) {
 		t.Fatalf("stream contents mismatch: got %q want %q", string(data), string(expected))
 	}
 }
+
+func TestClusterStorage_WriteStream(t *testing.T) {
+	writeHandler := func(captured *[]byte) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			if r.Method != http.MethodPut || r.URL.Path != "/clusterStorage/write" {
+				w.WriteHeader(http.StatusNotFound)
+				return
+			}
+			var err error
+			*captured, err = io.ReadAll(r.Body)
+			if err != nil {
+				w.WriteHeader(http.StatusInternalServerError)
+				return
+			}
+			w.WriteHeader(http.StatusOK)
+		}
+	}
+
+	t.Run("single chunk reaches server and Close returns nil", func(t *testing.T) {
+		want := []byte("hello from stream")
+		var received []byte
+
+		srv := httptest.NewServer(writeHandler(&received))
+		defer srv.Close()
+
+		cs := newClusterStorageFromURL(t, srv.URL)
+
+		w, err := cs.WriteStream("some/path")
+		if err != nil {
+			t.Fatalf("WriteStream: %s", err)
+		}
+		if _, err = w.Write(want); err != nil {
+			_ = w.Close()
+			t.Fatalf("Write: %s", err)
+		}
+		if err = w.Close(); err != nil {
+			t.Fatalf("Close: %s", err)
+		}
+
+		// Checking received immediately after Close() proves it is synchronous.
+		if !bytes.Equal(received, want) {
+			t.Errorf("body mismatch: got %q, want %q", received, want)
+		}
+	})
+
+	t.Run("multi-chunk write concatenates correctly", func(t *testing.T) {
+		chunks := [][]byte{[]byte("alpha"), []byte("beta"), []byte("gamma")}
+		want := bytes.Join(chunks, nil)
+		var received []byte
+
+		srv := httptest.NewServer(writeHandler(&received))
+		defer srv.Close()
+
+		cs := newClusterStorageFromURL(t, srv.URL)
+
+		w, err := cs.WriteStream("some/path")
+		if err != nil {
+			t.Fatalf("WriteStream: %s", err)
+		}
+		for _, chunk := range chunks {
+			if _, err = w.Write(chunk); err != nil {
+				_ = w.Close()
+				t.Fatalf("Write: %s", err)
+			}
+		}
+		if err = w.Close(); err != nil {
+			t.Fatalf("Close: %s", err)
+		}
+
+		if !bytes.Equal(received, want) {
+			t.Errorf("body mismatch: got %q, want %q", received, want)
+		}
+	})
+
+	t.Run("server error propagates through Close", func(t *testing.T) {
+		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			_, _ = io.Copy(io.Discard, r.Body)
+			w.WriteHeader(http.StatusInternalServerError)
+		}))
+		defer srv.Close()
+
+		cs := newClusterStorageFromURL(t, srv.URL)
+
+		w, err := cs.WriteStream("some/path")
+		if err != nil {
+			t.Fatalf("WriteStream: %s", err)
+		}
+		_, _ = w.Write([]byte("data"))
+		if err = w.Close(); err == nil {
+			t.Fatalf("expected error from Close on server error, got nil")
+		}
+	})
+}
+
+// newClusterStorageFromURL constructs a ClusterStorage pointed at the given test server URL.
+func newClusterStorageFromURL(t *testing.T, rawURL string) *ClusterStorage {
+	t.Helper()
+	u, err := url.Parse(rawURL)
+	if err != nil {
+		t.Fatalf("parsing test server URL: %s", err)
+	}
+	port, err := strconv.Atoi(u.Port())
+	if err != nil {
+		t.Fatalf("parsing test server port: %s", err)
+	}
+	return &ClusterStorage{
+		client: &http.Client{},
+		host:   u.Hostname(),
+		port:   port,
+	}
+}

+ 19 - 0
core/pkg/storage/filestorage.go

@@ -184,6 +184,25 @@ func (fs *FileStorage) Write(path string, data []byte) error {
 	return nil
 }
 
+// WriteStream uses the relative path of the storage combined with the provided path
+// to write a new file or overwrite an existing file.
+//
+// It takes advantage of flock() based locking to improve safety. The returned `io.WriteCloser`
+// must be closed.
+func (fs *FileStorage) WriteStream(path string) (io.WriteCloser, error) {
+	f, err := fs.prepare(path)
+	if err != nil {
+		return nil, errors.Wrap(err, "Failed to prepare path")
+	}
+
+	w, err := fileutil.NewLockedFileWriter(f)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load file writer for: %s - %w", f, err)
+	}
+
+	return w, nil
+}
+
 // Remove uses the relative path of the storage combined with the provided path to
 // remove a file from storage permanently.
 func (fs *FileStorage) Remove(path string) error {

+ 6 - 0
core/pkg/storage/filestorage_test.go

@@ -47,3 +47,9 @@ func TestFileStorage_ReadStream(t *testing.T) {
 	store := NewFileStorage(storeBaseDir)
 	TestStorageReadStream(t, store)
 }
+
+func TestFileStorage_WriteStream(t *testing.T) {
+	storeBaseDir := t.TempDir()
+	store := NewFileStorage(storeBaseDir)
+	TestStorageWriteStream(t, store)
+}

+ 10 - 0
core/pkg/storage/gcsstorage.go

@@ -236,6 +236,16 @@ func (gs *GCSStorage) Write(name string, data []byte) error {
 	return nil
 }
 
+// WriteStream uses the relative path of the storage combined with the provided path
+// to write a new file or overwrite an existing file.
+func (gs *GCSStorage) WriteStream(name string) (io.WriteCloser, error) {
+	name = trimLeading(name)
+	log.Debugf("GCSStorage::WriteStream::HTTPS(%s)", name)
+
+	ctx := context.Background()
+	return gs.bucket.Object(name).NewWriter(ctx), nil
+}
+
 // Remove uses the relative path of the storage combined with the provided path to
 // remove a file from storage permanently.
 func (gs *GCSStorage) Remove(name string) error {

+ 44 - 0
core/pkg/storage/memorystorage.go

@@ -132,6 +132,50 @@ func (ms *MemoryStorage) Write(path string, data []byte) error {
 	return nil
 }
 
+// WriteStream creates a new relative path and returns the io.WriteCloser that can be used to
+// write into the storage path. Close() blocks until all data has been committed to storage.
+func (ms *MemoryStorage) WriteStream(path string) (io.WriteCloser, error) {
+	r, w := io.Pipe()
+	var wg sync.WaitGroup
+
+	wg.Go(func() {
+		data, err := io.ReadAll(r)
+		if err != nil {
+			r.CloseWithError(err)
+			return
+		}
+
+		ms.lock.Lock()
+		defer ms.lock.Unlock()
+
+		paths, pFile := memfile.Split(path)
+
+		f := memfile.NewMemoryFile(pFile, data)
+		currentDir := memfile.CreateSubdirectory(ms.fileTree, paths)
+
+		currentDir.AddFile(f)
+		ms.directPaths[path] = f
+	})
+
+	return &memWriteCloser{
+		PipeWriter: w,
+		wg:         &wg,
+	}, nil
+}
+
+// memWriteCloser wraps *io.PipeWriter so that Close() blocks until the drain goroutine
+// has finished committing data to the in-memory store.
+type memWriteCloser struct {
+	*io.PipeWriter
+	wg *sync.WaitGroup
+}
+
+func (m *memWriteCloser) Close() error {
+	err := m.PipeWriter.Close()
+	m.wg.Wait()
+	return err
+}
+
 // Remove uses the relative path of the storage combined with the provided path to
 // remove a file from storage permanently.
 func (ms *MemoryStorage) Remove(path string) error {

+ 5 - 0
core/pkg/storage/memorystorage_test.go

@@ -390,3 +390,8 @@ func TestMemoryStorage_ReadStream(t *testing.T) {
 	store := NewMemoryStorage()
 	TestStorageReadStream(t, store)
 }
+
+func TestMemoryStorage_WriteStream(t *testing.T) {
+	store := NewMemoryStorage()
+	TestStorageWriteStream(t, store)
+}

+ 5 - 0
core/pkg/storage/prefixedbucketstorage.go

@@ -106,3 +106,8 @@ func (pbs *PrefixedBucketStorage) Stat(name string) (*StorageInfo, error) {
 func (pbs *PrefixedBucketStorage) Write(name string, data []byte) error {
 	return pbs.storage.Write(conditionalPrefix(pbs.prefix, name), data)
 }
+
+// WriteStream uploads the contents written to the returned writer as an object into the bucket.
+func (pbs *PrefixedBucketStorage) WriteStream(name string) (io.WriteCloser, error) {
+	return pbs.storage.WriteStream(conditionalPrefix(pbs.prefix, name))
+}

+ 10 - 0
core/pkg/storage/prefixedbucketstorage_test.go

@@ -21,3 +21,13 @@ func TestPrefixedBucketStorage_ReadStream(t *testing.T) {
 
 	TestStorageReadStream(t, store)
 }
+
+func TestPrefixedBucketStorage_WriteStream(t *testing.T) {
+	base := NewMemoryStorage()
+	store, err := NewPrefixedBucketStorage(base, "myprefix")
+	if err != nil {
+		t.Fatalf("failed to create prefixed storage: %s", err)
+	}
+
+	TestStorageWriteStream(t, store)
+}

+ 29 - 1
core/pkg/storage/s3storage.go

@@ -448,7 +448,6 @@ func (s3 *S3Storage) Write(name string, data []byte) error {
 	// the sub-parts. To remain consistent with other storage implementations,
 	// we would rather attempt to lower cost fast upload and fast-fail.
 	var partSize uint64 = 0
-
 	r := bytes.NewReader(data)
 	_, err = s3.client.PutObject(ctx, s3.name, name, r, int64(size), minio.PutObjectOptions{
 		PartSize:             partSize,
@@ -463,6 +462,35 @@ func (s3 *S3Storage) Write(name string, data []byte) error {
 	return nil
 }
 
+// Upload the contents of the reader as an object into the bucket. The returned `io.WriteCloser` must
+// be closed to finalize the write.
+func (s3 *S3Storage) WriteStream(name string) (io.WriteCloser, error) {
+	name = trimLeading(name)
+
+	log.Debugf("S3Storage::WriteStream::%s(%s)", s3.protocol(), name)
+
+	ctx := context.Background()
+	sse, err := s3.getServerSideEncryption(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	r, w := io.Pipe()
+	doneCh := make(chan error, 1)
+
+	go func() {
+		_, err = s3.client.PutObject(ctx, s3.name, name, r, -1, minio.PutObjectOptions{
+			ServerSideEncryption: sse,
+			UserMetadata:         s3.putUserMetadata,
+		})
+		wrapped := errors.Wrap(err, "upload s3 object")
+		r.CloseWithError(wrapped)
+		doneCh <- wrapped
+	}()
+
+	return newAsyncPipeWriter(w, doneCh), nil
+}
+
 // Attributes returns information about the specified object.
 func (s3 *S3Storage) Stat(name string) (*StorageInfo, error) {
 	name = trimLeading(name)

+ 4 - 0
core/pkg/storage/storage.go

@@ -53,6 +53,10 @@ type Storage interface {
 	// to write a new file or overwrite an existing file.
 	Write(path string, data []byte) error
 
+	// WriteStream creates a new relative path and returns the `io.WriteCloser` that can be used to
+	// write into the storage path. Ensure that Close() is run on the returned writer.
+	WriteStream(path string) (io.WriteCloser, error)
+
 	// Remove uses the relative path of the storage combined with the provided path to
 	// remove a file from storage permanently.
 	Remove(path string) error

+ 84 - 0
core/pkg/storage/test.go

@@ -1,6 +1,7 @@
 package storage
 
 import (
+	"bytes"
 	"fmt"
 	"io"
 	"os"
@@ -501,6 +502,89 @@ func TestStorageReadToLocalFile(t *testing.T, store Storage) {
 	}
 }
 
+func TestStorageWriteStream(t *testing.T, store Storage) {
+	testName := "write_stream"
+
+	testCases := map[string]struct {
+		path     string
+		chunks   [][]byte
+		prewrite bool
+	}{
+		"single chunk": {
+			path:   path.Join(testpath, testName, "single.bin"),
+			chunks: [][]byte{[]byte("single chunk data")},
+		},
+		"multi-chunk": {
+			path:   path.Join(testpath, testName, "multi.bin"),
+			chunks: [][]byte{[]byte("alpha"), []byte("beta"), []byte("gamma")},
+		},
+		"nested path": {
+			path:   path.Join(testpath, testName, "sub/dir/data.bin"),
+			chunks: [][]byte{[]byte("nested data")},
+		},
+		"empty content": {
+			path:   path.Join(testpath, testName, "empty.bin"),
+			chunks: [][]byte{},
+		},
+		"overwrite existing": {
+			path:     path.Join(testpath, testName, "overwrite.bin"),
+			chunks:   [][]byte{[]byte("replaced content")},
+			prewrite: true,
+		},
+	}
+
+	for name, tc := range testCases {
+		t.Run(name, func(t *testing.T) {
+			defer store.Remove(tc.path)
+
+			if tc.prewrite {
+				b, err := json.Marshal(tfc)
+				if err != nil {
+					t.Fatalf("marshal fixture: %s", err)
+				}
+				if err = store.Write(tc.path, b); err != nil {
+					t.Fatalf("pre-write: %s", err)
+				}
+			}
+
+			var want []byte
+			for _, chunk := range tc.chunks {
+				want = append(want, chunk...)
+			}
+
+			w, err := store.WriteStream(tc.path)
+			if err != nil {
+				t.Fatalf("WriteStream: %s", err)
+			}
+
+			for _, chunk := range tc.chunks {
+				n, writeErr := w.Write(chunk)
+				if writeErr != nil {
+					_ = w.Close()
+					t.Fatalf("Write: %s", writeErr)
+				}
+				if n != len(chunk) {
+					_ = w.Close()
+					t.Fatalf("short write: wrote %d of %d bytes", n, len(chunk))
+				}
+			}
+
+			if err = w.Close(); err != nil {
+				t.Fatalf("Close: %s", err)
+			}
+
+			got, err := store.Read(tc.path)
+			if err != nil {
+				t.Fatalf("Read after WriteStream: %s", err)
+			}
+
+			if !bytes.Equal(got, want) {
+				t.Errorf("content mismatch: got %q, want %q", got, want)
+			}
+		})
+	}
+}
+
 func TestStorageReadStream(t *testing.T, store Storage) {
 	testName := "read_stream"
 

+ 273 - 82
core/pkg/util/buffer.go

@@ -18,27 +18,43 @@ var bytePool *bufferPool = newBufferPool()
 // NonPrimitiveTypeError represents an error where the user provided a non-primitive data type for reading/writing
 var NonPrimitiveTypeError error = errors.New("Type provided to read/write does not fit inside 8 bytes.")
 
+// Mode is used to represent the 3 possible states of the buffer. note there is
+// no overlapping between states, as each Mode is handled exclusively.
+type Mode uint8
+
+const (
+	ReadWrite Mode = iota
+	ReadOnly
+	WriteOnly
+)
+
 // Buffer is a utility type which implements a very basic binary protocol for
-// writing core go types.
+// writing core go types. It can run as read-only, write-only, or read-write.
 type Buffer struct {
-	b  *bufio.Reader
-	bw *bytes.Buffer
+	r  *bufio.Reader
+	w  *bufio.Writer
+	rw *bytes.Buffer
+	m  Mode
 }
 
 // NewBuffer creates a new Buffer instance using LittleEndian ByteOrder.
 func NewBuffer() *Buffer {
-	var b bytes.Buffer
 	return &Buffer{
-		b:  nil,
-		bw: &b,
+		r:  nil,
+		w:  nil,
+		rw: new(bytes.Buffer),
+		m:  ReadWrite,
 	}
 }
 
-// NewBufferFromBytes creates a new Buffer instance using the provided byte slice.
+// NewBufferFromBytes creates a new read/write Buffer instance using the provided byte slice.
 // The new buffer assumes ownership of the byte slice.
 func NewBufferFromBytes(b []byte) *Buffer {
 	return &Buffer{
-		bw: bytes.NewBuffer(b),
+		r:  nil,
+		w:  nil,
+		rw: bytes.NewBuffer(b),
+		m:  ReadWrite,
 	}
 }
 
@@ -47,7 +63,10 @@ func NewBufferFromBytes(b []byte) *Buffer {
 func NewBufferFrom(b *Buffer) *Buffer {
 	bb := b.Bytes()
 	return &Buffer{
-		bw: bytes.NewBuffer(bb),
+		r:  nil,
+		w:  nil,
+		rw: bytes.NewBuffer(bb),
+		m:  ReadWrite,
 	}
 }
 
@@ -55,87 +74,178 @@ func NewBufferFrom(b *Buffer) *Buffer {
 // buffer is set to read-only.
 func NewBufferFromReader(reader io.Reader) *Buffer {
 	return &Buffer{
-		b:  bufio.NewReader(reader),
-		bw: nil,
+		r:  bufio.NewReader(reader),
+		w:  nil,
+		rw: nil,
+		m:  ReadOnly,
+	}
+}
+
+// NewBufferFromWriter creates a new Buffer instance using the provided io.Writer. This
+// buffer is set to write-only.
+func NewBufferFromWriter(writer io.Writer) *Buffer {
+	return &Buffer{
+		r:  nil,
+		w:  bufio.NewWriter(writer),
+		rw: nil,
+		m:  WriteOnly,
 	}
 }
 
 // WriteBool writes a bool value to the buffer
 func (b *Buffer) WriteBool(i bool) {
 	b.checkRO()
-	writeBool(b.bw, i)
+
+	if b.rw != nil {
+		writeBool(b.rw, i)
+		return
+	}
+
+	writeBuffBool(b.w, i)
 }
 
 // WriteInt writes an int value to the buffer.
 func (b *Buffer) WriteInt(i int) {
 	b.checkRO()
-	writeInt(b.bw, i)
+
+	if b.rw != nil {
+		writeInt(b.rw, i)
+		return
+	}
+
+	writeBuffInt(b.w, i)
 }
 
 // WriteInt8 writes an int8 value to the buffer.
 func (b *Buffer) WriteInt8(i int8) {
 	b.checkRO()
-	writeInt8(b.bw, i)
+
+	if b.rw != nil {
+		writeInt8(b.rw, i)
+		return
+	}
+
+	writeBuffInt8(b.w, i)
 }
 
 // WriteInt16 writes an int16 value to the buffer.
 func (b *Buffer) WriteInt16(i int16) {
 	b.checkRO()
-	writeInt16(b.bw, i)
+
+	if b.rw != nil {
+		writeInt16(b.rw, i)
+		return
+	}
+
+	writeBuffInt16(b.w, i)
 }
 
 // WriteInt32 writes an int32 value to the buffer.
 func (b *Buffer) WriteInt32(i int32) {
 	b.checkRO()
-	writeInt32(b.bw, i)
+
+	if b.rw != nil {
+		writeInt32(b.rw, i)
+		return
+	}
+
+	writeBuffInt32(b.w, i)
 }
 
 // WriteInt64 writes an int64 value to the buffer.
 func (b *Buffer) WriteInt64(i int64) {
 	b.checkRO()
-	writeInt64(b.bw, i)
+
+	if b.rw != nil {
+		writeInt64(b.rw, i)
+		return
+	}
+
+	writeBuffInt64(b.w, i)
 }
 
 // WriteUInt writes a uint value to the buffer.
 func (b *Buffer) WriteUInt(i uint) {
 	b.checkRO()
-	writeUint(b.bw, i)
+
+	if b.rw != nil {
+		writeUint(b.rw, i)
+		return
+	}
+
+	writeBuffUint(b.w, i)
 }
 
 // WriteUInt8 writes a uint8 value to the buffer.
 func (b *Buffer) WriteUInt8(i uint8) {
 	b.checkRO()
-	writeUint8(b.bw, i)
+
+	if b.rw != nil {
+		writeUint8(b.rw, i)
+		return
+	}
+
+	writeBuffUint8(b.w, i)
 }
 
 // WriteUInt16 writes a uint16 value to the buffer.
 func (b *Buffer) WriteUInt16(i uint16) {
 	b.checkRO()
-	writeUint16(b.bw, i)
+
+	if b.rw != nil {
+		writeUint16(b.rw, i)
+		return
+	}
+
+	writeBuffUint16(b.w, i)
 }
 
 // WriteUInt32 writes a uint32 value to the buffer.
 func (b *Buffer) WriteUInt32(i uint32) {
 	b.checkRO()
-	writeUint32(b.bw, i)
+
+	if b.rw != nil {
+		writeUint32(b.rw, i)
+		return
+	}
+
+	writeBuffUint32(b.w, i)
 }
 
 // WriteUInt64 writes a uint64 value to the buffer.
 func (b *Buffer) WriteUInt64(i uint64) {
 	b.checkRO()
-	writeUint64(b.bw, i)
+
+	if b.rw != nil {
+		writeUint64(b.rw, i)
+		return
+	}
+
+	writeBuffUint64(b.w, i)
 }
 
 // WriteFloat32 writes a float32 value to the buffer.
 func (b *Buffer) WriteFloat32(i float32) {
 	b.checkRO()
-	writeFloat32(b.bw, i)
+
+	if b.rw != nil {
+		writeFloat32(b.rw, i)
+		return
+	}
+
+	writeBuffFloat32(b.w, i)
 }
 
 // WriteFloat64 writes a float64 value to the buffer.
 func (b *Buffer) WriteFloat64(i float64) {
 	b.checkRO()
-	writeFloat64(b.bw, i)
+
+	if b.rw != nil {
+		writeFloat64(b.rw, i)
+		return
+	}
+
+	writeBuffFloat64(b.w, i)
 }
 
 // WriteString writes the string's length as a uint16 followed by the string contents.
@@ -147,216 +257,295 @@ func (b *Buffer) WriteString(i string) {
 	if len(s) > math.MaxUint16 {
 		s = s[:math.MaxUint16]
 	}
-	writeUint16(b.bw, uint16(len(s)))
-	b.bw.Write(s)
+
+	l := uint16(len(s))
+
+	if b.rw != nil {
+		writeUint16(b.rw, l)
+		b.rw.Write(s)
+		return
+	}
+
+	writeBuffUint16(b.w, l)
+	b.w.Write(s)
 }
 
 // WriteBytes writes the contents of the byte slice to the buffer.
 func (b *Buffer) WriteBytes(bytes []byte) {
 	b.checkRO()
-	b.bw.Write(bytes)
+
+	if b.rw != nil {
+		b.rw.Write(bytes)
+		return
+	}
+
+	b.w.Write(bytes)
 }
 
 // Bytes returns the unread portion of the underlying buffer storage. If the buffer was
 // created with an `io.Reader`, then the remaining unread bytes are drained into a byte
 // slice and returned.
 func (b *Buffer) Bytes() []byte {
-	if b.bw != nil {
-		return b.bw.Bytes()
+	b.checkWO()
+
+	if b.rw != nil {
+		return b.rw.Bytes()
 	}
 
-	bytes, err := io.ReadAll(b.b)
+	bytes, err := io.ReadAll(b.r)
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "failed to read remaining bytes from Buffer: %s\n", err)
 	}
 	return bytes
 }
 
+// Peek will attempt to peek ahead if the buffer is in read-only mode.
 func (b *Buffer) Peek(length int) ([]byte, error) {
-	if b.bw != nil {
+	b.checkWO()
+
+	if b.rw != nil {
 		return nil, fmt.Errorf("unsupported Peek() operation on read/write buffer.")
 	}
-	return b.b.Peek(length)
+
+	return b.r.Peek(length)
+}
+
+// Flush will attempt to flush any pending writes if the buffer is in write-only mode.
+func (b *Buffer) Flush() {
+	if b.IsWriteOnly() {
+		if err := b.w.Flush(); err != nil {
+			fmt.Fprintf(os.Stderr, "Flushing io.Writer failed: %s\n", err)
+		}
+	}
 }
 
 // this should be inlined
 func (b *Buffer) checkRO() {
-	if b.bw == nil {
-		panic("Buffer is set to read-only")
+	if b.IsReadOnly() {
+		panic("Tried to write to a Buffer that is set to read-only")
 	}
 }
 
+func (b *Buffer) checkWO() {
+	if b.IsWriteOnly() {
+		panic("Tried to read from a Buffer that is set to write-only")
+	}
+}
+
+// IsReadOnly returns true if the buffer is set to only read mode.
+func (b *Buffer) IsReadOnly() bool {
+	return b.m == ReadOnly
+}
+
+// IsWriteOnly returns true if the buffer is set to only write mode.
+func (b *Buffer) IsWriteOnly() bool {
+	return b.m == WriteOnly
+}
+
+// IsReadWrite returns true if the buffer can be written to and read from.
+func (b *Buffer) IsReadWrite() bool {
+	return b.m == ReadWrite
+}
+
 // ReadBool reads a bool value from the buffer.
 func (b *Buffer) ReadBool() bool {
+	b.checkWO()
+
 	var i bool
-	if b.bw != nil {
-		readBool(b.bw, &i)
+	if b.rw != nil {
+		readBool(b.rw, &i)
 		return i
 	}
 
-	readBuffBool(b.b, &i)
+	readBuffBool(b.r, &i)
 	return i
 }
 
 // ReadInt reads an int value from the buffer.
 func (b *Buffer) ReadInt() int {
+	b.checkWO()
+
 	var i int
-	if b.bw != nil {
-		readInt(b.bw, &i)
+	if b.rw != nil {
+		readInt(b.rw, &i)
 		return i
 	}
 
-	readBuffInt(b.b, &i)
+	readBuffInt(b.r, &i)
 	return i
 }
 
 // ReadInt8 reads an int8 value from the buffer.
 func (b *Buffer) ReadInt8() int8 {
+	b.checkWO()
+
 	var i int8
-	if b.bw != nil {
-		readInt8(b.bw, &i)
+	if b.rw != nil {
+		readInt8(b.rw, &i)
 		return i
 	}
 
-	readBuffInt8(b.b, &i)
+	readBuffInt8(b.r, &i)
 	return i
 }
 
 // ReadInt16 reads an int16 value from the buffer.
 func (b *Buffer) ReadInt16() int16 {
+	b.checkWO()
+
 	var i int16
-	if b.bw != nil {
-		readInt16(b.bw, &i)
+	if b.rw != nil {
+		readInt16(b.rw, &i)
 		return i
 	}
 
-	readBuffInt16(b.b, &i)
+	readBuffInt16(b.r, &i)
 	return i
 }
 
 // ReadInt32 reads an int32 value from the buffer.
 func (b *Buffer) ReadInt32() int32 {
+	b.checkWO()
+
 	var i int32
-	if b.bw != nil {
-		readInt32(b.bw, &i)
+	if b.rw != nil {
+		readInt32(b.rw, &i)
 		return i
 	}
 
-	readBuffInt32(b.b, &i)
+	readBuffInt32(b.r, &i)
 	return i
 }
 
 // ReadInt64 reads an int64 value from the buffer.
 func (b *Buffer) ReadInt64() int64 {
+	b.checkWO()
+
 	var i int64
-	if b.bw != nil {
-		readInt64(b.bw, &i)
+	if b.rw != nil {
+		readInt64(b.rw, &i)
 		return i
 	}
 
-	readBuffInt64(b.b, &i)
+	readBuffInt64(b.r, &i)
 	return i
 }
 
 // ReadUInt reads a uint value from the buffer.
 func (b *Buffer) ReadUInt() uint {
+	b.checkWO()
+
 	var i uint
-	if b.bw != nil {
-		readUint(b.bw, &i)
+	if b.rw != nil {
+		readUint(b.rw, &i)
 		return i
 	}
 
-	readBuffUint(b.b, &i)
+	readBuffUint(b.r, &i)
 	return i
 }
 
 // ReadUInt8 reads a uint8 value from the buffer.
 func (b *Buffer) ReadUInt8() uint8 {
+	b.checkWO()
+
 	var i uint8
-	if b.bw != nil {
-		readUint8(b.bw, &i)
+	if b.rw != nil {
+		readUint8(b.rw, &i)
 		return i
 	}
 
-	readBuffUint8(b.b, &i)
+	readBuffUint8(b.r, &i)
 	return i
 }
 
 // ReadUInt16 reads a uint16 value from the buffer.
 func (b *Buffer) ReadUInt16() uint16 {
+	b.checkWO()
+
 	var i uint16
-	if b.bw != nil {
-		readUint16(b.bw, &i)
+	if b.rw != nil {
+		readUint16(b.rw, &i)
 		return i
 	}
 
-	readBuffUint16(b.b, &i)
+	readBuffUint16(b.r, &i)
 	return i
 }
 
 // ReadUInt32 reads a uint32 value from the buffer.
 func (b *Buffer) ReadUInt32() uint32 {
+	b.checkWO()
+
 	var i uint32
-	if b.bw != nil {
-		readUint32(b.bw, &i)
+	if b.rw != nil {
+		readUint32(b.rw, &i)
 		return i
 	}
 
-	readBuffUint32(b.b, &i)
+	readBuffUint32(b.r, &i)
 	return i
 }
 
 // ReadUInt64 reads a uint64 value from the buffer.
 func (b *Buffer) ReadUInt64() uint64 {
+	b.checkWO()
+
 	var i uint64
-	if b.bw != nil {
-		readUint64(b.bw, &i)
+	if b.rw != nil {
+		readUint64(b.rw, &i)
 		return i
 	}
 
-	readBuffUint64(b.b, &i)
+	readBuffUint64(b.r, &i)
 	return i
 }
 
 // ReadFloat32 reads a float32 value from the buffer.
 func (b *Buffer) ReadFloat32() float32 {
+	b.checkWO()
+
 	var i float32
-	if b.bw != nil {
-		readFloat32(b.bw, &i)
+	if b.rw != nil {
+		readFloat32(b.rw, &i)
 		return i
 	}
 
-	readBuffFloat32(b.b, &i)
+	readBuffFloat32(b.r, &i)
 	return i
 }
 
 // ReadFloat64 reads a float64 value from the buffer.
 func (b *Buffer) ReadFloat64() float64 {
+	b.checkWO()
+
 	var i float64
-	if b.bw != nil {
-		readFloat64(b.bw, &i)
+	if b.rw != nil {
+		readFloat64(b.rw, &i)
 		return i
 	}
 
-	readBuffFloat64(b.b, &i)
+	readBuffFloat64(b.r, &i)
 	return i
 }
 
 // ReadString reads a uint16 value from the buffer representing the string's length,
 // then uses the length to extract the exact length []byte representing the string.
 func (b *Buffer) ReadString() string {
+	b.checkWO()
+
 	var l uint16
-	if b.bw != nil {
-		readUint16(b.bw, &l)
-		return bytesToString(b.bw.Next(int(l)))
+	if b.rw != nil {
+		readUint16(b.rw, &l)
+		return bytesToString(b.rw.Next(int(l)))
 	}
 
-	readBuffUint16(b.b, &l)
+	readBuffUint16(b.r, &l)
 
 	bytes := bytePool.Get(int(l))
 	defer bytePool.Put(bytes)
 
-	_, err := readBuffFull(b.b, bytes)
+	_, err := readBuffFull(b.r, bytes)
 	if err != nil {
 		return ""
 	}
@@ -366,12 +555,14 @@ func (b *Buffer) ReadString() string {
 
 // ReadBytes reads the specified length from the buffer and returns the byte slice.
 func (b *Buffer) ReadBytes(length int) []byte {
-	if b.bw != nil {
-		return b.bw.Next(length)
+	b.checkWO()
+
+	if b.rw != nil {
+		return b.rw.Next(length)
 	}
 
 	bytes := make([]byte, length)
-	_, err := readBuffFull(b.b, bytes)
+	_, err := readBuffFull(b.r, bytes)
 	if err != nil {
 		return bytes
 	}
@@ -402,7 +593,7 @@ func (b *Buffer) ReadBytes(length int) []byte {
 //	  return bytesAsString(bytes)
 //	}
 //
-// In this case, we've create a byte array just big enough for the string, we extract the string data from the reader
+// In this case, we've created a byte array just big enough for the string, we extract the string data from the reader
 // and then cast the byte array in place to the string, and finally drop the byte array reference. This omits an additional
 // allocation if you were to use string(bytes)
 func bytesAsString(b []byte) string {

+ 45 - 0
core/pkg/util/buffer_test.go

@@ -321,6 +321,51 @@ func (sbr *randomByteReader) Read(b []byte) (int, error) {
 	return bytesCopied, err
 }
 
+func TestBufferWriterSupport(t *testing.T) {
+	byteBuff := new(bytes.Buffer)
+
+	buf := NewBufferFromWriter(byteBuff)
+	buf.WriteBool(true)
+	buf.WriteInt(42)
+	buf.WriteFloat64(3.14)
+	buf.WriteString("Testing, 1, 2, 3!")
+	buf.WriteUInt64(uint64(123456))
+	buf.WriteInt16(44)
+	buf.WriteFloat32(float32(5.0))
+	buf.Flush()
+
+	readerBuff := NewBufferFromBytes(byteBuff.Bytes())
+	b := readerBuff.ReadBool()
+	i := readerBuff.ReadInt()
+	f := readerBuff.ReadFloat64()
+	s := readerBuff.ReadString()
+	ui64 := readerBuff.ReadUInt64()
+	i16 := readerBuff.ReadInt16()
+	f32 := readerBuff.ReadFloat32()
+
+	if !b {
+		t.Errorf("expected true, got: false")
+	}
+	if i != 42 {
+		t.Errorf("expected 42, got: %d", i)
+	}
+	if f != 3.14 {
+		t.Errorf("expected 3.14, got: %f", f)
+	}
+	if s != "Testing, 1, 2, 3!" {
+		t.Errorf("expected 'Testing, 1, 2, 3!', got: '%s'", s)
+	}
+	if ui64 != uint64(123456) {
+		t.Errorf("expected 123456, got: %d", ui64)
+	}
+	if i16 != int16(44) {
+		t.Errorf("expected 44, got: %d", i16)
+	}
+	if f32 != float32(5.0) {
+		t.Errorf("expected 5.0, got: %f", f32)
+	}
+}
+
 func TestBufferReaderSupport(t *testing.T) {
 	buf := NewBuffer()
 	buf.WriteBool(true)

+ 110 - 8
core/pkg/util/bufferhelper.go

@@ -384,22 +384,18 @@ func readFull(r *bytes.Buffer, buf []byte) (n int, err error) {
 
 func writeBool(w *bytes.Buffer, data bool) error {
 	if data {
-		w.WriteByte(1)
-		return nil
+		return w.WriteByte(1)
 	}
 
-	w.WriteByte(0)
-	return nil
+	return w.WriteByte(0)
 }
 
 func writeInt8(w *bytes.Buffer, data int8) error {
-	w.WriteByte(byte(data))
-	return nil
+	return w.WriteByte(byte(data))
 }
 
 func writeUint8(w *bytes.Buffer, data uint8) error {
-	w.WriteByte(byte(data))
-	return nil
+	return w.WriteByte(byte(data))
 }
 
 func writeInt16(w *bytes.Buffer, data int16) error {
@@ -491,3 +487,109 @@ func writeFloat64(w *bytes.Buffer, data float64) error {
 	_, err := w.Write(bs)
 	return err
 }
+
+func writeBuffBool(w *bufio.Writer, data bool) error {
+	if data {
+		return w.WriteByte(1)
+	}
+
+	return w.WriteByte(0)
+}
+
+func writeBuffInt8(w *bufio.Writer, data int8) error {
+	return w.WriteByte(byte(data))
+}
+
+func writeBuffUint8(w *bufio.Writer, data uint8) error {
+	return w.WriteByte(byte(data))
+}
+
+func writeBuffInt16(w *bufio.Writer, data int16) error {
+	var b [2]byte
+	bs := b[:]
+
+	binary.LittleEndian.PutUint16(bs, uint16(data))
+	_, err := w.Write(bs)
+	return err
+}
+
+func writeBuffUint16(w *bufio.Writer, data uint16) error {
+	var b [2]byte
+	bs := b[:]
+
+	binary.LittleEndian.PutUint16(bs, data)
+	_, err := w.Write(bs)
+	return err
+}
+
+func writeBuffInt32(w *bufio.Writer, data int32) error {
+	var b [4]byte
+	bs := b[:]
+
+	binary.LittleEndian.PutUint32(bs, uint32(data))
+	_, err := w.Write(bs)
+	return err
+}
+
+func writeBuffUint32(w *bufio.Writer, data uint32) error {
+	var b [4]byte
+	bs := b[:]
+
+	binary.LittleEndian.PutUint32(bs, data)
+	_, err := w.Write(bs)
+	return err
+}
+
+func writeBuffInt(w *bufio.Writer, data int) error {
+	var b [4]byte
+	bs := b[:]
+
+	binary.LittleEndian.PutUint32(bs, uint32(int32(data)))
+	_, err := w.Write(bs)
+	return err
+}
+
+func writeBuffUint(w *bufio.Writer, data uint) error {
+	var b [4]byte
+	bs := b[:]
+
+	binary.LittleEndian.PutUint32(bs, uint32(data))
+	_, err := w.Write(bs)
+	return err
+}
+
+func writeBuffInt64(w *bufio.Writer, data int64) error {
+	var b [8]byte
+	bs := b[:]
+
+	binary.LittleEndian.PutUint64(bs, uint64(data))
+	_, err := w.Write(bs)
+	return err
+}
+
+func writeBuffUint64(w *bufio.Writer, data uint64) error {
+	var b [8]byte
+	bs := b[:]
+
+	binary.LittleEndian.PutUint64(bs, data)
+	_, err := w.Write(bs)
+	return err
+}
+
+func writeBuffFloat32(w *bufio.Writer, data float32) error {
+	var b [4]byte
+	bs := b[:]
+
+	binary.LittleEndian.PutUint32(bs, math.Float32bits(data))
+	_, err := w.Write(bs)
+	return err
+}
+
+func writeBuffFloat64(w *bufio.Writer, data float64) error {
+	var b [8]byte
+	bs := b[:]
+
+	binary.LittleEndian.PutUint64(bs, math.Float64bits(data))
+	_, err := w.Write(bs)
+	return err
+}

+ 24 - 0
core/pkg/util/fileutil/locks_unix.go

@@ -14,6 +14,30 @@ import (
 	"github.com/opencost/opencost/core/pkg/log"
 )
 
+// LockFile directly attempts to flock EX the file instance provided.
+func LockFile(f *os.File) error {
+	if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
+		return fmt.Errorf("unexpected error flock()-ing with EX: %w", err)
+	}
+	return nil
+}
+
+// ReadLockFile directly attempts to flock SH the file instance provided.
+func ReadLockFile(f *os.File) error {
+	if err := syscall.Flock(int(f.Fd()), syscall.LOCK_SH); err != nil {
+		return fmt.Errorf("unexpected error flock()-ing FD: %d directly: %w", f.Fd(), err)
+	}
+	return nil
+}
+
+// UnlockFile directly attempts to flock UN the file instance provided.
+func UnlockFile(f *os.File) error {
+	if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
+		return fmt.Errorf("unexpected error flock()-ing FD %d with UN: %w", f.Fd(), err)
+	}
+	return nil
+}
+
 // WriteLockedFD uses the flock() syscall to safely write to an open file as
 // long as other users of the file are also using flock()-based access.
 //

+ 15 - 0
core/pkg/util/fileutil/locks_windows.go

@@ -22,3 +22,18 @@ func ReadLockedFD(f *os.File) ([]byte, error) {
 func ReadLocked(filename string) ([]byte, error) {
 	return nil, fmt.Errorf("ReadLocked is not implemented on Windows. Please open an issue.")
 }
+
+// LockFile directly attempts to flock EX the file instance provided.
+func LockFile(f *os.File) error {
+	return fmt.Errorf("LockFile is not implemented on Windows. Please open an issue.")
+}
+
+// ReadLockFile directly attempts to flock SH the file instance provided.
+func ReadLockFile(f *os.File) error {
+	return fmt.Errorf("ReadLockFile is not implemented on Windows. Please open an issue.")
+}
+
+// UnlockFile directly attempts to flock UN the file instance provided.
+func UnlockFile(f *os.File) error {
+	return fmt.Errorf("UnlockFile is not implemented on Windows. Please open an issue.")
+}

+ 54 - 0
core/pkg/util/fileutil/writer.go

@@ -0,0 +1,54 @@
+package fileutil
+
+import (
+	"io"
+	"os"
+)
+
+// LockedFileWriter is an io.WriteCloser implementation of a writer that will use flock
+// for file locking during the write and unlock on close.
+type LockedFileWriter struct {
+	f *os.File
+}
+
+// Creates a new FLocking file writer that will flock a file on open, and unlock when the writer is
+// closed.
+func NewLockedFileWriter(path string) (io.WriteCloser, error) {
+	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0666)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := LockFile(f); err != nil {
+		f.Close()
+		return nil, err
+	}
+
+	if err := f.Truncate(0); err != nil {
+		UnlockFile(f)
+		f.Close()
+		return nil, err
+	}
+
+	if _, err := f.Seek(0, io.SeekStart); err != nil {
+		UnlockFile(f)
+		f.Close()
+		return nil, err
+	}
+
+	return &LockedFileWriter{
+		f: f,
+	}, nil
+}
+
+func (lf *LockedFileWriter) Write(p []byte) (int, error) {
+	return lf.f.Write(p)
+}
+
+func (lf *LockedFileWriter) Close() error {
+	if err := UnlockFile(lf.f); err != nil {
+		lf.f.Close()
+		return err
+	}
+	return lf.f.Close()
+}

+ 6 - 4
go.mod

@@ -53,12 +53,14 @@ require (
 	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36
 	github.com/spf13/cobra v1.10.2
 	github.com/spf13/viper v1.21.0
+	github.com/stackitcloud/stackit-sdk-go/core v0.24.0
+	github.com/stackitcloud/stackit-sdk-go/services/cost v0.2.1
 	github.com/stretchr/testify v1.11.1
 	go.opentelemetry.io/otel v1.41.0
 	golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
 	golang.org/x/oauth2 v0.35.0
 	golang.org/x/sync v0.20.0
-	golang.org/x/text v0.35.0
+	golang.org/x/text v0.36.0
 	google.golang.org/api v0.269.0
 	google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
 	k8s.io/api v0.36.0
@@ -214,13 +216,13 @@ require (
 	go.yaml.in/yaml/v2 v2.4.3 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	golang.org/x/crypto v0.49.0 // indirect
-	golang.org/x/mod v0.33.0 // indirect
+	golang.org/x/mod v0.34.0 // indirect
 	golang.org/x/net v0.52.0 // indirect
 	golang.org/x/sys v0.42.0 // indirect
-	golang.org/x/telemetry v0.0.0-20260213145524-e0ab670178e1 // indirect
+	golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
 	golang.org/x/term v0.41.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
-	golang.org/x/tools v0.42.0 // indirect
+	golang.org/x/tools v0.43.0 // indirect
 	golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
 	google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect

+ 12 - 8
go.sum

@@ -426,6 +426,10 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
 github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
 github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
 github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
+github.com/stackitcloud/stackit-sdk-go/core v0.24.0 h1:kHCcezCJ5OGSP7RRuGOxD5rF2wejpkEiRr/OdvNcuPQ=
+github.com/stackitcloud/stackit-sdk-go/core v0.24.0/go.mod h1:osMglDby4csGZ5sIfhNyYq1bS1TxIdPY88+skE/kkmI=
+github.com/stackitcloud/stackit-sdk-go/services/cost v0.2.1 h1:U2sBfMeBCdZUvCW+vqPbo+HPtGxMjCF21PYyQncPnpg=
+github.com/stackitcloud/stackit-sdk-go/services/cost v0.2.1/go.mod h1:Qt/scoasQrONlQ9FauvafUJ/3sP3xIFnhBQC8/Yhqgc=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
@@ -504,8 +508,8 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
-golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
+golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
+golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -544,8 +548,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
 golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/telemetry v0.0.0-20260213145524-e0ab670178e1 h1:QNaHp8YvpPswfDNxlCmJyeesxbGOgaKf41iT9/QrErY=
-golang.org/x/telemetry v0.0.0-20260213145524-e0ab670178e1/go.mod h1:NuITXsA9cTiqnXtVk+/wrBT2Ja4X5hsfGOYRJ6kgYjs=
+golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
+golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -560,8 +564,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
+golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
 golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
 golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
 golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -571,8 +575,8 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
-golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
+golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
+golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

+ 295 - 101
modules/collector-source/pkg/metric/metric_codecs.go

@@ -12,11 +12,13 @@
 package metric
 
 import (
+	"cmp"
 	"fmt"
 	"io"
 	"iter"
 	"os"
 	"reflect"
+	"slices"
 	"strings"
 	"sync"
 	"time"
@@ -28,16 +30,12 @@ import (
 const (
 	// GeneratorPackageName is the package the generator is targetting
 	GeneratorPackageName string = "metric"
-)
+	StringHeaderSize            = int64(unsafe.Sizeof(""))
 
-// BinaryTags represent the formatting tag used for specific optimization features
-const (
 	// BinaryTagStringTable is written and/or read prior to the existence of a string
 	// table (where each index is encoded as a string entry in the resource
 	BinaryTagStringTable string = "BGST"
-)
 
-const (
 	// DefaultCodecVersion is used for any resources listed in the Default version set
 	DefaultCodecVersion uint8 = 1
 )
@@ -60,14 +58,19 @@ type BingenConfiguration struct {
 
 	// FileBackedStringTableDir is the directory to write the string table files for reading.
 	FileBackedStringTableDir string
+
+	// FileBackedStringTableMemoMaxBytes limits in-memory memoization for file-backed table lookups.
+	// 0 disables memoization.
+	FileBackedStringTableMemoMaxBytes int64
 }
 
 // DefaultBingenConfiguration creates the default implementation of the bingen configuration
 // and returns it.
 func DefaultBingenConfiguration() *BingenConfiguration {
 	return &BingenConfiguration{
-		FileBackedStringTableEnabled: false,
-		FileBackedStringTableDir:     os.TempDir(),
+		FileBackedStringTableEnabled:      false,
+		FileBackedStringTableDir:          os.TempDir(),
+		FileBackedStringTableMemoMaxBytes: 0,
 	}
 }
 
@@ -99,12 +102,19 @@ func BingenFileBackedStringTableDir() string {
 	return bingenConfig.FileBackedStringTableDir
 }
 
+// BingenFileBackedStringTableMemoMaxBytes returns the maximum bytes used for file-backed memo cache.
+func BingenFileBackedStringTableMemoMaxBytes() int64 {
+	bingenConfigLock.RLock()
+	defer bingenConfigLock.RUnlock()
+
+	return bingenConfig.FileBackedStringTableMemoMaxBytes
+}
+
 //--------------------------------------------------------------------------
 //  Type Map
 //--------------------------------------------------------------------------
 
-// Generated type map for resolving interface implementations to
-// to concrete types
+// Generated type map for resolving interface implementations to to concrete types
 var typeMap map[string]reflect.Type = map[string]reflect.Type{
 	"Update":    reflect.TypeFor[Update](),
 	"UpdateSet": reflect.TypeFor[UpdateSet](),
@@ -136,21 +146,6 @@ func isReaderBinaryTag(buff *util.Buffer, tag string) bool {
 	return string(data[:len(tag)]) == tag
 }
 
-// appendBytes combines a and b into a new byte array
-func appendBytes(a []byte, b []byte) []byte {
-	al := len(a)
-	bl := len(b)
-	tl := al + bl
-
-	// allocate a new byte array for the combined
-	// use native copy for speedy byte copying
-	result := make([]byte, tl)
-	copy(result, a)
-	copy(result[al:], b)
-
-	return result
-}
-
 // typeToString determines the basic properties of the type, the qualifier, package path, and
 // type name, and returns the qualified type
 func typeToString(f interface{}) string {
@@ -263,33 +258,33 @@ type BingenFieldInfo struct {
 //  String Table Writer
 //--------------------------------------------------------------------------
 
-// StringTableWriter maps strings to specific indices for encoding
-type StringTableWriter struct {
-	l       sync.Mutex
+// StringTableWriter is the interface used to write the string table for encoding.
+type StringTableWriter interface {
+	// AddOrGet adds a string to the string table and returns the new index or
+	// an existing index.
+	AddOrGet(s string) int
+
+	// WriteTo will write the StringTable data (with the header) to the provided
+	// Buffer starting a the current write position
+	WriteTo(b *util.Buffer)
+}
+
+// IndexedStringTableWriter maps strings to specific indices for encoding
+type IndexedStringTableWriter struct {
 	indices map[string]int
 	next    int
 }
 
-// NewStringTableWriter Creates a new StringTableWriter instance with provided contents
-func NewStringTableWriter(contents ...string) *StringTableWriter {
-	st := &StringTableWriter{
-		indices: make(map[string]int, len(contents)),
-		next:    len(contents),
-	}
-
-	for i, entry := range contents {
-		st.indices[entry] = i
+// NewIndexedStringTableWriter Creates a new IndexedStringTableWriter instance.
+func NewIndexedStringTableWriter() *IndexedStringTableWriter {
+	return &IndexedStringTableWriter{
+		indices: make(map[string]int),
+		next:    0,
 	}
-
-	return st
 }
 
-// AddOrGet atomically retrieves a string entry's index if it exist. Otherwise, it will
-// add the entry and return the index.
-func (st *StringTableWriter) AddOrGet(s string) int {
-	st.l.Lock()
-	defer st.l.Unlock()
-
+// AddOrGet retrieves a string entry's index if it exists. Otherwise, it adds the entry and returns the new index.
+func (st *IndexedStringTableWriter) AddOrGet(s string) int {
 	if ind, ok := st.indices[s]; ok {
 		return ind
 	}
@@ -302,10 +297,7 @@ func (st *StringTableWriter) AddOrGet(s string) int {
 }
 
 // ToSlice Converts the contents to a string array for encoding.
-func (st *StringTableWriter) ToSlice() []string {
-	st.l.Lock()
-	defer st.l.Unlock()
-
+func (st *IndexedStringTableWriter) ToSlice() []string {
 	if st.next == 0 {
 		return []string{}
 	}
@@ -318,18 +310,95 @@ func (st *StringTableWriter) ToSlice() []string {
 }
 
 // ToBytes Converts the contents to a binary encoded representation
-func (st *StringTableWriter) ToBytes() []byte {
+func (st *IndexedStringTableWriter) ToBytes() []byte {
 	buff := util.NewBuffer()
-	buff.WriteBytes([]byte(BinaryTagStringTable)) // bingen table header
+	st.WriteTo(buff)
+	return buff.Bytes()
+}
+
+// WriteTo will write the StringTable data (with the header) to the provided
+// Buffer starting a the current write position
+func (st *IndexedStringTableWriter) WriteTo(buff *util.Buffer) {
+	// bingen string table header
+	buff.WriteBytes([]byte(BinaryTagStringTable))
 
+	// get an ordered string slice to encode
 	strs := st.ToSlice()
 
 	buff.WriteInt(len(strs)) // table length
 	for _, s := range strs {
 		buff.WriteString(s)
 	}
+}
 
-	return buff.Bytes()
+type indexed struct {
+	s     string
+	count uint64
+	index int
+}
+
+func newIndexed(s string, index int) *indexed {
+	return &indexed{
+		s:     s,
+		count: 1,
+		index: index,
+	}
+}
+
+// PrepassStringTableWriter maps strings to specific indices for encoding, sorted by the total
+// number of times they're accessed
+type PrepassStringTableWriter struct {
+	prepass map[string]*indexed
+	next    int
+}
+
+// NewPrepassStringTableWriter creates a new PrepassStringTableWriter instance.
+func NewPrepassStringTableWriter() *PrepassStringTableWriter {
+	return &PrepassStringTableWriter{
+		prepass: make(map[string]*indexed),
+	}
+}
+
+// AddOrGet retrieves a string entry's index if it exists. Otherwise, it adds the entry and returns the new index.
+func (st *PrepassStringTableWriter) AddOrGet(s string) int {
+	if ind, ok := st.prepass[s]; ok {
+		ind.count += 1
+		return ind.index
+	}
+
+	current := st.next
+	st.next++
+
+	st.prepass[s] = newIndexed(s, current)
+	return current
+}
+
+// WriteSortedTo sorts the string table by the number of accesses, writes the table in that
+// order, then returns a new StringTableWriter implementation that can be used for the new
+// sorted order index lookups.
+func (st *PrepassStringTableWriter) WriteSortedTo(buff *util.Buffer) StringTableWriter {
+	sl := make([]*indexed, st.next)
+	for _, ind := range st.prepass {
+		sl[ind.index] = ind
+	}
+
+	slices.SortFunc(sl, func(a *indexed, b *indexed) int {
+		return -cmp.Compare(a.count, b.count)
+	})
+
+	sti := NewIndexedStringTableWriter()
+	for _, ind := range sl {
+		sti.AddOrGet(ind.s)
+	}
+
+	sti.WriteTo(buff)
+	return sti
+}
+
+// WriteTo will write the StringTable data (with the header) to the provided
+// Buffer starting a the current write position
+func (st *PrepassStringTableWriter) WriteTo(buff *util.Buffer) {
+	panic("Prepass StringTableWriter cannot write directly")
 }
 
 //--------------------------------------------------------------------------
@@ -350,7 +419,7 @@ type StringTableReader interface {
 
 // SliceStringTableReader is a basic pre-loaded []string that provides index-based access.
 // The cost of this implementation is holding all strings in memory, which provides faster
-// lookup performance for memory usage.
+// lookup performance at the expense of memory usage.
 type SliceStringTableReader struct {
 	table []string
 }
@@ -411,11 +480,12 @@ type fileStringRef struct {
 type FileStringTableReader struct {
 	f    *os.File
 	refs []fileStringRef
+	memo []string
 }
 
 // NewFileStringTableFromBuffer reads exactly tl length-prefixed (uint16) string payloads from buffer
 // and appends each payload to a new temp file. It does not retain full strings in memory.
-func NewFileStringTableReaderFrom(buffer *util.Buffer, dir string) StringTableReader {
+func NewFileStringTableReaderFrom(buffer *util.Buffer, dir string, memoMaxBytes int64) StringTableReader {
 	// helper func to cast a string in-place to a byte slice.
 	// NOTE: Return value is READ-ONLY. DO NOT MODIFY!
 	byteSliceFor := func(s string) []byte {
@@ -469,9 +539,40 @@ func NewFileStringTableReaderFrom(buffer *util.Buffer, dir string) StringTableRe
 		}
 	}
 
+	var memo []string
+
+	// Pre-load cache with strings up to memoMaxBytes, respecting string boundaries
+	if memoMaxBytes > 0 && len(refs) > 0 {
+		memo = make([]string, len(refs))
+		var cumulativeSize int64
+		for i, ref := range refs {
+			// Check if adding this string would exceed the limit
+			if cumulativeSize+int64(ref.length)+StringHeaderSize > memoMaxBytes {
+				// Would exceed limit, stop here
+				break
+			}
+
+			// Read string from file and cache it
+			if ref.length > 0 {
+				b := make([]byte, ref.length)
+				_, err := f.ReadAt(b, ref.off)
+				if err != nil {
+					// If we can't read, skip this entry but continue
+					continue
+				}
+
+				// Cast the allocated bytes to a string in-place
+				str := unsafe.String(unsafe.SliceData(b), len(b))
+				memo[i] = str
+				cumulativeSize += int64(ref.length) + StringHeaderSize
+			}
+		}
+	}
+
 	return &FileStringTableReader{
 		f:    f,
 		refs: refs,
+		memo: memo,
 	}
 }
 
@@ -489,14 +590,19 @@ func (fstr *FileStringTableReader) At(index int) string {
 		return ""
 	}
 
+	// Check cache first
+	if fstr.memo != nil && len(fstr.memo) > index && fstr.memo[index] != "" {
+		return fstr.memo[index]
+	}
+
+	// Cache miss - read from file
 	b := make([]byte, ref.length)
 	_, err := fstr.f.ReadAt(b, ref.off)
 	if err != nil {
 		return ""
 	}
 
-	// cast the allocated bytes to a string in-place, as we
-	// were the ones that allocated the bytes
+	// Cast the allocated bytes to a string in-place, as we were the ones that allocated the bytes
 	return unsafe.String(unsafe.SliceData(b), len(b))
 }
 
@@ -519,6 +625,7 @@ func (fstr *FileStringTableReader) Close() error {
 	err := fstr.f.Close()
 	fstr.f = nil
 	fstr.refs = nil
+	fstr.memo = nil
 
 	if path != "" {
 		_ = os.Remove(path)
@@ -535,7 +642,49 @@ func (fstr *FileStringTableReader) Close() error {
 // and table data
 type EncodingContext struct {
 	Buffer *util.Buffer
-	Table  *StringTableWriter
+	Table  StringTableWriter
+}
+
+// NewEncodingContext creates a new EncodingContext instance that will create a new []byte buffer
+// for writing, and return the context
+func NewEncodingContext(tableWriter StringTableWriter) *EncodingContext {
+	return &EncodingContext{
+		Buffer: util.NewBuffer(),
+		Table:  tableWriter,
+	}
+}
+
+// NewEncodingContextFromWriter creates a new EncodingContext instance that will create a new Buffer
+// from the provided io.Writer and StringTableWriter.
+func NewEncodingContextFromWriter(writer io.Writer, tableWriter StringTableWriter) *EncodingContext {
+	return &EncodingContext{
+		Buffer: util.NewBufferFromWriter(writer),
+		Table:  tableWriter,
+	}
+}
+
+// NewEncodingContextFromBuffer creates a new EncodingContext instance that will leverage an existing
+// Buffer and StringTableWriter.
+func NewEncodingContextFromBuffer(buffer *util.Buffer, tableWriter StringTableWriter) *EncodingContext {
+	return &EncodingContext{
+		Buffer: buffer,
+		Table:  tableWriter,
+	}
+}
+
+// ToBytes returns the encoded string table bytes (if applicable) combined with the encoded buffer bytes. If
+// a string table is being used, the string table bytes will be written first to ensure correct ordering for
+// decoding.
+func (ec *EncodingContext) ToBytes() []byte {
+	encBytes := ec.Buffer.Bytes()
+	if ec.Table != nil {
+		buff := util.NewBuffer()
+		ec.Table.WriteTo(buff)
+		buff.WriteBytes(encBytes)
+		return buff.Bytes()
+	}
+
+	return encBytes
 }
 
 // IsStringTable returns true if the table is available
@@ -583,7 +732,7 @@ func NewDecodingContextFromReader(reader io.Reader) *DecodingContext {
 
 		// create correct string table implementation
 		if IsBingenFileBackedStringTableEnabled() {
-			table = NewFileStringTableReaderFrom(buff, BingenFileBackedStringTableDir())
+			table = NewFileStringTableReaderFrom(buff, BingenFileBackedStringTableDir(), BingenFileBackedStringTableMemoMaxBytes())
 		} else {
 			table = NewSliceStringTableReaderFrom(buff)
 		}
@@ -630,18 +779,25 @@ type BinDecoder interface {
 // MarshalBinary serializes the internal properties of this Update instance
 // into a byte array
 func (target *Update) MarshalBinary() (data []byte, err error) {
-	ctx := &EncodingContext{
-		Buffer: util.NewBuffer(),
-		Table:  nil,
-	}
+	ctx := NewEncodingContext(nil)
 
 	e := target.MarshalBinaryWithContext(ctx)
 	if e != nil {
 		return nil, e
 	}
 
-	encBytes := ctx.Buffer.Bytes()
-	return encBytes, nil
+	return ctx.ToBytes(), nil
+}
+
+// MarshalBinary serializes the internal properties of this Update instance
+// into an io.Writer.
+func (target *Update) MarshalBinaryTo(writer io.Writer) error {
+	buff := util.NewBufferFromWriter(writer)
+	defer buff.Flush()
+
+	ctx := NewEncodingContextFromBuffer(buff, nil)
+
+	return target.MarshalBinaryWithContext(ctx)
 }
 
 // MarshalBinaryWithContext serializes the internal properties of this Update instance
@@ -653,9 +809,9 @@ func (target *Update) MarshalBinaryWithContext(ctx *EncodingContext) (err error)
 			if e, ok := r.(error); ok {
 				err = e
 			} else if s, ok := r.(string); ok {
-				err = fmt.Errorf("Unexpected panic: %s", s)
+				err = fmt.Errorf("unexpected panic: %s", s)
 			} else {
-				err = fmt.Errorf("Unexpected panic: %+v", r)
+				err = fmt.Errorf("unexpected panic: %+v", r)
 			}
 		}
 	}()
@@ -669,6 +825,7 @@ func (target *Update) MarshalBinaryWithContext(ctx *EncodingContext) (err error)
 	} else {
 		buff.WriteString(target.Name) // write string
 	}
+
 	if target.Labels == nil {
 		buff.WriteUInt8(uint8(0)) // write nil byte
 	} else {
@@ -683,17 +840,21 @@ func (target *Update) MarshalBinaryWithContext(ctx *EncodingContext) (err error)
 			} else {
 				buff.WriteString(v) // write string
 			}
+
 			if ctx.IsStringTable() {
 				c := ctx.Table.AddOrGet(z)
 				buff.WriteInt(c) // write table index
 			} else {
 				buff.WriteString(z) // write string
 			}
+
 		}
 		// --- [end][write][map](map[string]string) ---
 
 	}
+
 	buff.WriteFloat64(target.Value) // write float64
+
 	if target.AdditionalInfo == nil {
 		buff.WriteUInt8(uint8(0)) // write nil byte
 	} else {
@@ -708,16 +869,19 @@ func (target *Update) MarshalBinaryWithContext(ctx *EncodingContext) (err error)
 			} else {
 				buff.WriteString(vv) // write string
 			}
+
 			if ctx.IsStringTable() {
 				e := ctx.Table.AddOrGet(zz)
 				buff.WriteInt(e) // write table index
 			} else {
 				buff.WriteString(zz) // write string
 			}
+
 		}
 		// --- [end][write][map](map[string]string) ---
 
 	}
+
 	return nil
 }
 
@@ -726,6 +890,7 @@ func (target *Update) MarshalBinaryWithContext(ctx *EncodingContext) (err error)
 func (target *Update) UnmarshalBinary(data []byte) error {
 	ctx := NewDecodingContextFromBytes(data)
 	defer ctx.Close()
+
 	err := target.UnmarshalBinaryWithContext(ctx)
 	if err != nil {
 		return err
@@ -739,6 +904,7 @@ func (target *Update) UnmarshalBinary(data []byte) error {
 func (target *Update) UnmarshalBinaryFromReader(reader io.Reader) error {
 	ctx := NewDecodingContextFromReader(reader)
 	defer ctx.Close()
+
 	err := target.UnmarshalBinaryWithContext(ctx)
 	if err != nil {
 		return err
@@ -756,9 +922,9 @@ func (target *Update) UnmarshalBinaryWithContext(ctx *DecodingContext) (err erro
 			if e, ok := r.(error); ok {
 				err = e
 			} else if s, ok := r.(string); ok {
-				err = fmt.Errorf("Unexpected panic: %s", s)
+				err = fmt.Errorf("unexpected panic: %s", s)
 			} else {
-				err = fmt.Errorf("Unexpected panic: %+v", r)
+				err = fmt.Errorf("unexpected panic: %+v", r)
 			}
 		}
 	}()
@@ -767,7 +933,7 @@ func (target *Update) UnmarshalBinaryWithContext(ctx *DecodingContext) (err erro
 	version := buff.ReadUInt8()
 
 	if version > DefaultCodecVersion {
-		return fmt.Errorf("Invalid Version Unmarshaling Update. Expected %d or less, got %d", DefaultCodecVersion, version)
+		return fmt.Errorf("Invalid Version Unmarshalling Update. Expected %d or less, got %d", DefaultCodecVersion, version)
 	}
 
 	var b string
@@ -786,7 +952,7 @@ func (target *Update) UnmarshalBinaryWithContext(ctx *DecodingContext) (err erro
 		// --- [begin][read][map](map[string]string) ---
 		e := buff.ReadInt() // map len
 		d := make(map[string]string, e)
-		for i := 0; i < e; i++ {
+		for range e {
 			var v string
 			var g string
 			if ctx.IsStringTable() {
@@ -815,6 +981,7 @@ func (target *Update) UnmarshalBinaryWithContext(ctx *DecodingContext) (err erro
 		// --- [end][read][map](map[string]string) ---
 
 	}
+
 	o := buff.ReadFloat64() // read float64
 	target.Value = o
 
@@ -824,7 +991,7 @@ func (target *Update) UnmarshalBinaryWithContext(ctx *DecodingContext) (err erro
 		// --- [begin][read][map](map[string]string) ---
 		q := buff.ReadInt() // map len
 		p := make(map[string]string, q)
-		for j := 0; j < q; j++ {
+		for range q {
 			var vv string
 			var s string
 			if ctx.IsStringTable() {
@@ -853,6 +1020,7 @@ func (target *Update) UnmarshalBinaryWithContext(ctx *DecodingContext) (err erro
 		// --- [end][read][map](map[string]string) ---
 
 	}
+
 	return nil
 }
 
@@ -863,20 +1031,37 @@ func (target *Update) UnmarshalBinaryWithContext(ctx *DecodingContext) (err erro
 // MarshalBinary serializes the internal properties of this UpdateSet instance
 // into a byte array
 func (target *UpdateSet) MarshalBinary() (data []byte, err error) {
-	ctx := &EncodingContext{
-		Buffer: util.NewBuffer(),
-		Table:  NewStringTableWriter(),
-	}
+	ctx := NewEncodingContext(NewIndexedStringTableWriter())
 
 	e := target.MarshalBinaryWithContext(ctx)
 	if e != nil {
 		return nil, e
 	}
 
-	encBytes := ctx.Buffer.Bytes()
-	sTableBytes := ctx.Table.ToBytes()
-	merged := appendBytes(sTableBytes, encBytes)
-	return merged, nil
+	return ctx.ToBytes(), nil
+}
+
+// MarshalBinary serializes the internal properties of this UpdateSet instance
+// into an io.Writer.
+func (target *UpdateSet) MarshalBinaryTo(writer io.Writer) error {
+	buff := util.NewBufferFromWriter(writer)
+	defer buff.Flush()
+
+	// run a pre-pass to collect all strings into the string table and discard all writes to the main
+	// buffer. Then, we write the string table, sorted by number of repeated uses (descending), to the
+	// main buffer, and use the resulting table as part of the context for the main pass.
+	prepass := NewPrepassStringTableWriter()
+	prepassCtx := NewEncodingContextFromWriter(io.Discard, prepass)
+
+	e := target.MarshalBinaryWithContext(prepassCtx)
+	if e != nil {
+		return e
+	}
+
+	tableWriter := prepass.WriteSortedTo(buff)
+	ctx := NewEncodingContextFromBuffer(buff, tableWriter)
+
+	return target.MarshalBinaryWithContext(ctx)
 }
 
 // MarshalBinaryWithContext serializes the internal properties of this UpdateSet instance
@@ -888,9 +1073,9 @@ func (target *UpdateSet) MarshalBinaryWithContext(ctx *EncodingContext) (err err
 			if e, ok := r.(error); ok {
 				err = e
 			} else if s, ok := r.(string); ok {
-				err = fmt.Errorf("Unexpected panic: %s", s)
+				err = fmt.Errorf("unexpected panic: %s", s)
 			} else {
-				err = fmt.Errorf("Unexpected panic: %+v", r)
+				err = fmt.Errorf("unexpected panic: %+v", r)
 			}
 		}
 	}()
@@ -913,8 +1098,9 @@ func (target *UpdateSet) MarshalBinaryWithContext(ctx *EncodingContext) (err err
 		buff.WriteUInt8(uint8(1)) // write non-nil byte
 
 		// --- [begin][write][slice]([]Update) ---
-		buff.WriteInt(len(target.Updates)) // array length
-		for i := 0; i < len(target.Updates); i++ {
+		buff.WriteInt(len(target.Updates)) // slice length
+		for i := range target.Updates {
+
 			// --- [begin][write][struct](Update) ---
 			buff.WriteInt(0) // [compatibility, unused]
 			errB := target.Updates[i].MarshalBinaryWithContext(ctx)
@@ -927,6 +1113,7 @@ func (target *UpdateSet) MarshalBinaryWithContext(ctx *EncodingContext) (err err
 		// --- [end][write][slice]([]Update) ---
 
 	}
+
 	return nil
 }
 
@@ -935,6 +1122,7 @@ func (target *UpdateSet) MarshalBinaryWithContext(ctx *EncodingContext) (err err
 func (target *UpdateSet) UnmarshalBinary(data []byte) error {
 	ctx := NewDecodingContextFromBytes(data)
 	defer ctx.Close()
+
 	err := target.UnmarshalBinaryWithContext(ctx)
 	if err != nil {
 		return err
@@ -948,6 +1136,7 @@ func (target *UpdateSet) UnmarshalBinary(data []byte) error {
 func (target *UpdateSet) UnmarshalBinaryFromReader(reader io.Reader) error {
 	ctx := NewDecodingContextFromReader(reader)
 	defer ctx.Close()
+
 	err := target.UnmarshalBinaryWithContext(ctx)
 	if err != nil {
 		return err
@@ -965,9 +1154,9 @@ func (target *UpdateSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (err e
 			if e, ok := r.(error); ok {
 				err = e
 			} else if s, ok := r.(string); ok {
-				err = fmt.Errorf("Unexpected panic: %s", s)
+				err = fmt.Errorf("unexpected panic: %s", s)
 			} else {
-				err = fmt.Errorf("Unexpected panic: %+v", r)
+				err = fmt.Errorf("unexpected panic: %+v", r)
 			}
 		}
 	}()
@@ -976,13 +1165,13 @@ func (target *UpdateSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (err e
 	version := buff.ReadUInt8()
 
 	if version > DefaultCodecVersion {
-		return fmt.Errorf("Invalid Version Unmarshaling UpdateSet. Expected %d or less, got %d", DefaultCodecVersion, version)
+		return fmt.Errorf("Invalid Version Unmarshalling UpdateSet. Expected %d or less, got %d", DefaultCodecVersion, version)
 	}
 
 	// --- [begin][read][reference](time.Time) ---
-	a := &time.Time{}
-	b := buff.ReadInt()    // byte array length
-	c := buff.ReadBytes(b) // byte array
+	a := new(time.Time)
+	b := buff.ReadInt() // byte array length
+	c := buff.ReadBytes(b)
 	errA := a.UnmarshalBinary(c)
 	if errA != nil {
 		return errA
@@ -994,11 +1183,12 @@ func (target *UpdateSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (err e
 		target.Updates = nil
 	} else {
 		// --- [begin][read][slice]([]Update) ---
-		e := buff.ReadInt() // array len
+		e := buff.ReadInt() // slice len
 		d := make([]Update, e)
-		for i := 0; i < e; i++ {
+		for i := range e {
+
 			// --- [begin][read][struct](Update) ---
-			g := &Update{}
+			g := new(Update)
 			buff.ReadInt() // [compatibility, unused]
 			errB := g.UnmarshalBinaryWithContext(ctx)
 			if errB != nil {
@@ -1013,6 +1203,7 @@ func (target *UpdateSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (err e
 		// --- [end][read][slice]([]Update) ---
 
 	}
+
 	return nil
 }
 
@@ -1022,7 +1213,7 @@ func (target *UpdateSet) UnmarshalBinaryWithContext(ctx *DecodingContext) (err e
 
 // UpdateSetStream is a single use field stream for the contents of an UpdateSet instance. Instead of creating an instance and populating
 // the fields on that instance, we provide a streaming iterator which yields (BingenFieldInfo, *BingenValue) tuples for each
-// stremable element. All slices and maps will be flattened one depth and each element streamed individually.
+// streamable element. All slices and maps will be flattened one depth and each element streamed individually.
 type UpdateSetStream struct {
 	reader io.Reader
 	ctx    *DecodingContext
@@ -1064,7 +1255,7 @@ func (stream *UpdateSetStream) Stream() iter.Seq2[BingenFieldInfo, *BingenValue]
 		version := buff.ReadUInt8()
 
 		if version > DefaultCodecVersion {
-			stream.err = fmt.Errorf("Invalid Version Unmarshaling UpdateSet. Expected %d or less, got %d", DefaultCodecVersion, version)
+			stream.err = fmt.Errorf("Invalid Version Unmarshalling UpdateSet. Expected %d or less, got %d", DefaultCodecVersion, version)
 			return
 		}
 
@@ -1074,40 +1265,42 @@ func (stream *UpdateSetStream) Stream() iter.Seq2[BingenFieldInfo, *BingenValue]
 		}
 
 		// --- [begin][read][reference](time.Time) ---
-		b := &time.Time{}
-		c := buff.ReadInt()    // byte array length
-		d := buff.ReadBytes(c) // byte array
+		b := new(time.Time)
+		c := buff.ReadInt() // byte array length
+		d := buff.ReadBytes(c)
 		errA := b.UnmarshalBinary(d)
 		if errA != nil {
 			stream.err = errA
 			return
+
 		}
 		a := *b
 		// --- [end][read][reference](time.Time) ---
-
 		if !yield(fi, singleV(a)) {
 			return
 		}
+
 		fi = BingenFieldInfo{
 			Type: reflect.TypeFor[[]Update](),
 			Name: "Updates",
 		}
-
 		if buff.ReadUInt8() == uint8(0) {
 			if !yield(fi, nil) {
 				return
 			}
 		} else {
 			// --- [begin][read][streaming-slice]([]Update) ---
-			e := buff.ReadInt() // array len
-			for i := 0; i < e; i++ {
+			e := buff.ReadInt() // slice len
+			for i := range e {
+
 				// --- [begin][read][struct](Update) ---
-				g := &Update{}
+				g := new(Update)
 				buff.ReadInt() // [compatibility, unused]
 				errB := g.UnmarshalBinaryWithContext(ctx)
 				if errB != nil {
 					stream.err = errB
 					return
+
 				}
 				f := *g
 				// --- [end][read][struct](Update) ---
@@ -1119,5 +1312,6 @@ func (stream *UpdateSetStream) Stream() iter.Seq2[BingenFieldInfo, *BingenValue]
 			// --- [end][read][streaming-slice]([]Update) ---
 
 		}
+
 	}
 }

+ 1 - 0
modules/prometheus-source/pkg/prom/clustermap.go

@@ -47,6 +47,7 @@ func newPrometheusClusterMap(contextFactory *ContextFactory, cip clusters.Cluste
 
 		// Tick on interval and refresh clusters
 		ticker := time.NewTicker(refresh)
+		defer ticker.Stop()
 		for {
 			select {
 			case <-ticker.C:

+ 1 - 1
pkg/allocation/autocompletequeryservice.go

@@ -18,7 +18,7 @@ func QueryAllocationAutocompleteFromSetRange(asr *opencost.AllocationSetRange, r
 	}
 
 	var matcher opencost.AllocationMatcher
-	if req.Filter != nil {
+	if autocomplete.HasFilter(req.Filter) {
 		compiler := opencost.NewAllocationMatchCompiler(req.LabelConfig)
 		matcher, err = compiler.Compile(req.Filter)
 		if err != nil {

+ 2 - 0
pkg/allocation/autocompletequeryservice_test.go

@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/opencost/opencost/core/pkg/autocomplete"
+	"github.com/opencost/opencost/core/pkg/filter/ast"
 	"github.com/opencost/opencost/core/pkg/opencost"
 )
 
@@ -41,6 +42,7 @@ func TestQueryAllocationAutocompleteFromSetRange(t *testing.T) {
 		Field:  "label",
 		Limit:  10,
 		Window: window,
+		Filter: &ast.VoidOp{},
 	})
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)

+ 1 - 1
pkg/asset/autocompletequeryservice.go

@@ -37,7 +37,7 @@ func QueryAssetAutocompleteFromSet(assetSet *opencost.AssetSet, req autocomplete
 	}
 
 	var matcher opencost.AssetMatcher
-	if req.Filter != nil {
+	if autocomplete.HasFilter(req.Filter) {
 		compiler := opencost.NewAssetMatchCompiler()
 		matcher, err = compiler.Compile(req.Filter)
 		if err != nil {

+ 2 - 0
pkg/asset/autocompletequeryservice_test.go

@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/opencost/opencost/core/pkg/autocomplete"
+	"github.com/opencost/opencost/core/pkg/filter/ast"
 	"github.com/opencost/opencost/core/pkg/opencost"
 )
 
@@ -29,6 +30,7 @@ func TestQueryAssetAutocompleteFromSet(t *testing.T) {
 		TenantID: "opencost",
 		Field:    "cluster",
 		Window:   window,
+		Filter:   &ast.VoidOp{},
 	})
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)

+ 40 - 0
pkg/cloud/config/configurations.go

@@ -11,6 +11,7 @@ import (
 	"github.com/opencost/opencost/pkg/cloud/azure"
 	"github.com/opencost/opencost/pkg/cloud/gcp"
 	"github.com/opencost/opencost/pkg/cloud/oracle"
+	"github.com/opencost/opencost/pkg/cloud/stackit"
 )
 
 // MultiCloudConfig struct is used to unmarshal cloud configs for each provider out of cloud-integration file
@@ -68,6 +69,7 @@ type Configurations struct {
 	Azure   *AzureConfigs   `json:"azure,omitempty"`
 	Alibaba *AlibabaConfigs `json:"alibaba,omitempty"`
 	OCI     *OCIConfigs     `json:"oci,omitempty"`
+	STACKIT *STACKITConfigs `json:"stackit,omitempty"`
 }
 
 // UnmarshalJSON custom json unmarshalling to maintain support for MultiCloudConfig format
@@ -122,6 +124,10 @@ func (c *Configurations) Equals(that *Configurations) bool {
 		return false
 	}
 
+	if !c.STACKIT.Equals(that.STACKIT) {
+		return false
+	}
+
 	return true
 }
 
@@ -157,6 +163,11 @@ func (c *Configurations) Insert(keyedConfig cloud.Config) error {
 			c.OCI = &OCIConfigs{}
 		}
 		c.OCI.UsageAPI = append(c.OCI.UsageAPI, keyedConfig.(*oracle.UsageApiConfiguration))
+	case *stackit.CostConfiguration:
+		if c.STACKIT == nil {
+			c.STACKIT = &STACKITConfigs{}
+		}
+		c.STACKIT.CostAPI = append(c.STACKIT.CostAPI, keyedConfig.(*stackit.CostConfiguration))
 	default:
 		return fmt.Errorf("Configurations: Insert: failed to insert config of type: %T", keyedConfig)
 	}
@@ -199,6 +210,12 @@ func (c *Configurations) ToSlice() []cloud.KeyedConfig {
 		}
 	}
 
+	if c.STACKIT != nil {
+		for _, costConfig := range c.STACKIT.CostAPI {
+			keyedConfigs = append(keyedConfigs, costConfig)
+		}
+	}
+
 	return keyedConfigs
 
 }
@@ -339,3 +356,26 @@ func (oc *OCIConfigs) Equals(that *OCIConfigs) bool {
 
 	return true
 }
+
+type STACKITConfigs struct {
+	CostAPI []*stackit.CostConfiguration `json:"costApi,omitempty"`
+}
+
+func (sc *STACKITConfigs) Equals(that *STACKITConfigs) bool {
+	if sc == nil && that == nil {
+		return true
+	}
+	if sc == nil || that == nil {
+		return false
+	}
+	if len(sc.CostAPI) != len(that.CostAPI) {
+		return false
+	}
+	for i, thisCost := range sc.CostAPI {
+		thatCost := that.CostAPI[i]
+		if !thisCost.Equals(thatCost) {
+			return false
+		}
+	}
+	return true
+}

+ 6 - 0
pkg/cloud/config/statuses.go

@@ -10,6 +10,7 @@ import (
 	"github.com/opencost/opencost/pkg/cloud/azure"
 	"github.com/opencost/opencost/pkg/cloud/gcp"
 	"github.com/opencost/opencost/pkg/cloud/oracle"
+	"github.com/opencost/opencost/pkg/cloud/stackit"
 )
 
 const (
@@ -18,6 +19,7 @@ const (
 	BigQueryConfigType     = "bigquery"
 	AzureStorageConfigType = "azurestorage"
 	UsageApiConfigType     = "usageapi"
+	STACKITCostConfigType  = "stackitcost"
 )
 
 func ConfigTypeFromConfig(config cloud.KeyedConfig) (string, error) {
@@ -32,6 +34,8 @@ func ConfigTypeFromConfig(config cloud.KeyedConfig) (string, error) {
 		return AzureStorageConfigType, nil
 	case *oracle.UsageApiConfiguration:
 		return UsageApiConfigType, nil
+	case *stackit.CostConfiguration:
+		return STACKITCostConfigType, nil
 	}
 	return "", fmt.Errorf("failed to determine config type for config with key: %s, type %T", config.Key(), config)
 }
@@ -120,6 +124,8 @@ func (s *Status) UnmarshalJSON(b []byte) error {
 		config = &azure.StorageConfiguration{}
 	case UsageApiConfigType:
 		config = &oracle.UsageApiConfiguration{}
+	case STACKITCostConfigType:
+		config = &stackit.CostConfiguration{}
 	default:
 		return fmt.Errorf("Status: UnmarshalJSON: config type '%s' is not recognized", configType)
 	}

+ 51 - 8
pkg/cloud/gcp/provider.go

@@ -6,6 +6,7 @@ import (
 	"io"
 	"math"
 	"net/http"
+	"net/url"
 	"os"
 	"path"
 	"regexp"
@@ -37,7 +38,8 @@ import (
 
 const GKE_GPU_TAG = "cloud.google.com/gke-accelerator"
 const BigqueryUpdateType = "bigqueryupdate"
-const BillingAPIURLFmt = "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=%s&currencyCode=%s"
+const BillingAPIURL = "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus"
+const GCPCloudOAuthScope = "https://www.googleapis.com/auth/cloud-platform"
 
 const (
 	GCPHourlyPublicIPCost = 0.01
@@ -955,8 +957,38 @@ func (gcp *GCP) parsePage(r io.Reader, inputKeys map[string]models.Key, pvKeys m
 	return gcpPricingList, nextPageToken, nil
 }
 
-func (gcp *GCP) getBillingAPIURL(apiKey, currencyCode string) string {
-	return fmt.Sprintf(BillingAPIURLFmt, apiKey, currencyCode)
+func (gcp *GCP) buildBillingAPIURL(apiKey, currencyCode string) *url.URL {
+	url, err := url.Parse(BillingAPIURL)
+	if err != nil {
+		panic("BillingAPIURL must be a valid URL")
+	}
+
+	query := url.Query()
+	query.Add("currencyCode", currencyCode)
+
+	if apiKey != "" {
+		query.Add("key", apiKey)
+	}
+
+	url.RawQuery = query.Encode()
+
+	return url
+}
+
+func (gcp *GCP) getBillingAPIClientAndURL(apiKey, currencyCode string) (*http.Client, string, error) {
+	url := gcp.buildBillingAPIURL(apiKey, currencyCode)
+
+	if apiKey != "" {
+		return http.DefaultClient, url.String(), nil
+	}
+
+	googleHttpClient, err := google.DefaultClient(context.TODO(), GCPCloudOAuthScope)
+	if err != nil {
+		log.Errorf("GCP Billing API: Workload Identity detected but failed to create authenticated client: %v", err)
+		return nil, "", err
+	}
+
+	return googleHttpClient, url.String(), nil
 }
 
 func (gcp *GCP) parsePages(inputKeys map[string]models.Key, pvKeys map[string]models.PVKey) (map[string]*GCPPricing, error) {
@@ -966,7 +998,10 @@ func (gcp *GCP) parsePages(inputKeys map[string]models.Key, pvKeys map[string]mo
 		return nil, err
 	}
 
-	url := gcp.getBillingAPIURL(gcp.APIKey, c.CurrencyCode)
+	httpClient, url, err := gcp.getBillingAPIClientAndURL(gcp.APIKey, c.CurrencyCode)
+	if err != nil {
+		return nil, err
+	}
 
 	var parsePagesHelper func(string) error
 	parsePagesHelper = func(pageToken string) error {
@@ -975,7 +1010,7 @@ func (gcp *GCP) parsePages(inputKeys map[string]models.Key, pvKeys map[string]mo
 		} else if pageToken != "" {
 			url = url + "&pageToken=" + pageToken
 		}
-		resp, err := http.Get(url)
+		resp, err := httpClient.Get(url)
 		if err != nil {
 			return err
 		}
@@ -1339,7 +1374,15 @@ func (gcp *GCP) getReservedInstances() ([]*GCPReservedInstance, error) {
 		return nil, err
 	}
 
-	commitments, err := computeService.RegionCommitments.AggregatedList(gcp.ProjectID).Do()
+	projID := gcp.ProjectID
+	if projID == "" {
+		projID, err = gcp.MetadataClient.ProjectID()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	commitments, err := computeService.RegionCommitments.AggregatedList(projID).Do()
 	if err != nil {
 		return nil, err
 	}
@@ -1568,7 +1611,7 @@ func (gcp *GCP) NodePricing(key models.Key) (*models.Node, models.PricingMetadat
 		log.Debugf("Returning pricing for node %s: %+v from SKU %s", key, n.Node, n.Name)
 
 		// Add pricing URL, but redact the key (hence, "***"")
-		meta.Source = fmt.Sprintf("Downloaded pricing from %s", gcp.getBillingAPIURL("***", c.CurrencyCode))
+		meta.Source = fmt.Sprintf("Downloaded pricing from %s", gcp.buildBillingAPIURL("***", c.CurrencyCode))
 
 		n.Node.BaseCPUPrice = gcp.BaseCPUPrice
 
@@ -1588,7 +1631,7 @@ func (gcp *GCP) NodePricing(key models.Key) (*models.Node, models.PricingMetadat
 			log.Debugf("Returning pricing for node %s: %+v from SKU %s", key, n.Node, n.Name)
 
 			// Add pricing URL, but redact the key (hence, "***"")
-			meta.Source = fmt.Sprintf("Downloaded pricing from %s", gcp.getBillingAPIURL("***", c.CurrencyCode))
+			meta.Source = fmt.Sprintf("Downloaded pricing from %s", gcp.buildBillingAPIURL("***", c.CurrencyCode))
 
 			n.Node.BaseCPUPrice = gcp.BaseCPUPrice
 

+ 56 - 3
pkg/cloud/gcp/provider_test.go

@@ -4,6 +4,8 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"net/http"
+	"net/url"
 	"os"
 	"reflect"
 	"strings"
@@ -707,11 +709,62 @@ func TestGCP_findCostForDisk(t *testing.T) {
 }
 
 func TestGCP_getBillingAPIURL(t *testing.T) {
+	tests := []struct {
+		name           string
+		apiKey         string
+		currency       string
+		expectedParams map[string]string
+		absentParams   []string
+	}{
+		{
+			name:           "with API key and currency",
+			apiKey:         "test-key",
+			currency:       "USD",
+			expectedParams: map[string]string{"key": "test-key", "currencyCode": "USD"},
+		},
+		{
+			name:           "empty API key omits key param",
+			apiKey:         "",
+			currency:       "USD",
+			expectedParams: map[string]string{"currencyCode": "USD"},
+			absentParams:   []string{"key"},
+		},
+		{
+			name:           "non-USD currency",
+			apiKey:         "my-key",
+			currency:       "EUR",
+			expectedParams: map[string]string{"key": "my-key", "currencyCode": "EUR"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gcp := &GCP{}
+			query := gcp.buildBillingAPIURL(tt.apiKey, tt.currency).Query()
+
+			for param, expected := range tt.expectedParams {
+				assert.Equal(t, expected, query.Get(param), "query param %q", param)
+			}
+			for _, param := range tt.absentParams {
+				assert.False(t, query.Has(param), "query param %q should be absent", param)
+			}
+		})
+	}
+}
+
+func TestGCP_getBillingAPIClientAndURL(t *testing.T) {
 	gcp := &GCP{}
 
-	url := gcp.getBillingAPIURL("test-key", "USD")
-	expected := "https://cloudbilling.googleapis.com/v1/services/6F81-5844-456A/skus?key=test-key&currencyCode=USD"
-	assert.Equal(t, expected, url)
+	client, rawURL, err := gcp.getBillingAPIClientAndURL("test-key", "USD")
+
+	assert.NoError(t, err)
+	assert.Equal(t, http.DefaultClient, client)
+
+	parsedURL, err := url.Parse(rawURL)
+	assert.NoError(t, err)
+	query := parsedURL.Query()
+	assert.Equal(t, "test-key", query.Get("key"))
+	assert.Equal(t, "USD", query.Get("currencyCode"))
 }
 
 func TestGCP_GpuPricing(t *testing.T) {

+ 23 - 4
pkg/cloud/provider/provider.go

@@ -2,7 +2,6 @@ package provider
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net"
 	"net/http"
@@ -22,6 +21,7 @@ import (
 	"github.com/opencost/opencost/pkg/cloud/otc"
 	"github.com/opencost/opencost/pkg/cloud/ovh"
 	"github.com/opencost/opencost/pkg/cloud/scaleway"
+	"github.com/opencost/opencost/pkg/cloud/stackit"
 
 	"github.com/opencost/opencost/core/pkg/opencost"
 	"github.com/opencost/opencost/core/pkg/util"
@@ -115,6 +115,8 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 			cp.configFileName = "otc.json"
 		case opencost.OVHProvider:
 			cp.configFileName = "ovh.json"
+		case opencost.STACKITProvider:
+			cp.configFileName = "stackit.json"
 		case opencost.CSVProvider:
 			cp.configFileName = "default.json"
 		}
@@ -140,9 +142,6 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 		}, nil
 	case opencost.GCPProvider:
 		log.Info("Found ProviderID starting with \"gce\", using GCP Provider")
-		if apiKey == "" {
-			return nil, errors.New("Supply a GCP Key to start getting data")
-		}
 		return &gcp.GCP{
 			Clientset:        cache,
 			APIKey:           apiKey,
@@ -220,6 +219,14 @@ func NewProvider(cache clustercache.ClusterCache, apiKey string, config *config.
 			ClusterAccountID: cp.accountID,
 			Config:           NewProviderConfig(config, cp.configFileName),
 		}, nil
+	case opencost.STACKITProvider:
+		log.Info("Found STACKIT provider, using STACKIT Provider")
+		return &stackit.STACKIT{
+			Clientset:        cache,
+			ClusterRegion:    cp.region,
+			ClusterAccountID: cp.accountID,
+			Config:           NewProviderConfig(config, cp.configFileName),
+		}, nil
 	case opencost.DigitalOceanProvider:
 		log.Info("Detected DigitalOcean, using DOKS")
 		return &digitalocean.DOKS{
@@ -312,6 +319,18 @@ func getClusterProperties(node *clustercache.Node) clusterProperties {
 		log.Debug("using DigitalOcean provider")
 		cp.provider = opencost.DigitalOceanProvider
 		cp.configFileName = "digitalocean.json"
+	} else if strings.HasPrefix(providerID, "stackit") || strings.Contains(providerID, "stackit") {
+		log.Debug("using STACKIT provider")
+		cp.provider = opencost.STACKITProvider
+		cp.configFileName = "stackit.json"
+	} else if _, ok := node.Labels["node.stackit.cloud/ske"]; ok {
+		log.Debug("using STACKIT provider (detected via node label)")
+		cp.provider = opencost.STACKITProvider
+		cp.configFileName = "stackit.json"
+	} else if _, ok := node.Labels["topology.block-storage.csi.stackit.cloud/zone"]; ok {
+		log.Debug("using STACKIT provider (detected via CSI topology label)")
+		cp.provider = opencost.STACKITProvider
+		cp.configFileName = "stackit.json"
 	}
 	// Override provider to CSV if CSVProvider is used and custom provider is not set
 	if env.IsUseCSVProvider() {

+ 92 - 0
pkg/cloud/stackit/costconfiguration.go

@@ -0,0 +1,92 @@
+package stackit
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/pkg/cloud"
+)
+
+// CostConfiguration holds the configuration needed to connect to STACKIT Cost API
+type CostConfiguration struct {
+	CustomerAccountID     string `json:"customerAccountId"`
+	ProjectID             string `json:"projectId"`
+	ServiceAccountKeyPath string `json:"serviceAccountKeyPath,omitempty"`
+}
+
+func (c *CostConfiguration) Validate() error {
+	if c.CustomerAccountID == "" {
+		return fmt.Errorf("CostConfiguration: missing customerAccountId")
+	}
+	if c.ProjectID == "" {
+		return fmt.Errorf("CostConfiguration: missing projectId")
+	}
+	return nil
+}
+
+func (c *CostConfiguration) Equals(config cloud.Config) bool {
+	if config == nil {
+		return false
+	}
+	thatConfig, ok := config.(*CostConfiguration)
+	if !ok {
+		return false
+	}
+
+	return c.CustomerAccountID == thatConfig.CustomerAccountID &&
+		c.ProjectID == thatConfig.ProjectID &&
+		c.ServiceAccountKeyPath == thatConfig.ServiceAccountKeyPath
+}
+
+func (c *CostConfiguration) Sanitize() cloud.Config {
+	sanitized := &CostConfiguration{
+		CustomerAccountID: c.CustomerAccountID,
+		ProjectID:         c.ProjectID,
+	}
+	if c.ServiceAccountKeyPath != "" {
+		sanitized.ServiceAccountKeyPath = cloud.Redacted
+	}
+	return sanitized
+}
+
+func (c *CostConfiguration) Key() string {
+	return c.CustomerAccountID + "/" + c.ProjectID
+}
+
+func (c *CostConfiguration) Provider() string {
+	return opencost.STACKITProvider
+}
+
+func (c *CostConfiguration) UnmarshalJSON(b []byte) error {
+	var f interface{}
+	err := json.Unmarshal(b, &f)
+	if err != nil {
+		return err
+	}
+
+	fmap, ok := f.(map[string]interface{})
+	if !ok {
+		return fmt.Errorf("CostConfiguration: UnmarshalJSON: expected object")
+	}
+
+	customerAccountID, err := cloud.GetInterfaceValue[string](fmap, "customerAccountId")
+	if err != nil {
+		return fmt.Errorf("CostConfiguration: UnmarshalJSON: %w", err)
+	}
+	c.CustomerAccountID = customerAccountID
+
+	projectID, err := cloud.GetInterfaceValue[string](fmap, "projectId")
+	if err != nil {
+		return fmt.Errorf("CostConfiguration: UnmarshalJSON: %w", err)
+	}
+	c.ProjectID = projectID
+
+	if saKeyPath, ok := fmap["serviceAccountKeyPath"]; ok {
+		if s, ok := saKeyPath.(string); ok {
+			c.ServiceAccountKeyPath = s
+		}
+	}
+
+	return nil
+}

+ 228 - 0
pkg/cloud/stackit/costintegration.go

@@ -0,0 +1,228 @@
+package stackit
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/pkg/cloud"
+	"github.com/stackitcloud/stackit-sdk-go/core/config"
+	cost "github.com/stackitcloud/stackit-sdk-go/services/cost/v3api"
+)
+
+type CostIntegration struct {
+	CostConfiguration
+	ConnectionStatus cloud.ConnectionStatus
+}
+
+func (ci *CostIntegration) GetCloudCost(start time.Time, end time.Time) (*opencost.CloudCostSetRange, error) {
+	var opts []config.ConfigurationOption
+	if ci.ServiceAccountKeyPath != "" {
+		opts = append(opts, config.WithServiceAccountKeyPath(ci.ServiceAccountKeyPath))
+	}
+
+	client, err := cost.NewAPIClient(opts...)
+	if err != nil {
+		ci.ConnectionStatus = cloud.FailedConnection
+		return nil, fmt.Errorf("creating STACKIT cost API client: %w", err)
+	}
+
+	fromStr := start.Format("2006-01-02")
+	// STACKIT Cost API uses inclusive end dates; OpenCost windows are end-exclusive,
+	// so subtract one day to align.
+	toStr := end.AddDate(0, 0, -1).Format("2006-01-02")
+
+	resp, err := client.DefaultAPI.
+		GetCostsForProject(context.Background(), ci.CustomerAccountID, ci.ProjectID).
+		From(fromStr).
+		To(toStr).
+		Depth("service").
+		Granularity("daily").
+		Execute()
+	if err != nil {
+		ci.ConnectionStatus = cloud.FailedConnection
+		return nil, fmt.Errorf("querying STACKIT costs: %w", err)
+	}
+
+	ccsr, err := opencost.NewCloudCostSetRange(start, end, opencost.AccumulateOptionDay, ci.Key())
+	if err != nil {
+		return nil, err
+	}
+
+	if resp == nil || resp.ProjectCostWithDetailedServices == nil {
+		if ci.ConnectionStatus != cloud.SuccessfulConnection {
+			ci.ConnectionStatus = cloud.MissingData
+		}
+		return ccsr, nil
+	}
+
+	detailed := resp.ProjectCostWithDetailedServices
+
+	for _, svc := range detailed.GetServices() {
+		serviceName := svc.GetServiceName()
+		category := selectSTACKITCategory(serviceName)
+		sku := svc.GetSku()
+		regionID := extractRegionFromServiceName(serviceName)
+
+		reportData := svc.GetReportData()
+		if len(reportData) == 0 {
+			// No daily granularity data; use total charge
+			totalChargeCents := svc.GetTotalCharge()
+			totalDiscountCents := svc.GetTotalDiscount()
+			totalCharge := totalChargeCents / 100.0
+			totalDiscount := totalDiscountCents / 100.0
+			netCost := totalCharge
+
+			properties := &opencost.CloudCostProperties{
+				Provider:        opencost.STACKITProvider,
+				AccountID:       ci.CustomerAccountID,
+				InvoiceEntityID: ci.CustomerAccountID,
+				RegionID:        regionID,
+				Service:         serviceName,
+				Category:        category,
+				ProviderID:      sku,
+				Labels:          opencost.CloudCostLabels{},
+			}
+
+			listCost := totalCharge + totalDiscount
+
+			cc := &opencost.CloudCost{
+				Properties: properties,
+				Window:     opencost.NewWindow(&start, &end),
+				ListCost: opencost.CostMetric{
+					Cost: listCost,
+				},
+				NetCost: opencost.CostMetric{
+					Cost: netCost,
+				},
+				AmortizedNetCost: opencost.CostMetric{
+					Cost: netCost,
+				},
+				AmortizedCost: opencost.CostMetric{
+					Cost: listCost,
+				},
+				InvoicedCost: opencost.CostMetric{
+					Cost: netCost,
+				},
+			}
+
+			ccsr.LoadCloudCost(cc)
+			continue
+		}
+
+		for _, rd := range reportData {
+			chargeCents := rd.GetCharge()
+			discountCents := rd.GetDiscount()
+			charge := chargeCents / 100.0
+			discount := discountCents / 100.0
+
+			tp := rd.GetTimePeriod()
+			periodStart, periodEnd := parsePeriod(tp.GetStart(), tp.GetEnd(), start, end)
+
+			properties := &opencost.CloudCostProperties{
+				Provider:        opencost.STACKITProvider,
+				AccountID:       ci.CustomerAccountID,
+				InvoiceEntityID: ci.CustomerAccountID,
+				RegionID:        regionID,
+				Service:         serviceName,
+				Category:        category,
+				ProviderID:      sku,
+				Labels:          opencost.CloudCostLabels{},
+			}
+
+			listCost := charge + discount
+
+			cc := &opencost.CloudCost{
+				Properties: properties,
+				Window:     opencost.NewWindow(&periodStart, &periodEnd),
+				ListCost: opencost.CostMetric{
+					Cost: listCost,
+				},
+				NetCost: opencost.CostMetric{
+					Cost: charge,
+				},
+				AmortizedNetCost: opencost.CostMetric{
+					Cost: charge,
+				},
+				AmortizedCost: opencost.CostMetric{
+					Cost: listCost,
+				},
+				InvoicedCost: opencost.CostMetric{
+					Cost: charge,
+				},
+			}
+
+			ccsr.LoadCloudCost(cc)
+		}
+	}
+
+	ci.ConnectionStatus = cloud.SuccessfulConnection
+	return ccsr, nil
+}
+
+// parsePeriod parses start/end date strings from the STACKIT API, falling back to the given defaults.
+func parsePeriod(startStr, endStr string, defaultStart, defaultEnd time.Time) (time.Time, time.Time) {
+	periodStart := defaultStart
+	periodEnd := defaultEnd
+
+	if startStr != "" {
+		if t, err := time.Parse("2006-01-02", startStr); err == nil {
+			periodStart = t
+		} else if t, err := time.Parse(time.RFC3339, startStr); err == nil {
+			periodStart = t
+		}
+	}
+
+	if endStr != "" {
+		if t, err := time.Parse("2006-01-02", endStr); err == nil {
+			// End date is inclusive in the API, add one day for the window
+			periodEnd = t.AddDate(0, 0, 1)
+		} else if t, err := time.Parse(time.RFC3339, endStr); err == nil {
+			periodEnd = t
+		}
+	}
+
+	return periodStart, periodEnd
+}
+
+func (ci *CostIntegration) GetStatus() cloud.ConnectionStatus {
+	if ci.ConnectionStatus.String() == "" {
+		ci.ConnectionStatus = cloud.InitialStatus
+	}
+	return ci.ConnectionStatus
+}
+
+func (ci *CostIntegration) RefreshStatus() cloud.ConnectionStatus {
+	log.Warn("status refresh is not supported for the STACKIT provider")
+	return ci.ConnectionStatus
+}
+
+// extractRegionFromServiceName extracts the region suffix from a STACKIT Cost API
+// service name (e.g. "Tiny Server-t1.2-EU01" -> "eu01").
+func extractRegionFromServiceName(serviceName string) string {
+	idx := strings.LastIndex(serviceName, "-")
+	if idx >= 0 {
+		suffix := strings.ToLower(serviceName[idx+1:])
+		if strings.HasPrefix(suffix, "eu") || strings.HasPrefix(suffix, "us") {
+			return suffix
+		}
+	}
+	return "eu01"
+}
+
+func selectSTACKITCategory(serviceName string) string {
+	lower := strings.ToLower(serviceName)
+	switch {
+	case strings.Contains(lower, "compute") || strings.Contains(lower, "server") || strings.Contains(lower, "ske"):
+		return opencost.ComputeCategory
+	case strings.Contains(lower, "storage") || strings.Contains(lower, "object store") || strings.Contains(lower, "backup"):
+		return opencost.StorageCategory
+	case strings.Contains(lower, "network") || strings.Contains(lower, "load balancer") || strings.Contains(lower, "dns"):
+		return opencost.NetworkCategory
+	default:
+		return opencost.OtherCategory
+	}
+}

+ 109 - 0
pkg/cloud/stackit/costintegration_test.go

@@ -0,0 +1,109 @@
+package stackit
+
+import (
+	"testing"
+	"time"
+)
+
+func TestParsePeriodDateOnly(t *testing.T) {
+	defaultStart := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+	defaultEnd := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC)
+
+	start, end := parsePeriod("2026-01-05", "2026-01-10", defaultStart, defaultEnd)
+
+	expectedStart := time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC)
+	expectedEnd := time.Date(2026, 1, 11, 0, 0, 0, 0, time.UTC) // inclusive end + 1 day
+
+	if !start.Equal(expectedStart) {
+		t.Errorf("start = %v, want %v", start, expectedStart)
+	}
+	if !end.Equal(expectedEnd) {
+		t.Errorf("end = %v, want %v", end, expectedEnd)
+	}
+}
+
+func TestParsePeriodRFC3339(t *testing.T) {
+	defaultStart := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+	defaultEnd := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC)
+
+	start, end := parsePeriod("2026-01-05T10:00:00Z", "2026-01-10T18:00:00Z", defaultStart, defaultEnd)
+
+	expectedStart := time.Date(2026, 1, 5, 10, 0, 0, 0, time.UTC)
+	expectedEnd := time.Date(2026, 1, 10, 18, 0, 0, 0, time.UTC) // RFC3339 used as-is
+
+	if !start.Equal(expectedStart) {
+		t.Errorf("start = %v, want %v", start, expectedStart)
+	}
+	if !end.Equal(expectedEnd) {
+		t.Errorf("end = %v, want %v", end, expectedEnd)
+	}
+}
+
+func TestParsePeriodEmpty(t *testing.T) {
+	defaultStart := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+	defaultEnd := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC)
+
+	start, end := parsePeriod("", "", defaultStart, defaultEnd)
+
+	if !start.Equal(defaultStart) {
+		t.Errorf("start = %v, want default %v", start, defaultStart)
+	}
+	if !end.Equal(defaultEnd) {
+		t.Errorf("end = %v, want default %v", end, defaultEnd)
+	}
+}
+
+func TestParsePeriodInvalidFallsBack(t *testing.T) {
+	defaultStart := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+	defaultEnd := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC)
+
+	start, end := parsePeriod("not-a-date", "also-not", defaultStart, defaultEnd)
+
+	if !start.Equal(defaultStart) {
+		t.Errorf("start = %v, want default %v", start, defaultStart)
+	}
+	if !end.Equal(defaultEnd) {
+		t.Errorf("end = %v, want default %v", end, defaultEnd)
+	}
+}
+
+func TestExtractRegionFromServiceName(t *testing.T) {
+	tests := []struct {
+		service string
+		want    string
+	}{
+		{"Tiny Server-t1.2-EU01", "eu01"},
+		{"Object Storage Premium-EU02", "eu02"},
+		{"GPU Server-n2.14d.g1-EU01", "eu01"},
+		{"DNS-100-EU01", "eu01"},
+		{"Some Future Service-US01", "us01"},
+		{"NoRegionSuffix", "eu01"},
+	}
+	for _, tt := range tests {
+		got := extractRegionFromServiceName(tt.service)
+		if got != tt.want {
+			t.Errorf("extractRegionFromServiceName(%q) = %q, want %q", tt.service, got, tt.want)
+		}
+	}
+}
+
+func TestSelectSTACKITCategory(t *testing.T) {
+	tests := []struct {
+		service string
+		want    string
+	}{
+		{"Compute Engine", "Compute"},
+		{"SKE Cluster", "Compute"},
+		{"Block Storage", "Storage"},
+		{"Object Store", "Storage"},
+		{"Load Balancer", "Network"},
+		{"DNS Service", "Network"},
+		{"Some Other Service", "Other"},
+	}
+	for _, tt := range tests {
+		got := selectSTACKITCategory(tt.service)
+		if got != tt.want {
+			t.Errorf("selectSTACKITCategory(%q) = %q, want %q", tt.service, got, tt.want)
+		}
+	}
+}

+ 321 - 0
pkg/cloud/stackit/pim.go

@@ -0,0 +1,321 @@
+package stackit
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/log"
+)
+
+const (
+	pimBaseURL    = "https://pim-service.pim.stackit.cloud"
+	pimMaxPage    = 100
+	pimMaxPages   = 200 // safety limit to prevent infinite pagination
+	pimTimeoutSec = 30
+)
+
+var pimHTTPClient = &http.Client{
+	Timeout: pimTimeoutSec * time.Second,
+}
+
+// pimFlavorPricing holds PIM-fetched pricing for a VM flavor.
+type pimFlavorPricing struct {
+	HourlyCost string
+	VCPU       int
+	RAMGB      float64
+	GPUCount   int
+	GPUType    string
+}
+
+// pimStoragePricing holds PIM-fetched pricing for a storage class.
+type pimStoragePricing struct {
+	CostPerGBHr string
+}
+
+// pimSearchRequest is the POST body for /v2/skus/search.
+type pimSearchRequest struct {
+	GeneralProductGroup string `json:"generalProductGroup,omitempty"`
+	CategoryName        string `json:"categoryName,omitempty"`
+	ProductName         string `json:"productName,omitempty"`
+	Metro               *bool  `json:"metro,omitempty"`
+}
+
+// pimSKU represents the relevant fields of a PublicSKU from the PIM API.
+type pimSKU struct {
+	ID                        string                  `json:"id"`
+	Name                      string                  `json:"name"`
+	CategoryName              string                  `json:"categoryName"`
+	GeneralProductGroup       string                  `json:"generalProductGroup"`
+	Unit                      string                  `json:"unit"`
+	UnitBilling               string                  `json:"unitBilling"`
+	Region                    string                  `json:"region"`
+	CPUOverprovisioning       *bool                   `json:"cpuOverprovisioning"`
+	Prices                    []pimPrice              `json:"prices"`
+	ProductSpecificAttributes pimProductSpecificAttrs `json:"productSpecificAttributes"`
+	ServiceID                 []string                `json:"serviceId"`
+}
+
+type pimPrice struct {
+	Value        string `json:"value"`
+	MonthlyPrice string `json:"monthlyPrice"`
+	CurrencyCode string `json:"currencyCode"`
+}
+
+type pimProductSpecificAttrs struct {
+	Discriminator string   `json:"discriminator"`
+	Flavor        string   `json:"flavor"`
+	Hardware      string   `json:"hardware"`
+	VCPU          *int     `json:"vCPU"`
+	RAM           *float64 `json:"ram"`
+	Metro         *bool    `json:"metro"`
+	OS            string   `json:"os"`
+	// Storage-specific
+	Class       string `json:"class"`
+	StorageType string `json:"storage"`
+	Type        string `json:"type"`
+}
+
+type pimSearchResponse struct {
+	Meta struct {
+		NextCursor string `json:"nextCursor"`
+		PageSize   int    `json:"pageSize"`
+	} `json:"meta"`
+	Data []pimSKU `json:"data"`
+}
+
+// fetchAllPIMSKUs fetches all SKUs matching the search criteria, handling pagination.
+func fetchAllPIMSKUs(req pimSearchRequest) ([]pimSKU, error) {
+	var allSKUs []pimSKU
+	cursor := ""
+
+	for page := 0; page < pimMaxPages; page++ {
+		reqURL := fmt.Sprintf("%s/v2/skus/search?pageSize=%d", pimBaseURL, pimMaxPage)
+		if cursor != "" {
+			reqURL += "&cursor=" + url.QueryEscape(cursor)
+		}
+
+		body, err := json.Marshal(req)
+		if err != nil {
+			return nil, fmt.Errorf("marshaling PIM search request: %w", err)
+		}
+
+		httpReq, err := http.NewRequest("POST", reqURL, bytes.NewReader(body))
+		if err != nil {
+			return nil, fmt.Errorf("creating PIM request: %w", err)
+		}
+		httpReq.Header.Set("Content-Type", "application/json")
+
+		resp, err := pimHTTPClient.Do(httpReq)
+		if err != nil {
+			return nil, fmt.Errorf("PIM API request failed: %w", err)
+		}
+
+		respBody, err := io.ReadAll(resp.Body)
+		resp.Body.Close()
+		if err != nil {
+			return nil, fmt.Errorf("reading PIM response: %w", err)
+		}
+
+		if resp.StatusCode != http.StatusOK {
+			return nil, fmt.Errorf("PIM API returned status %d: %s", resp.StatusCode, string(respBody))
+		}
+
+		var searchResp pimSearchResponse
+		if err := json.Unmarshal(respBody, &searchResp); err != nil {
+			return nil, fmt.Errorf("decoding PIM response: %w", err)
+		}
+
+		allSKUs = append(allSKUs, searchResp.Data...)
+
+		if searchResp.Meta.NextCursor == "" || len(searchResp.Data) == 0 || len(searchResp.Data) < pimMaxPage {
+			break
+		}
+		cursor = searchResp.Meta.NextCursor
+	}
+
+	return allSKUs, nil
+}
+
+// parsePIMVMFlavors converts VM SKUs into a flavor -> pricing map.
+// It filters for non-metro SKUs and extracts flavor name, vCPU, RAM, and hourly price.
+func parsePIMVMFlavors(skus []pimSKU) map[string]*pimFlavorPricing {
+	flavors := make(map[string]*pimFlavorPricing)
+
+	for _, sku := range skus {
+		attrs := sku.ProductSpecificAttributes
+		flavor := attrs.Flavor
+		if flavor == "" {
+			continue
+		}
+
+		// Skip metro variants (priced separately, ~2x)
+		if attrs.Metro != nil && *attrs.Metro {
+			continue
+		}
+
+		// Must have pricing data
+		if len(sku.Prices) == 0 {
+			continue
+		}
+
+		hourlyPrice := sku.Prices[0].Value
+		if hourlyPrice == "" {
+			continue
+		}
+
+		vcpu := 0
+		if attrs.VCPU != nil {
+			vcpu = *attrs.VCPU
+		}
+		ramGB := 0.0
+		if attrs.RAM != nil {
+			ramGB = *attrs.RAM
+		}
+
+		// Detect GPU count from flavor name (e.g. "n1.14d.g1" -> 1, "n1.28d.g2" -> 2)
+		gpuCount := 0
+		gpuType := ""
+		if strings.HasPrefix(flavor, "n1.") || strings.HasPrefix(flavor, "n2.") || strings.HasPrefix(flavor, "n3.") {
+			gpuCount = gpuCountFromFlavor(flavor)
+			if gpuCount > 0 {
+				gpuType = gpuTypeFromFlavor(flavor)
+			}
+		}
+
+		flavors[flavor] = &pimFlavorPricing{
+			HourlyCost: hourlyPrice,
+			VCPU:       vcpu,
+			RAMGB:      ramGB,
+			GPUCount:   gpuCount,
+			GPUType:    gpuType,
+		}
+	}
+
+	return flavors
+}
+
+// parsePIMStoragePricing extracts per-GB-hour storage pricing from Storage SKUs.
+// Returns a map keyed by storage class name (e.g. "storage_premium_perf0").
+// Also includes a "default" entry for the cheapest capacity-based storage found.
+func parsePIMStoragePricing(skus []pimSKU) map[string]*pimStoragePricing {
+	pricing := make(map[string]*pimStoragePricing)
+	var defaultCost float64
+
+	for _, sku := range skus {
+		attrs := sku.ProductSpecificAttributes
+
+		// Skip metro variants
+		if attrs.Metro != nil && *attrs.Metro {
+			continue
+		}
+
+		// Only capacity-based storage with per-GB/hour billing
+		if sku.UnitBilling != "per GB/hour" {
+			continue
+		}
+
+		if len(sku.Prices) == 0 || sku.Prices[0].Value == "" {
+			continue
+		}
+
+		costStr := sku.Prices[0].Value
+
+		// Key by storage class if available
+		if attrs.Class != "" {
+			pricing[attrs.Class] = &pimStoragePricing{CostPerGBHr: costStr}
+		}
+
+		// Track cheapest for default
+		cost, err := strconv.ParseFloat(costStr, 64)
+		if err == nil && (defaultCost == 0 || cost < defaultCost) {
+			defaultCost = cost
+			pricing["default"] = &pimStoragePricing{CostPerGBHr: costStr}
+		}
+	}
+
+	return pricing
+}
+
+// gpuCountFromFlavor extracts GPU count from a STACKIT GPU flavor name.
+// e.g. "n1.14d.g1" -> 1, "n1.28d.g2" -> 2, "n3.104d.g8" -> 8
+func gpuCountFromFlavor(flavor string) int {
+	parts := strings.Split(flavor, ".g")
+	if len(parts) == 2 {
+		if count, err := strconv.Atoi(parts[1]); err == nil {
+			return count
+		}
+	}
+	return 0
+}
+
+// gpuTypeFromFlavor returns the GPU model based on the flavor prefix.
+func gpuTypeFromFlavor(flavor string) string {
+	switch {
+	case strings.HasPrefix(flavor, "n1."):
+		return "NVIDIA A100"
+	case strings.HasPrefix(flavor, "n2."):
+		return "NVIDIA L40S"
+	case strings.HasPrefix(flavor, "n3."):
+		return "NVIDIA H100 HGX"
+	default:
+		return ""
+	}
+}
+
+// downloadPIMPricing fetches all VM, GPU, and storage pricing from the PIM API.
+// Returns the flavor map, storage map, and any error.
+func downloadPIMPricing() (map[string]*pimFlavorPricing, map[string]*pimStoragePricing, error) {
+	metro := false
+
+	// 1. Fetch non-metro VM SKUs
+	log.Infof("STACKIT: fetching VM pricing from PIM API...")
+	vmSKUs, err := fetchAllPIMSKUs(pimSearchRequest{
+		GeneralProductGroup: "Virtual Machines",
+		Metro:               &metro,
+	})
+	if err != nil {
+		return nil, nil, fmt.Errorf("fetching VM SKUs: %w", err)
+	}
+
+	flavors := parsePIMVMFlavors(vmSKUs)
+	log.Infof("STACKIT: fetched %d VM flavor prices from PIM API", len(flavors))
+
+	// 2. Fetch GPU SKUs (separate category)
+	log.Infof("STACKIT: fetching GPU pricing from PIM API...")
+	gpuSKUs, err := fetchAllPIMSKUs(pimSearchRequest{
+		CategoryName: "Compute Engine GPU",
+		Metro:        &metro,
+	})
+	if err != nil {
+		log.Warnf("STACKIT: failed to fetch GPU pricing from PIM API: %v", err)
+	} else {
+		gpuFlavors := parsePIMVMFlavors(gpuSKUs)
+		for k, v := range gpuFlavors {
+			flavors[k] = v
+		}
+		log.Infof("STACKIT: fetched %d GPU flavor prices from PIM API", len(gpuFlavors))
+	}
+
+	// 3. Fetch Storage SKUs
+	log.Infof("STACKIT: fetching Storage pricing from PIM API...")
+	storageSKUs, err := fetchAllPIMSKUs(pimSearchRequest{
+		CategoryName: "Storage",
+		Metro:        &metro,
+	})
+	var storagePricing map[string]*pimStoragePricing
+	if err != nil {
+		log.Warnf("STACKIT: failed to fetch storage pricing from PIM API: %v", err)
+	} else {
+		storagePricing = parsePIMStoragePricing(storageSKUs)
+		log.Infof("STACKIT: fetched %d storage price entries from PIM API", len(storagePricing))
+	}
+
+	return flavors, storagePricing, nil
+}

+ 247 - 0
pkg/cloud/stackit/pim_test.go

@@ -0,0 +1,247 @@
+package stackit
+
+import (
+	"testing"
+)
+
+func TestGpuCountFromFlavor(t *testing.T) {
+	tests := []struct {
+		flavor string
+		want   int
+	}{
+		{"n1.14d.g1", 1},
+		{"n1.28d.g2", 2},
+		{"n3.104d.g8", 8},
+		{"c1i.2", 0},
+		{"g2i.1", 0},
+		{"n1.14d", 0},
+	}
+	for _, tt := range tests {
+		got := gpuCountFromFlavor(tt.flavor)
+		if got != tt.want {
+			t.Errorf("gpuCountFromFlavor(%q) = %d, want %d", tt.flavor, got, tt.want)
+		}
+	}
+}
+
+func TestGpuTypeFromFlavor(t *testing.T) {
+	tests := []struct {
+		flavor string
+		want   string
+	}{
+		{"n1.14d.g1", "NVIDIA A100"},
+		{"n2.28d.g2", "NVIDIA L40S"},
+		{"n3.104d.g8", "NVIDIA H100 HGX"},
+		{"c1i.2", ""},
+	}
+	for _, tt := range tests {
+		got := gpuTypeFromFlavor(tt.flavor)
+		if got != tt.want {
+			t.Errorf("gpuTypeFromFlavor(%q) = %q, want %q", tt.flavor, got, tt.want)
+		}
+	}
+}
+
+func TestParsePIMVMFlavors(t *testing.T) {
+	metro := true
+	nonMetro := false
+	vcpu2 := 2
+	ram4 := 4.0
+
+	skus := []pimSKU{
+		{
+			Name: "g2i.1",
+			ProductSpecificAttributes: pimProductSpecificAttrs{
+				Flavor: "g2i.1",
+				VCPU:   &vcpu2,
+				RAM:    &ram4,
+				Metro:  &nonMetro,
+			},
+			Prices: []pimPrice{{Value: "0.05045"}},
+		},
+		{
+			// Metro variant should be skipped
+			Name: "g2i.1-metro",
+			ProductSpecificAttributes: pimProductSpecificAttrs{
+				Flavor: "g2i.1",
+				VCPU:   &vcpu2,
+				RAM:    &ram4,
+				Metro:  &metro,
+			},
+			Prices: []pimPrice{{Value: "0.10"}},
+		},
+		{
+			// No flavor -> skipped
+			Name:                      "no-flavor",
+			ProductSpecificAttributes: pimProductSpecificAttrs{},
+			Prices:                    []pimPrice{{Value: "0.01"}},
+		},
+		{
+			// No price -> skipped
+			Name: "no-price",
+			ProductSpecificAttributes: pimProductSpecificAttrs{
+				Flavor: "c1i.2",
+				VCPU:   &vcpu2,
+				RAM:    &ram4,
+			},
+			Prices: []pimPrice{},
+		},
+	}
+
+	flavors := parsePIMVMFlavors(skus)
+
+	if len(flavors) != 1 {
+		t.Fatalf("expected 1 flavor, got %d", len(flavors))
+	}
+
+	f, ok := flavors["g2i.1"]
+	if !ok {
+		t.Fatal("expected flavor g2i.1")
+	}
+	if f.HourlyCost != "0.05045" {
+		t.Errorf("expected hourly cost 0.05045, got %s", f.HourlyCost)
+	}
+	if f.VCPU != 2 {
+		t.Errorf("expected 2 vCPU, got %d", f.VCPU)
+	}
+	if f.RAMGB != 4.0 {
+		t.Errorf("expected 4.0 RAM GB, got %f", f.RAMGB)
+	}
+}
+
+func TestParsePIMVMFlavorsGPU(t *testing.T) {
+	nonMetro := false
+	vcpu14 := 14
+	ram56 := 56.0
+
+	skus := []pimSKU{
+		{
+			Name: "n1.14d.g1",
+			ProductSpecificAttributes: pimProductSpecificAttrs{
+				Flavor: "n1.14d.g1",
+				VCPU:   &vcpu14,
+				RAM:    &ram56,
+				Metro:  &nonMetro,
+			},
+			Prices: []pimPrice{{Value: "3.50"}},
+		},
+	}
+
+	flavors := parsePIMVMFlavors(skus)
+	f, ok := flavors["n1.14d.g1"]
+	if !ok {
+		t.Fatal("expected flavor n1.14d.g1")
+	}
+	if f.GPUCount != 1 {
+		t.Errorf("expected GPU count 1, got %d", f.GPUCount)
+	}
+	if f.GPUType != "NVIDIA A100" {
+		t.Errorf("expected GPU type NVIDIA A100, got %s", f.GPUType)
+	}
+}
+
+func TestParsePIMVMFlavorsNonGPUNPrefix(t *testing.T) {
+	nonMetro := false
+	vcpu14 := 14
+	ram56 := 56.0
+
+	skus := []pimSKU{
+		{
+			Name: "n1.14d",
+			ProductSpecificAttributes: pimProductSpecificAttrs{
+				Flavor: "n1.14d",
+				VCPU:   &vcpu14,
+				RAM:    &ram56,
+				Metro:  &nonMetro,
+			},
+			Prices: []pimPrice{{Value: "1.50"}},
+		},
+	}
+
+	flavors := parsePIMVMFlavors(skus)
+	f, ok := flavors["n1.14d"]
+	if !ok {
+		t.Fatal("expected flavor n1.14d")
+	}
+	if f.GPUCount != 0 {
+		t.Errorf("expected GPU count 0, got %d", f.GPUCount)
+	}
+	if f.GPUType != "" {
+		t.Errorf("expected empty GPU type for non-GPU n1 flavor, got %q", f.GPUType)
+	}
+}
+
+func TestParsePIMStoragePricing(t *testing.T) {
+	nonMetro := false
+
+	skus := []pimSKU{
+		{
+			Name:        "premium-perf0",
+			UnitBilling: "per GB/hour",
+			ProductSpecificAttributes: pimProductSpecificAttrs{
+				Class: "storage_premium_perf0",
+				Metro: &nonMetro,
+			},
+			Prices: []pimPrice{{Value: "0.0001"}},
+		},
+		{
+			Name:        "premium-perf2",
+			UnitBilling: "per GB/hour",
+			ProductSpecificAttributes: pimProductSpecificAttrs{
+				Class: "storage_premium_perf2",
+				Metro: &nonMetro,
+			},
+			Prices: []pimPrice{{Value: "0.0005"}},
+		},
+		{
+			// Non-GB/hour billing -> skipped
+			Name:        "iops-based",
+			UnitBilling: "per IOPS/hour",
+			ProductSpecificAttributes: pimProductSpecificAttrs{
+				Class: "storage_iops",
+				Metro: &nonMetro,
+			},
+			Prices: []pimPrice{{Value: "0.01"}},
+		},
+	}
+
+	pricing := parsePIMStoragePricing(skus)
+
+	if _, ok := pricing["storage_premium_perf0"]; !ok {
+		t.Error("expected storage_premium_perf0")
+	}
+	if _, ok := pricing["storage_premium_perf2"]; !ok {
+		t.Error("expected storage_premium_perf2")
+	}
+	if _, ok := pricing["storage_iops"]; ok {
+		t.Error("storage_iops should have been skipped (non GB/hour billing)")
+	}
+
+	// Default should be the cheapest
+	def, ok := pricing["default"]
+	if !ok {
+		t.Fatal("expected default storage entry")
+	}
+	if def.CostPerGBHr != "0.0001" {
+		t.Errorf("expected default cost 0.0001, got %s", def.CostPerGBHr)
+	}
+}
+
+func TestPaginationTermination(t *testing.T) {
+	// Verify that empty data or empty cursor terminates pagination.
+	// This is a logic check - fetchAllPIMSKUs breaks on:
+	//   searchResp.Meta.NextCursor == "" || len(searchResp.Data) == 0
+	// We can't call the real API, but we verify the parsing logic
+	// handles the termination conditions in parsePIMVMFlavors.
+
+	// Empty input should produce empty output
+	flavors := parsePIMVMFlavors(nil)
+	if len(flavors) != 0 {
+		t.Errorf("expected 0 flavors from nil input, got %d", len(flavors))
+	}
+
+	flavors = parsePIMVMFlavors([]pimSKU{})
+	if len(flavors) != 0 {
+		t.Errorf("expected 0 flavors from empty input, got %d", len(flavors))
+	}
+}

+ 390 - 0
pkg/cloud/stackit/provider.go

@@ -0,0 +1,390 @@
+package stackit
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+	"sync"
+
+	coreenv "github.com/opencost/opencost/core/pkg/env"
+	"github.com/opencost/opencost/pkg/cloud/models"
+	"github.com/opencost/opencost/pkg/cloud/utils"
+
+	"github.com/opencost/opencost/core/pkg/clustercache"
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/core/pkg/util"
+	"github.com/opencost/opencost/core/pkg/util/json"
+	"github.com/opencost/opencost/pkg/env"
+
+	"github.com/opencost/opencost/core/pkg/log"
+)
+
+const (
+	StackitPIMPricingSource = "STACKIT PIM API Pricing"
+)
+
+type STACKIT struct {
+	Clientset               clustercache.ClusterCache
+	Config                  models.ProviderConfig
+	ClusterRegion           string
+	ClusterAccountID        string
+	DownloadPricingDataLock sync.RWMutex
+
+	// PIM API pricing cache (protected by DownloadPricingDataLock)
+	pimFlavors map[string]*pimFlavorPricing
+	pimStorage map[string]*pimStoragePricing
+}
+
+func (s *STACKIT) PricingSourceSummary() interface{} {
+	s.DownloadPricingDataLock.RLock()
+	defer s.DownloadPricingDataLock.RUnlock()
+	return s.pimFlavors
+}
+
+func (s *STACKIT) DownloadPricingData() error {
+	s.DownloadPricingDataLock.Lock()
+	defer s.DownloadPricingDataLock.Unlock()
+
+	flavors, storage, err := downloadPIMPricing()
+	if err != nil {
+		return fmt.Errorf("STACKIT: failed to download pricing from PIM API: %w", err)
+	}
+
+	if len(flavors) == 0 {
+		return fmt.Errorf("STACKIT: PIM API returned no VM flavor pricing data")
+	}
+
+	s.pimFlavors = flavors
+	s.pimStorage = storage
+	return nil
+}
+
+func (s *STACKIT) AllNodePricing() (interface{}, error) {
+	s.DownloadPricingDataLock.RLock()
+	defer s.DownloadPricingDataLock.RUnlock()
+	return s.pimFlavors, nil
+}
+
+type stackitKey struct {
+	Labels map[string]string
+}
+
+func (k *stackitKey) Features() string {
+	instanceType, _ := util.GetInstanceType(k.Labels)
+	zone, _ := util.GetZone(k.Labels)
+	return zone + "," + instanceType
+}
+
+func (k *stackitKey) GPUCount() int {
+	return 0
+}
+
+func (k *stackitKey) GPUType() string {
+	return ""
+}
+
+func (k *stackitKey) ID() string {
+	return ""
+}
+
+func (s *STACKIT) NodePricing(key models.Key) (*models.Node, models.PricingMetadata, error) {
+	s.DownloadPricingDataLock.RLock()
+	defer s.DownloadPricingDataLock.RUnlock()
+
+	meta := models.PricingMetadata{}
+
+	// Extract instance type from key features ("zone,instanceType")
+	features := key.Features()
+	parts := strings.Split(features, ",")
+	instanceType := ""
+	if len(parts) >= 2 {
+		instanceType = parts[1]
+	}
+
+	pf, ok := s.pimFlavors[instanceType]
+	if !ok {
+		return nil, meta, fmt.Errorf("STACKIT: no pricing data found for instance type %q", instanceType)
+	}
+
+	ramBytes := int64(pf.RAMGB * 1024 * 1024 * 1024)
+	return &models.Node{
+		Cost:         pf.HourlyCost,
+		VCPU:         fmt.Sprintf("%d", pf.VCPU),
+		RAM:          fmt.Sprintf("%g", pf.RAMGB),
+		RAMBytes:     fmt.Sprintf("%d", ramBytes),
+		GPU:          fmt.Sprintf("%d", pf.GPUCount),
+		GPUName:      pf.GPUType,
+		InstanceType: instanceType,
+		Region:       s.ClusterRegion,
+		PricingType:  models.DefaultPrices,
+	}, meta, nil
+}
+
+func (s *STACKIT) LoadBalancerPricing() (*models.LoadBalancer, error) {
+	config, err := s.GetConfig()
+	if err != nil {
+		return nil, fmt.Errorf("unable to get config: %w", err)
+	}
+
+	lbPrice := 0.0
+	if config.DefaultLBPrice != "" {
+		lbPrice, _ = strconv.ParseFloat(config.DefaultLBPrice, 64)
+	}
+
+	return &models.LoadBalancer{
+		Cost: lbPrice,
+	}, nil
+}
+
+func (s *STACKIT) NetworkPricing() (*models.Network, error) {
+	config, err := s.GetConfig()
+	if err != nil {
+		return nil, fmt.Errorf("unable to get config: %w", err)
+	}
+
+	zoneEgress, _ := strconv.ParseFloat(config.ZoneNetworkEgress, 64)
+	regionEgress, _ := strconv.ParseFloat(config.RegionNetworkEgress, 64)
+	internetEgress, _ := strconv.ParseFloat(config.InternetNetworkEgress, 64)
+	natEgress, _ := strconv.ParseFloat(config.NatGatewayEgress, 64)
+	natIngress, _ := strconv.ParseFloat(config.NatGatewayIngress, 64)
+
+	return &models.Network{
+		ZoneNetworkEgressCost:     zoneEgress,
+		RegionNetworkEgressCost:   regionEgress,
+		InternetNetworkEgressCost: internetEgress,
+		NatGatewayEgressCost:      natEgress,
+		NatGatewayIngressCost:     natIngress,
+	}, nil
+}
+
+func (s *STACKIT) GetKey(l map[string]string, n *clustercache.Node) models.Key {
+	return &stackitKey{
+		Labels: l,
+	}
+}
+
+type stackitPVKey struct {
+	Labels                 map[string]string
+	StorageClassName       string
+	StorageClassParameters map[string]string
+	Name                   string
+	Zone                   string
+}
+
+func (key *stackitPVKey) ID() string {
+	return ""
+}
+
+func (key *stackitPVKey) GetStorageClass() string {
+	return key.StorageClassName
+}
+
+func (key *stackitPVKey) Features() string {
+	return key.Zone
+}
+
+func (s *STACKIT) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
+	zone := defaultRegion
+	if pv.Spec.CSI != nil {
+		parts := strings.Split(pv.Spec.CSI.VolumeHandle, "/")
+		if len(parts) >= 2 && parts[0] != "" {
+			zone = parts[0]
+		}
+	}
+	return &stackitPVKey{
+		Labels:                 pv.Labels,
+		StorageClassName:       pv.Spec.StorageClassName,
+		StorageClassParameters: parameters,
+		Name:                   pv.Name,
+		Zone:                   zone,
+	}
+}
+
+func (s *STACKIT) GpuPricing(nodeLabels map[string]string) (string, error) {
+	s.DownloadPricingDataLock.RLock()
+	defer s.DownloadPricingDataLock.RUnlock()
+
+	instanceType, _ := util.GetInstanceType(nodeLabels)
+	pf, ok := s.pimFlavors[instanceType]
+	if !ok || pf.GPUCount == 0 || pf.HourlyCost == "" {
+		return "", nil
+	}
+
+	hourlyCost, err := strconv.ParseFloat(pf.HourlyCost, 64)
+	if err != nil {
+		return "", fmt.Errorf("parsing STACKIT GPU hourly cost %q for %q: %w", pf.HourlyCost, instanceType, err)
+	}
+
+	perGPUCost := hourlyCost / float64(pf.GPUCount)
+	return strconv.FormatFloat(perGPUCost, 'f', -1, 64), nil
+}
+
+func (s *STACKIT) PVPricing(pvk models.PVKey) (*models.PV, error) {
+	s.DownloadPricingDataLock.RLock()
+	defer s.DownloadPricingDataLock.RUnlock()
+
+	storageClass := pvk.GetStorageClass()
+
+	if len(s.pimStorage) > 0 {
+		// Exact storage class match
+		if sp, ok := s.pimStorage[storageClass]; ok {
+			return &models.PV{
+				Cost:  sp.CostPerGBHr,
+				Class: storageClass,
+			}, nil
+		}
+		// Default to cheapest capacity-based storage
+		if sp, ok := s.pimStorage["default"]; ok {
+			return &models.PV{
+				Cost:  sp.CostPerGBHr,
+				Class: storageClass,
+			}, nil
+		}
+	}
+
+	log.Debugf("STACKIT: no PV pricing found for storage class %q", storageClass)
+	return &models.PV{}, nil
+}
+
+func (s *STACKIT) ServiceAccountStatus() *models.ServiceAccountStatus {
+	return &models.ServiceAccountStatus{
+		Checks: []*models.ServiceAccountCheck{},
+	}
+}
+
+func (*STACKIT) ClusterManagementPricing() (string, float64, error) {
+	return "", 0.0, nil
+}
+
+func (s *STACKIT) CombinedDiscountForNode(instanceType string, isPreemptible bool, defaultDiscount, negotiatedDiscount float64) float64 {
+	return 1.0 - ((1.0 - defaultDiscount) * (1.0 - negotiatedDiscount))
+}
+
+func (s *STACKIT) Regions() []string {
+	regionOverrides := env.GetRegionOverrideList()
+	if len(regionOverrides) > 0 {
+		log.Debugf("Overriding STACKIT regions with configured region list: %+v", regionOverrides)
+		return regionOverrides
+	}
+	return []string{"eu01"}
+}
+
+func (*STACKIT) ApplyReservedInstancePricing(map[string]*models.Node) {}
+
+func (*STACKIT) GetAddresses() ([]byte, error) {
+	return nil, nil
+}
+
+func (*STACKIT) GetDisks() ([]byte, error) {
+	return nil, nil
+}
+
+func (*STACKIT) GetOrphanedResources() ([]models.OrphanedResource, error) {
+	return nil, errors.New("not implemented")
+}
+
+func (s *STACKIT) ClusterInfo() (map[string]string, error) {
+	remoteEnabled := env.IsRemoteEnabled()
+
+	m := make(map[string]string)
+	m["name"] = "STACKIT Cluster #1"
+	c, err := s.GetConfig()
+	if err != nil {
+		return nil, err
+	}
+	if c.ClusterName != "" {
+		m["name"] = c.ClusterName
+	}
+	m["provider"] = opencost.STACKITProvider
+	m["region"] = s.ClusterRegion
+	m["account"] = s.ClusterAccountID
+	m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
+	m["id"] = coreenv.GetClusterID()
+	return m, nil
+}
+
+func (s *STACKIT) UpdateConfigFromConfigMap(a map[string]string) (*models.CustomPricing, error) {
+	return s.Config.UpdateFromMap(a)
+}
+
+func (s *STACKIT) UpdateConfig(r io.Reader, updateType string) (*models.CustomPricing, error) {
+	cp, err := s.Config.Update(func(c *models.CustomPricing) error {
+		a := make(map[string]interface{})
+		err := json.NewDecoder(r).Decode(&a)
+		if err != nil {
+			return err
+		}
+		for k, v := range a {
+			kUpper := utils.ToTitle.String(k)
+			vstr, ok := v.(string)
+			if ok {
+				err := models.SetCustomPricingField(c, kUpper, vstr)
+				if err != nil {
+					return fmt.Errorf("error setting custom pricing field: %w", err)
+				}
+			} else {
+				return fmt.Errorf("type error while updating config for %s", kUpper)
+			}
+		}
+
+		if env.IsRemoteEnabled() {
+			err := utils.UpdateClusterMeta(coreenv.GetClusterID(), c.ClusterName)
+			if err != nil {
+				return err
+			}
+		}
+
+		return nil
+	})
+	if err != nil {
+		return cp, err
+	}
+
+	if refreshErr := s.DownloadPricingData(); refreshErr != nil {
+		log.Warnf("STACKIT: failed to refresh pricing after config update: %v", refreshErr)
+	}
+
+	return cp, nil
+}
+
+func (s *STACKIT) GetConfig() (*models.CustomPricing, error) {
+	c, err := s.Config.GetCustomPricingData()
+	if err != nil {
+		return nil, err
+	}
+	if c.Discount == "" {
+		c.Discount = "0%"
+	}
+	if c.NegotiatedDiscount == "" {
+		c.NegotiatedDiscount = "0%"
+	}
+	if c.CurrencyCode == "" {
+		c.CurrencyCode = "EUR"
+	}
+	return c, nil
+}
+
+func (s *STACKIT) GetManagementPlatform() (string, error) {
+	nodes := s.Clientset.GetAllNodes()
+	if len(nodes) > 0 {
+		n := nodes[0]
+		if _, ok := n.Labels["node.stackit.cloud/ske"]; ok {
+			return "ske", nil
+		}
+	}
+	return "", nil
+}
+
+func (s *STACKIT) PricingSourceStatus() map[string]*models.PricingSource {
+	s.DownloadPricingDataLock.RLock()
+	defer s.DownloadPricingDataLock.RUnlock()
+	return map[string]*models.PricingSource{
+		StackitPIMPricingSource: {
+			Name:      StackitPIMPricingSource,
+			Enabled:   true,
+			Available: len(s.pimFlavors) > 0,
+		},
+	}
+}

+ 192 - 0
pkg/cloud/stackit/provider_test.go

@@ -0,0 +1,192 @@
+package stackit
+
+import (
+	"testing"
+)
+
+func newTestProvider(flavors map[string]*pimFlavorPricing, storage map[string]*pimStoragePricing) *STACKIT {
+	return &STACKIT{
+		ClusterRegion: "eu01",
+		pimFlavors:    flavors,
+		pimStorage:    storage,
+	}
+}
+
+func TestNodePricing(t *testing.T) {
+	s := newTestProvider(map[string]*pimFlavorPricing{
+		"g2i.4": {HourlyCost: "0.201", VCPU: 4, RAMGB: 16.0},
+	}, nil)
+
+	key := &stackitKey{Labels: map[string]string{
+		"node.kubernetes.io/instance-type": "g2i.4",
+		"topology.kubernetes.io/zone":      "eu01-3",
+	}}
+
+	node, _, err := s.NodePricing(key)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if node.Cost != "0.201" {
+		t.Errorf("Cost = %q, want %q", node.Cost, "0.201")
+	}
+	if node.VCPU != "4" {
+		t.Errorf("VCPU = %q, want %q", node.VCPU, "4")
+	}
+	if node.RAM != "16" {
+		t.Errorf("RAM = %q, want %q", node.RAM, "16")
+	}
+	if node.RAMBytes != "17179869184" {
+		t.Errorf("RAMBytes = %q, want %q", node.RAMBytes, "17179869184")
+	}
+	if node.InstanceType != "g2i.4" {
+		t.Errorf("InstanceType = %q, want %q", node.InstanceType, "g2i.4")
+	}
+	if node.Region != "eu01" {
+		t.Errorf("Region = %q, want %q", node.Region, "eu01")
+	}
+}
+
+func TestNodePricingUnknownType(t *testing.T) {
+	s := newTestProvider(map[string]*pimFlavorPricing{}, nil)
+
+	key := &stackitKey{Labels: map[string]string{
+		"node.kubernetes.io/instance-type": "unknown.1",
+	}}
+
+	_, _, err := s.NodePricing(key)
+	if err == nil {
+		t.Error("expected error for unknown instance type")
+	}
+}
+
+func TestNodePricingGPU(t *testing.T) {
+	s := newTestProvider(map[string]*pimFlavorPricing{
+		"n1.14d.g1": {HourlyCost: "3.50", VCPU: 14, RAMGB: 56.0, GPUCount: 1, GPUType: "NVIDIA A100"},
+	}, nil)
+
+	key := &stackitKey{Labels: map[string]string{
+		"node.kubernetes.io/instance-type": "n1.14d.g1",
+	}}
+
+	node, _, err := s.NodePricing(key)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if node.GPU != "1" {
+		t.Errorf("GPU = %q, want %q", node.GPU, "1")
+	}
+	if node.GPUName != "NVIDIA A100" {
+		t.Errorf("GPUName = %q, want %q", node.GPUName, "NVIDIA A100")
+	}
+}
+
+func TestGpuPricingPerGPU(t *testing.T) {
+	s := newTestProvider(map[string]*pimFlavorPricing{
+		"n1.28d.g2": {HourlyCost: "7.00", VCPU: 28, RAMGB: 112.0, GPUCount: 2, GPUType: "NVIDIA A100"},
+	}, nil)
+
+	labels := map[string]string{
+		"node.kubernetes.io/instance-type": "n1.28d.g2",
+	}
+
+	cost, err := s.GpuPricing(labels)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if cost != "3.5" {
+		t.Errorf("per-GPU cost = %q, want %q", cost, "3.5")
+	}
+}
+
+func TestGpuPricingNoGPU(t *testing.T) {
+	s := newTestProvider(map[string]*pimFlavorPricing{
+		"g2i.4": {HourlyCost: "0.201", VCPU: 4, RAMGB: 16.0, GPUCount: 0},
+	}, nil)
+
+	labels := map[string]string{
+		"node.kubernetes.io/instance-type": "g2i.4",
+	}
+
+	cost, err := s.GpuPricing(labels)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if cost != "" {
+		t.Errorf("expected empty cost for non-GPU instance, got %q", cost)
+	}
+}
+
+func TestGpuPricingUnknownInstance(t *testing.T) {
+	s := newTestProvider(map[string]*pimFlavorPricing{}, nil)
+
+	labels := map[string]string{
+		"node.kubernetes.io/instance-type": "unknown.1",
+	}
+
+	cost, err := s.GpuPricing(labels)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if cost != "" {
+		t.Errorf("expected empty cost for unknown instance, got %q", cost)
+	}
+}
+
+func TestPVPricingExactMatch(t *testing.T) {
+	s := newTestProvider(nil, map[string]*pimStoragePricing{
+		"storage_premium_perf2": {CostPerGBHr: "0.0005"},
+		"default":               {CostPerGBHr: "0.0001"},
+	})
+
+	pvk := &stackitPVKey{StorageClassName: "storage_premium_perf2"}
+	pv, err := s.PVPricing(pvk)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if pv.Cost != "0.0005" {
+		t.Errorf("Cost = %q, want %q", pv.Cost, "0.0005")
+	}
+}
+
+func TestPVPricingDefaultFallback(t *testing.T) {
+	s := newTestProvider(nil, map[string]*pimStoragePricing{
+		"default": {CostPerGBHr: "0.0001"},
+	})
+
+	pvk := &stackitPVKey{StorageClassName: "unknown-class"}
+	pv, err := s.PVPricing(pvk)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if pv.Cost != "0.0001" {
+		t.Errorf("Cost = %q, want default %q", pv.Cost, "0.0001")
+	}
+	if pv.Class != "unknown-class" {
+		t.Errorf("Class = %q, want %q", pv.Class, "unknown-class")
+	}
+}
+
+func TestPVPricingNoPricing(t *testing.T) {
+	s := newTestProvider(nil, nil)
+
+	pvk := &stackitPVKey{StorageClassName: "some-class"}
+	pv, err := s.PVPricing(pvk)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if pv.Cost != "" {
+		t.Errorf("expected empty cost when no pricing data, got %q", pv.Cost)
+	}
+}
+
+func TestStackitKeyFeatures(t *testing.T) {
+	key := &stackitKey{Labels: map[string]string{
+		"node.kubernetes.io/instance-type": "g2i.4",
+		"topology.kubernetes.io/zone":      "eu01-3",
+	}}
+
+	features := key.Features()
+	if features != "eu01-3,g2i.4" {
+		t.Errorf("Features() = %q, want %q", features, "eu01-3,g2i.4")
+	}
+}

+ 7 - 1
pkg/cloudcost/ingestor.go

@@ -188,12 +188,18 @@ func (ing *ingestor) Stop() {
 
 // Status returns an IngestorStatus that describes the current state of the ingestor
 func (ing *ingestor) Status() IngestorStatus {
+	// Read coverage under the lock; the build loop reassigns it under
+	// coverageLock, so an unlocked read here is a data race.
+	ing.coverageLock.Lock()
+	coverage := ing.coverage
+	ing.coverageLock.Unlock()
+
 	return IngestorStatus{
 		Created:          ing.creationTime,
 		LastRun:          ing.lastRun,
 		NextRun:          ing.lastRun.Add(ing.config.RefreshRate).UTC(),
 		Runs:             ing.runs,
-		Coverage:         ing.coverage,
+		Coverage:         coverage,
 		ConnectionStatus: ing.integration.GetStatus(),
 	}
 }

+ 47 - 0
pkg/cloudcost/ingestor_test.go

@@ -0,0 +1,47 @@
+package cloudcost
+
+import (
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/opencost"
+	"github.com/opencost/opencost/pkg/cloud"
+)
+
+type fakeIntegration struct{}
+
+func (fakeIntegration) GetCloudCost(time.Time, time.Time) (*opencost.CloudCostSetRange, error) {
+	return nil, nil
+}
+func (fakeIntegration) GetStatus() cloud.ConnectionStatus     { return cloud.InitialStatus }
+func (fakeIntegration) RefreshStatus() cloud.ConnectionStatus { return cloud.InitialStatus }
+
+// TestIngestor_Status_ConcurrentWithCoverageWrite guards against a data race on
+// the coverage window: Status() read it without holding coverageLock while the
+// build loop reassigns it under the lock. Run with -race to detect it.
+func TestIngestor_Status_ConcurrentWithCoverageWrite(t *testing.T) {
+	now := time.Now().UTC()
+	ing := &ingestor{
+		integration: fakeIntegration{},
+		coverage:    opencost.NewClosedWindow(now, now.Add(time.Hour)),
+	}
+
+	window := opencost.NewClosedWindow(now, now.Add(2*time.Hour))
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+	go func() {
+		defer wg.Done()
+		for i := 0; i < 2000; i++ {
+			ing.expandCoverage(window)
+		}
+	}()
+	go func() {
+		defer wg.Done()
+		for i := 0; i < 2000; i++ {
+			_ = ing.Status()
+		}
+	}()
+	wg.Wait()
+}

+ 5 - 0
pkg/cloudcost/integration.go

@@ -10,6 +10,7 @@ import (
 	"github.com/opencost/opencost/pkg/cloud/azure"
 	"github.com/opencost/opencost/pkg/cloud/gcp"
 	"github.com/opencost/opencost/pkg/cloud/oracle"
+	"github.com/opencost/opencost/pkg/cloud/stackit"
 )
 
 // CloudCostIntegration is an interface for retrieving daily granularity CloudCost data for a given range
@@ -105,6 +106,10 @@ func GetIntegrationFromConfig(kc cloud.KeyedConfig) CloudCostIntegration {
 		return &oracle.UsageApiIntegration{
 			UsageApiConfiguration: *keyedConfig,
 		}
+	case *stackit.CostConfiguration:
+		return &stackit.CostIntegration{
+			CostConfiguration: *keyedConfig,
+		}
 	default:
 		return nil
 	}

+ 20 - 3
pkg/cmd/costmodel/costmodel.go

@@ -11,6 +11,7 @@ import (
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/opencost/opencost/core/pkg/util/apiutil"
+	"github.com/opencost/opencost/core/pkg/util/timeutil"
 	"github.com/opencost/opencost/pkg/cloudcost"
 	"github.com/opencost/opencost/pkg/customcost"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -196,14 +197,16 @@ func StartMCPServer(ctx context.Context, accesses *costmodel.Accesses, cloudCost
 
 	// Define tool handlers
 	handleAllocationCosts := func(ctx context.Context, req *mcp_sdk.CallToolRequest, args AllocationArgs) (*mcp_sdk.CallToolResult, interface{}, error) {
-		// Parse step duration if provided
 		var step time.Duration
-		var err error
 		if args.Step != "" {
-			step, err = time.ParseDuration(args.Step)
+			var err error
+			step, err = timeutil.ParseDuration(args.Step)
 			if err != nil {
 				return nil, nil, fmt.Errorf("invalid step duration '%s': %w", args.Step, err)
 			}
+			if step <= 0 {
+				return nil, nil, fmt.Errorf("invalid step duration '%s': must be > 0", args.Step)
+			}
 		}
 
 		queryRequest := &opencost_mcp.OpenCostQueryRequest{
@@ -283,10 +286,23 @@ func StartMCPServer(ctx context.Context, accesses *costmodel.Accesses, cloudCost
 	}
 
 	handleEfficiency := func(ctx context.Context, req *mcp_sdk.CallToolRequest, args EfficiencyArgs) (*mcp_sdk.CallToolResult, interface{}, error) {
+		var step time.Duration
+		if args.Step != "" {
+			var err error
+			step, err = timeutil.ParseDuration(args.Step)
+			if err != nil {
+				return nil, nil, fmt.Errorf("invalid step duration '%s': %w", args.Step, err)
+			}
+			if step <= 0 {
+				return nil, nil, fmt.Errorf("invalid step duration '%s': must be > 0", args.Step)
+			}
+		}
+
 		queryRequest := &opencost_mcp.OpenCostQueryRequest{
 			QueryType: opencost_mcp.EfficiencyQueryType,
 			Window:    args.Window,
 			EfficiencyParams: &opencost_mcp.EfficiencyQuery{
+				Step:                       step,
 				Aggregate:                  args.Aggregate,
 				Filter:                     args.Filter,
 				EfficiencyBufferMultiplier: args.BufferMultiplier,
@@ -413,4 +429,5 @@ type EfficiencyArgs struct {
 	Aggregate        string   `json:"aggregate,omitempty"`         // Aggregation level (e.g., "pod", "namespace", "controller")
 	Filter           string   `json:"filter,omitempty"`            // Filter expression (same as allocation filters)
 	BufferMultiplier *float64 `json:"buffer_multiplier,omitempty"` // Buffer multiplier for recommendations (default: 1.2 for 20% headroom, e.g., 1.4 for 40%)
+	Step             string   `json:"step,omitempty"`              // Query step size (e.g., "1h", "6h"); smaller steps reduce peak memory by batching large windows, but may increase query time/requests
 }

+ 1 - 0
pkg/env/costmodel.go

@@ -39,6 +39,7 @@ const (
 	AzureRegionInfoEnvVar     = "AZURE_REGION_INFO"
 
 	DigitalOceanAccessTokenEnvVar = "DIGITALOCEAN_ACCESS_TOKEN"
+
 	// Azure rate card filter environment variables
 
 	// Currently being used for OCI and DigitalOcean

+ 47 - 7
pkg/mcp/server.go

@@ -107,9 +107,10 @@ type CloudCostQuery struct {
 
 // EfficiencyQuery contains the parameters for an efficiency query.
 type EfficiencyQuery struct {
-	Aggregate                  string   `json:"aggregate,omitempty"`                  // Aggregation properties (e.g., "pod", "namespace", "controller")
-	Filter                     string   `json:"filter,omitempty"`                     // Filter expression for allocations (same as AllocationQuery)
-	EfficiencyBufferMultiplier *float64 `json:"efficiencyBufferMultiplier,omitempty"` // Buffer multiplier for recommendations (default: 1.2 for 20% headroom)
+	Step                       time.Duration `json:"step,omitempty"`                       // Query step size; controls peak memory by batching large windows (default: auto-scaled based on window)
+	Aggregate                  string        `json:"aggregate,omitempty"`                  // Aggregation properties (e.g., "pod", "namespace", "controller")
+	Filter                     string        `json:"filter,omitempty"`                     // Filter expression for allocations (same as AllocationQuery)
+	EfficiencyBufferMultiplier *float64      `json:"efficiencyBufferMultiplier,omitempty"` // Buffer multiplier for recommendations (default: 1.2 for 20% headroom)
 }
 
 // AllocationResponse represents the allocation data returned to the AI agent.
@@ -1016,6 +1017,23 @@ func transformCloudCostSetRange(ccsr *opencost.CloudCostSetRange) *CloudCostResp
 	}
 }
 
+// defaultEfficiencyStep returns a step duration that keeps peak memory
+// bounded for large query windows. When the caller does not specify a step,
+// this provides a safe default that avoids loading the entire window into
+// memory at once.
+func defaultEfficiencyStep(windowDuration time.Duration) time.Duration {
+	switch {
+	case windowDuration >= 30*24*time.Hour:
+		return 24 * time.Hour
+	case windowDuration >= 7*24*time.Hour:
+		return 6 * time.Hour
+	case windowDuration >= 24*time.Hour:
+		return time.Hour
+	default:
+		return windowDuration
+	}
+}
+
 // QueryEfficiency queries allocation data and computes efficiency metrics with recommendations.
 func (s *MCPServer) QueryEfficiency(query *OpenCostQueryRequest) (*EfficiencyResponse, error) {
 	// 1. Parse Window
@@ -1060,9 +1078,31 @@ func (s *MCPServer) QueryEfficiency(query *OpenCostQueryRequest) (*EfficiencyRes
 		filterString = ""
 	}
 
-	// 4. Query allocations with the specified parameters
-	// Use the entire window as step to get aggregated data
-	step := window.Duration()
+	// 4. Determine query step size.
+	// A smaller step reduces peak memory by breaking large windows into batches.
+	// Results are accumulated so the output is functionally equivalent regardless
+	// of step, though minor floating-point differences are possible because
+	// per-step cost calculations (which use max(request, usage)) are summed
+	// rather than computed in a single pass.
+	var step time.Duration
+	if query.EfficiencyParams != nil && query.EfficiencyParams.Step > 0 {
+		step = query.EfficiencyParams.Step
+	} else {
+		step = defaultEfficiencyStep(window.Duration())
+	}
+
+	if step > window.Duration() {
+		step = window.Duration()
+	}
+	if step <= 0 {
+		return nil, fmt.Errorf("invalid query: window has zero or negative duration")
+	}
+
+	accumulateBy := opencost.AccumulateOptionNone
+	if step < window.Duration() {
+		accumulateBy = opencost.AccumulateOptionAll
+	}
+
 	asr, err := s.costModel.QueryAllocation(
 		window,
 		step,
@@ -1072,7 +1112,7 @@ func (s *MCPServer) QueryEfficiency(query *OpenCostQueryRequest) (*EfficiencyRes
 		false, // includeProportionalAssetResourceCosts
 		false, // includeAggregatedMetadata
 		false, // sharedLoadBalancer
-		opencost.AccumulateOptionNone,
+		accumulateBy,
 		false, // shareIdle
 		filterString,
 	)

+ 44 - 0
pkg/mcp/server_test.go

@@ -920,11 +920,13 @@ func TestCloudCostQuery_NewFields(t *testing.T) {
 func TestEfficiencyQueryStruct(t *testing.T) {
 	bufferMultiplier := 1.4
 	query := EfficiencyQuery{
+		Step:                       5 * time.Minute,
 		Aggregate:                  "pod",
 		Filter:                     "namespace:production",
 		EfficiencyBufferMultiplier: &bufferMultiplier,
 	}
 
+	assert.Equal(t, 5*time.Minute, query.Step)
 	assert.Equal(t, "pod", query.Aggregate)
 	assert.Equal(t, "namespace:production", query.Filter)
 	assert.NotNil(t, query.EfficiencyBufferMultiplier)
@@ -934,6 +936,7 @@ func TestEfficiencyQueryStruct(t *testing.T) {
 func TestEfficiencyQueryDefaultValues(t *testing.T) {
 	query := EfficiencyQuery{}
 
+	assert.Equal(t, time.Duration(0), query.Step)
 	assert.Empty(t, query.Aggregate)
 	assert.Empty(t, query.Filter)
 	assert.Nil(t, query.EfficiencyBufferMultiplier)
@@ -1365,6 +1368,47 @@ func TestEfficiencyQueryType(t *testing.T) {
 	assert.Equal(t, QueryType("efficiency"), EfficiencyQueryType)
 }
 
+func TestDefaultEfficiencyStep(t *testing.T) {
+	tests := []struct {
+		name     string
+		window   time.Duration
+		expected time.Duration
+	}{
+		{"30d window uses 1d step", 30 * 24 * time.Hour, 24 * time.Hour},
+		{"90d window uses 1d step", 90 * 24 * time.Hour, 24 * time.Hour},
+		{"7d window uses 6h step", 7 * 24 * time.Hour, 6 * time.Hour},
+		{"14d window uses 6h step", 14 * 24 * time.Hour, 6 * time.Hour},
+		{"1d window uses 1h step", 24 * time.Hour, time.Hour},
+		{"3d window uses 1h step", 3 * 24 * time.Hour, time.Hour},
+		{"12h window returns full window", 12 * time.Hour, 12 * time.Hour},
+		{"1h window returns full window", time.Hour, time.Hour},
+		{"30m window returns full window", 30 * time.Minute, 30 * time.Minute},
+		{"exactly at 7d boundary uses 6h step", 7 * 24 * time.Hour, 6 * time.Hour},
+		{"just under 7d uses 1h step", 7*24*time.Hour - time.Minute, time.Hour},
+		{"just under 1d uses full window", 24*time.Hour - time.Minute, 24*time.Hour - time.Minute},
+		{"zero window returns zero", 0, 0},
+		{"negative window returns negative", -time.Hour, -time.Hour},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, defaultEfficiencyStep(tt.window))
+		})
+	}
+}
+
+func TestEfficiencyQueryRequest_StepField(t *testing.T) {
+	req := &OpenCostQueryRequest{
+		QueryType: EfficiencyQueryType,
+		Window:    "7d",
+		EfficiencyParams: &EfficiencyQuery{
+			Step:      6 * time.Hour,
+			Aggregate: "pod",
+		},
+	}
+	assert.Equal(t, 6*time.Hour, req.EfficiencyParams.Step)
+	assert.Equal(t, "pod", req.EfficiencyParams.Aggregate)
+}
+
 // TestTransformCloudCostSetRange_NilPointerHandling verifies that nil pointer dereferences
 // are prevented in transformCloudCostSetRange for issue #3502
 func TestTransformCloudCostSetRange_NilPointerHandling(t *testing.T) {

Some files were not shown because too many files changed in this diff