@@ -2,11 +2,140 @@ package gcp
22
33import (
44 "context"
5+ "errors"
56 "fmt"
7+ "math"
8+ "path"
9+ "strings"
10+
11+ "cloud.google.com/go/compute/apiv1/computepb"
12+ "github.com/samber/lo"
13+ "google.golang.org/api/iterator"
614
715 "github.com/castai/kvisor/pkg/cloudprovider/types"
816)
917
1018func (p * Provider ) GetStorageState (ctx context.Context , instanceIds ... string ) (* types.StorageState , error ) {
11- return nil , fmt .Errorf ("GetStorageState not yet implemented for GCP" )
19+ p .log .Debug ("refreshing storage state" )
20+
21+ state := & types.StorageState {
22+ Domain : "googleapis.com" ,
23+ Provider : types .TypeGCP ,
24+ }
25+
26+ instanceVolumes , err := p .fetchInstanceVolumes (ctx , instanceIds ... )
27+ if err != nil {
28+ return nil , fmt .Errorf ("fetching volumes: %w" , err )
29+ }
30+ state .InstanceVolumes = instanceVolumes
31+
32+ return state , nil
33+ }
34+
35+ // fetchInstanceVolumes retrieves instance volumes from https://docs.cloud.google.com/compute/docs/reference/rest/v1/disks/aggregatedList
36+ func (p * Provider ) fetchInstanceVolumes (ctx context.Context , instanceIds ... string ) (map [string ][]types.Volume , error ) {
37+ instanceVolumes := make (map [string ][]types.Volume , len (instanceIds ))
38+
39+ if len (instanceIds ) == 0 {
40+ return instanceVolumes , nil
41+ }
42+
43+ instanceUrlsMap := make (map [string ]string , len (instanceIds ))
44+ for _ , instanceId := range instanceIds {
45+ url := buildInstanceUrlFromId (instanceId )
46+ if url == "" {
47+ p .log .WithField ("instance_id" , instanceId ).Warn ("could not build instance url" )
48+ continue
49+ }
50+ instanceUrlsMap [url ] = instanceId
51+ }
52+
53+ filter := buildDisksUsedByInstanceFilter (lo .Keys (instanceUrlsMap ))
54+
55+ req := & computepb.AggregatedListDisksRequest {
56+ Project : p .cfg .GCPProjectID ,
57+ Filter : & filter ,
58+ }
59+
60+ it := p .disksClient .AggregatedList (ctx , req )
61+ for result , err := range it .All () {
62+ if errors .Is (err , iterator .Done ) {
63+ break
64+ }
65+
66+ if err != nil {
67+ return instanceVolumes , fmt .Errorf ("listing disks: %w" , err )
68+ }
69+
70+ for _ , disk := range result .Value .Disks {
71+ if disk .GetName () == "" {
72+ p .log .Error ("disk missing name, skipping" )
73+ continue
74+ }
75+
76+ for _ , instanceUrl := range disk .Users {
77+ instanceId , ok := instanceUrlsMap [instanceUrl ]
78+ if ! ok {
79+ continue
80+ }
81+
82+ volume := types.Volume {
83+ VolumeID : disk .GetName (),
84+ VolumeState : strings .ToLower (disk .GetStatus ()),
85+ Encrypted : true , // GCP disks are encrypted by default
86+ }
87+
88+ if disk .GetType () != "" {
89+ volume .VolumeType = path .Base (disk .GetType ())
90+ }
91+
92+ if disk .GetZone () != "" {
93+ volume .Zone = path .Base (disk .GetZone ())
94+ }
95+
96+ if disk .GetSizeGb () > 0 {
97+ // Size is in GB, convert to bytes
98+ volume .SizeBytes = disk .GetSizeGb () * 1024 * 1024 * 1024
99+ }
100+
101+ if disk .GetProvisionedIops () > 0 {
102+ volume .IOPS = safeInt64ToInt32 (disk .GetProvisionedIops ())
103+ }
104+
105+ if disk .GetProvisionedThroughput () > 0 {
106+ // Throughput is in MB/s, convert to bytes/s
107+ volume .ThroughputBytes = safeInt64ToInt32 (disk .GetProvisionedThroughput () * 1024 * 1024 )
108+ }
109+
110+ instanceVolumes [instanceId ] = append (instanceVolumes [instanceId ], volume )
111+ }
112+ }
113+ }
114+
115+ return instanceVolumes , nil
116+ }
117+
118+ // buildInstanceUrlFromId converts an instance ID (project/zone/instance-name) to a full GCP instance URL
119+ func buildInstanceUrlFromId (instanceId string ) string {
120+ parts := strings .Split (instanceId , "/" )
121+ if len (parts ) != 3 {
122+ return ""
123+ }
124+ return fmt .Sprintf ("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/instances/%s" , parts [0 ], parts [1 ], parts [2 ])
125+ }
126+
127+ // buildDisksUsedByInstanceFilter builds a GCP API filter for disks attached to specific instances
128+ func buildDisksUsedByInstanceFilter (instanceUrls []string ) string {
129+ conditions := make ([]string , len (instanceUrls ))
130+ for i , url := range instanceUrls {
131+ conditions [i ] = fmt .Sprintf (`(users:%q)` , url )
132+ }
133+ return strings .Join (conditions , " OR " )
134+ }
135+
136+ func safeInt64ToInt32 (val int64 ) int32 {
137+ if val > math .MaxInt32 {
138+ return math .MaxInt32
139+ }
140+ return int32 (val ) // nolint:gosec
12141}
0 commit comments