Implemented tools for interacting with GCS buckets.

Added functionality for listing all modules in a GCS bucket directory and downloading a module from a GCS bucket directory.

Change-Id: Iedacb01b328c51bceb21df50e86fbf88d99730fc
Reviewed-on: https://cos-review.googlesource.com/c/cos/cos-extensions/+/75253
Reviewed-by: He Gao <hegao@google.com>
Tested-by: He Gao <hegao@google.com>
Reviewed-by: Miri Amarilio <mirilio@google.com>
diff --git a/go.mod b/go.mod
index 193e0a5..88d1e9f 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,47 @@
 require github.com/golang/glog v1.2.1
 
 require (
+	cloud.google.com/go v0.114.0 // indirect
+	cloud.google.com/go/auth v0.5.1 // indirect
+	cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
+	cloud.google.com/go/compute/metadata v0.3.0 // indirect
+	cloud.google.com/go/iam v1.1.8 // indirect
+	cloud.google.com/go/pubsub v1.38.0 // indirect
+	cloud.google.com/go/storage v1.42.0 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/fsouza/fake-gcs-server v1.49.2 // indirect
+	github.com/go-logr/logr v1.4.1 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
+	github.com/google/renameio/v2 v2.0.0 // indirect
+	github.com/google/s2a-go v0.1.7 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
+	github.com/googleapis/gax-go/v2 v2.12.4 // indirect
+	github.com/gorilla/handlers v1.5.2 // indirect
+	github.com/gorilla/mux v1.8.1 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/pkg/xattr v0.4.9 // indirect
 	github.com/spf13/cobra v1.8.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+	go.opentelemetry.io/otel v1.24.0 // indirect
+	go.opentelemetry.io/otel/metric v1.24.0 // indirect
+	go.opentelemetry.io/otel/trace v1.24.0 // indirect
+	golang.org/x/crypto v0.23.0 // indirect
+	golang.org/x/net v0.25.0 // indirect
+	golang.org/x/oauth2 v0.21.0 // indirect
+	golang.org/x/sync v0.7.0 // indirect
+	golang.org/x/sys v0.20.0 // indirect
+	golang.org/x/text v0.15.0 // indirect
+	golang.org/x/time v0.5.0 // indirect
+	google.golang.org/api v0.183.0 // indirect
+	google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
+	google.golang.org/grpc v1.64.0 // indirect
+	google.golang.org/protobuf v1.34.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 5092d83..e536941 100644
--- a/go.sum
+++ b/go.sum
@@ -1,14 +1,184 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY=
+cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E=
+cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw=
+cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s=
+cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
+cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
+cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
+cloud.google.com/go/pubsub v1.38.0 h1:J1OT7h51ifATIedjqk/uBNPh+1hkvUaH4VKbz4UuAsc=
+cloud.google.com/go/pubsub v1.38.0/go.mod h1:IPMJSWSus/cu57UyR01Jqa/bNOQA+XnPF6Z4dKW4fAA=
+cloud.google.com/go/storage v1.42.0 h1:4QtGpplCVt1wz6g5o1ifXd656P5z+yNgzdw1tVfp0cU=
+cloud.google.com/go/storage v1.42.0/go.mod h1:HjMXRFq65pGKFn6hxj6x3HCyR41uSB72Z0SO/Vn6JFQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsouza/fake-gcs-server v1.49.2 h1:fukDqzEQM50QkA0jAbl6cLqeDu3maQjwZBuys759TR4=
+github.com/fsouza/fake-gcs-server v1.49.2/go.mod h1:17SYzJEXRcaAA5ATwwvgBkSIqIy7r1icnGM0y/y4foY=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
 github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
+github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
+github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
+github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
+github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
+github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
+github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
+github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
 github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
+golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE=
+google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE=
+google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ=
+google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
+google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
+google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/tools/gcs/gcs.go b/tools/gcs/gcs.go
new file mode 100644
index 0000000..5498cc5
--- /dev/null
+++ b/tools/gcs/gcs.go
@@ -0,0 +1,143 @@
+// Package gcs provides tools for interacting with GCS buckets and files.
+package gcs
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"cloud.google.com/go/storage"
+	"google.golang.org/api/iterator"
+)
+
+var gcsClient = storage.NewClient
+
+// GCSConfig stores the gcs bucket handler and a module gcs directory path.
+type GCSConfig struct {
+	bucket *storage.BucketHandle
+	path   string
+}
+
+// Init initializes the gcs bucket and module directory path from a
+// gsutil uri.
+func (c *GCSConfig) Init(ctx context.Context, gsUri string) error {
+	client, err := gcsClient(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to create new GCS client: %v", err)
+	}
+	bucketName, dir, err := getBucketNameAndDir(gsUri)
+	if err != nil {
+		return fmt.Errorf("failed to retrieve bucket name and module path: %v", err)
+	}
+
+	// Creating bucket handle
+	bkt := client.Bucket(bucketName)
+
+	// Checking if bucket exists
+	_, err = bkt.Attrs(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to validate bucket(%s): %v", bucketName, err)
+	}
+
+	prefix := fmt.Sprintf("%s/", dir)
+	// Checking if directory exists and non-empty
+	it := bkt.Objects(ctx, &storage.Query{
+		Prefix: prefix,
+	})
+	if _, err = it.Next(); err != nil {
+		if err == iterator.Done {
+			return fmt.Errorf("failed to validate path(%s): empty or non-existent path", dir)
+		}
+		return fmt.Errorf("failed to validate path(%s): %v", dir, err)
+	}
+
+	c.bucket = bkt
+	c.path = dir
+	return nil
+}
+
+// ListModules returns a list of all the modules in a GCS directory.
+// Identifies modules as files with .ko extension.
+func (c *GCSConfig) ListModules(ctx context.Context) ([]string, error) {
+	prefix := fmt.Sprintf("%s/", c.path)
+
+	// Querying for files in gcs directory.
+	it := c.bucket.Objects(ctx, &storage.Query{
+		Prefix:    prefix,
+		Delimiter: "/"},
+	)
+
+	var modules []string
+	for {
+		attrs, err := it.Next()
+
+		if err == iterator.Done {
+			break
+		}
+		if err != nil {
+			return nil, fmt.Errorf("failed to retrieve modules from the bucket: %v", err)
+		}
+		if filepath.Ext(attrs.Name) == ".ko" {
+			moduleName, _ := strings.CutPrefix(attrs.Name, prefix)
+			modules = append(modules, moduleName)
+		}
+	}
+	return modules, nil
+}
+
+// DownloadKernelModule copies a module file from a GCS directory to a local directory.
+// It returns the local file path of the downloaded module.
+// If a file exists in that directory with the same name as the module, an error
+// is returned to the user.
+func (c *GCSConfig) DownloadKernelModule(ctx context.Context, moduleName, localDir string) (string, error) {
+	src := filepath.Join(c.path, moduleName)
+	dst := filepath.Join(localDir, moduleName)
+
+	// Checking if there exists a file with the same name as module in directory
+	if _, err := os.Stat(dst); !os.IsNotExist(err) {
+		return "", fmt.Errorf("failed to download %s to %s. File already exists", moduleName, c.path)
+	}
+
+	moduleObj := c.bucket.Object(src)
+	rc, err := moduleObj.NewReader(ctx)
+	if err != nil {
+		return "", fmt.Errorf("failed to read from module %s: %v", src, err)
+	}
+	defer rc.Close()
+
+	// Creating a file with the same name as the module, in the local directory
+	f, err := os.Create(dst)
+	if err != nil {
+		return "", fmt.Errorf("failed to create file %s: %v", dst, err)
+	}
+	defer f.Close()
+
+	if _, err := io.Copy(f, rc); err != nil {
+		return "", fmt.Errorf("failed to copy %s from gcs bucket (%s): %v", src, c.bucketName(), err)
+	}
+	return dst, nil
+}
+
+func getBucketNameAndDir(gcsPath string) (string, string, error) {
+	filePath, isValid := strings.CutPrefix(gcsPath, "gs://")
+	if !isValid {
+		expected := "gs://<bucket-name>/<path-to-module-directory>"
+		return "", "", fmt.Errorf("invalid syntax for gcs path. Expected gsutil uri format (%s) got: %v", expected, gcsPath)
+	}
+	bktName, path, isValid := strings.Cut(filePath, "/")
+	if !isValid || strings.TrimSpace(path) == "" {
+		return "", "", fmt.Errorf("file path '%s' should have both bucket name and path to module directory", filePath)
+	}
+	path, _ = strings.CutSuffix(path, "/")
+	return bktName, path, nil
+}
+
+func (c *GCSConfig) bucketName() string {
+	if c.bucket == nil {
+		return ""
+	}
+	return c.bucket.BucketName()
+}
diff --git a/tools/gcs/gcs_test.go b/tools/gcs/gcs_test.go
new file mode 100644
index 0000000..1ba7533
--- /dev/null
+++ b/tools/gcs/gcs_test.go
@@ -0,0 +1,321 @@
+package gcs
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"reflect"
+	"strings"
+	"testing"
+
+	"cloud.google.com/go/storage"
+	"google.golang.org/api/option"
+
+	"github.com/fsouza/fake-gcs-server/fakestorage"
+)
+
+func TestGetBucketAndDir(t *testing.T) {
+	tests := []struct {
+		desc       string
+		gsUri      string
+		wantErr    bool
+		wantBucket string
+		wantDir    string
+	}{
+		{
+			desc:    "Invalid path syntax",
+			gsUri:   "https://storage.mtls.cloud.google.com/gcs/my-bucket/folder",
+			wantErr: true,
+		},
+		{
+			desc:    "Missing gcs path",
+			gsUri:   "",
+			wantErr: true,
+		},
+		{
+			desc:    "Missing bucket name",
+			gsUri:   "gs://",
+			wantErr: true,
+		},
+		{
+			desc:    "Missing folder ending with /",
+			gsUri:   "gs://bucket-name/",
+			wantErr: true,
+		},
+		{
+			desc:    "Missing folder",
+			gsUri:   "gs://bucket-name",
+			wantErr: true,
+		},
+		{
+			desc:       "Valid gcs path",
+			gsUri:      "gs://my-bucket/path/to/modules",
+			wantBucket: "my-bucket",
+			wantDir:    "path/to/modules",
+		},
+		{
+			desc:       "Valid gcs path ending with /",
+			gsUri:      "gs://my-bucket/path/to/modules/",
+			wantBucket: "my-bucket",
+			wantDir:    "path/to/modules",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			bktName, dir, err := getBucketNameAndDir(test.gsUri)
+			if gotErr := err != nil; gotErr != test.wantErr {
+				t.Errorf("TestGetBucketAndDir(%s): Error: %s\n expected error: %t\n got error: %t", test.desc, err, test.wantErr, gotErr)
+			}
+			if bktName != test.wantBucket {
+				t.Errorf("TestGetBucketAndDir(%s): expected bucket:%s\t got bucket:%s", test.desc, test.wantBucket, bktName)
+			}
+			if dir != test.wantDir {
+				t.Errorf("TestGetBucketAndDir(%s): expected directory:%s\t got directory:%s", test.desc, test.wantDir, dir)
+			}
+		})
+	}
+}
+
+func TestInit(t *testing.T) {
+	ctx := context.Background()
+	tests := []struct {
+		desc       string
+		gsUri      string
+		server     *fakestorage.Server
+		wantErr    bool
+		wantBucket string
+		wantPath   string
+	}{
+		{
+			desc:  "Bucket does not exist",
+			gsUri: "gs://unknown/path/to/module",
+			server: fakestorage.NewServer(
+				[]fakestorage.Object{
+					{
+						ObjectAttrs: fakestorage.ObjectAttrs{
+							BucketName: "my-bucket",
+							Name:       "path/to/module/module1.ko",
+						},
+					},
+				}),
+			wantErr: true,
+		},
+		{
+			desc:  "Successfully sets up the gcs config",
+			gsUri: "gs://my-bucket/path/to/module",
+			server: fakestorage.NewServer(
+				[]fakestorage.Object{
+					{
+						ObjectAttrs: fakestorage.ObjectAttrs{
+							BucketName: "my-bucket",
+							Name:       "path/to/module/module1.ko",
+						},
+					},
+				}),
+			wantBucket: "my-bucket",
+			wantPath:   "path/to/module",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			defer test.server.Stop()
+			gcsClient = func(_ context.Context, _ ...option.ClientOption) (*storage.Client, error) {
+				return test.server.Client(), nil
+			}
+			var cfg GCSConfig
+			err := cfg.Init(ctx, test.gsUri)
+			if gotErr := err != nil; gotErr != test.wantErr {
+				t.Errorf("TestInit(%s): Error: %v\n expected error: %t\n got error: %t", test.desc, err, test.wantErr, gotErr)
+			}
+			bucket := cfg.bucketName()
+			if bucket != test.wantBucket {
+				t.Errorf("TestListModules(%s): expected bucket:%s\t got bucket:%s", test.desc, test.wantBucket, bucket)
+			}
+			if cfg.path != test.wantPath {
+				t.Errorf("TestListModules(%s): expected path:%s\t got path:%s", test.desc, test.wantPath, cfg.path)
+			}
+
+		})
+	}
+}
+
+func TestInitNonExistentPath(t *testing.T) {
+	ctx := context.Background()
+	server := fakestorage.NewServer([]fakestorage.Object{})
+	server.CreateBucketWithOpts(fakestorage.CreateBucketOpts{Name: "my-bucket"})
+	defer server.Stop()
+	gcsClient = func(_ context.Context, _ ...option.ClientOption) (*storage.Client, error) {
+		return server.Client(), nil
+	}
+	var cfg GCSConfig
+	err := cfg.Init(ctx, "gs://my-bucket/path/to/module")
+	if gotErr := err != nil; gotErr != true {
+		t.Errorf("TestInitNonExistentPath: Error: %v\n expected error: true\n got error: %t", err, gotErr)
+	}
+}
+
+func TestListModules(t *testing.T) {
+	ctx := context.Background()
+	tests := []struct {
+		desc        string
+		gcsPath     string
+		server      *fakestorage.Server
+		wantErr     bool
+		wantModules []string
+	}{
+		{
+			desc:    "Successfully retrieves modules",
+			gcsPath: "path/to/module",
+			server: fakestorage.NewServer(
+				[]fakestorage.Object{
+					{
+						ObjectAttrs: fakestorage.ObjectAttrs{
+							BucketName: "my-bucket",
+							Name:       "path/to/module/module1.ko",
+						},
+					},
+					{
+						ObjectAttrs: fakestorage.ObjectAttrs{
+							BucketName: "my-bucket",
+							Name:       "path/to/module/module2.ko",
+						},
+					},
+					{
+						ObjectAttrs: fakestorage.ObjectAttrs{
+							BucketName: "my-bucket",
+							Name:       "path/to/module/text.txt",
+						},
+					},
+				}),
+			wantModules: []string{"module1.ko", "module2.ko"},
+		}, {
+			desc:    "Does not return modules in subdirectory",
+			gcsPath: "path/to/module",
+			server: fakestorage.NewServer(
+				[]fakestorage.Object{
+					{
+						ObjectAttrs: fakestorage.ObjectAttrs{
+							BucketName: "my-bucket",
+							Name:       "path/to/module/module1.ko",
+						},
+					},
+					{
+						ObjectAttrs: fakestorage.ObjectAttrs{
+							BucketName: "my-bucket",
+							Name:       "path/to/module/subdirectory/module2.ko",
+						},
+					},
+				}),
+			wantModules: []string{"module1.ko"},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			defer test.server.Stop()
+			cfg := &GCSConfig{bucket: test.server.Client().Bucket("my-bucket"), path: test.gcsPath}
+			modules, err := cfg.ListModules(ctx)
+			if gotErr := err != nil; gotErr != test.wantErr {
+				t.Errorf("TestListModules(%s): Error: %s\n expected error: %t\n got error: %t", test.desc, err, test.wantErr, gotErr)
+			}
+			if !reflect.DeepEqual(test.wantModules, modules) {
+				t.Errorf("TestListModules(%s): expected modules: %s\t got modules: %s", test.desc, test.wantModules, modules)
+			}
+		})
+	}
+}
+
+func TestDownloadKernelModuleSuccessful(t *testing.T) {
+	ctx := context.Background()
+	tmpDir, err := os.MkdirTemp("", "test-gcs")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	gcsPath := "path/to/module"
+	server := fakestorage.NewServer(
+		[]fakestorage.Object{
+			{ObjectAttrs: fakestorage.ObjectAttrs{
+				BucketName: "my-bucket", Name: "path/to/module/module.ko",
+			},
+				Content: []byte("content"),
+			},
+		})
+	module := "module.ko"
+	wantContent := "content"
+	defer server.Stop()
+	cfg := &GCSConfig{bucket: server.Client().Bucket("my-bucket"), path: gcsPath}
+	modulePath, err := cfg.DownloadKernelModule(ctx, module, tmpDir)
+	if err != nil {
+		t.Errorf("TestDownloadKernelModuleSuccessful: failed to download module: %v", err)
+	}
+	wantPath := filepath.Join(tmpDir, module)
+	content, err := os.ReadFile(wantPath)
+	if err != nil {
+		t.Errorf("TestDownloadKernelModuleSuccessful: failed to read file: %v", err)
+	}
+	if string(content) != wantContent {
+		t.Errorf("TestDownloadKernelModuleSuccessful: expected content: %s\t got content: %s", wantContent, content)
+	}
+	if modulePath != wantPath {
+		t.Errorf("TestDownloadKernelModuleSuccessful: expected path: %s\t got path: %s", wantPath, modulePath)
+	}
+}
+
+func TestDownloadKernelModuleFileAlreadyExists(t *testing.T) {
+	ctx := context.Background()
+	module := "module.ko"
+	tmpDir, err := os.MkdirTemp("", "test-gcs")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(tmpDir)
+	dst := filepath.Join(tmpDir, module)
+	f, err := os.Create(dst)
+	f.Close()
+	if err != nil {
+		t.Fatalf("Failed to create temp file: %v", err)
+	}
+	gcsPath := "path/to/module"
+	server := fakestorage.NewServer(
+		[]fakestorage.Object{
+			{ObjectAttrs: fakestorage.ObjectAttrs{
+				BucketName: "my-bucket", Name: "path/to/module/module.ko",
+			},
+				Content: []byte("content"),
+			},
+		})
+	defer server.Stop()
+	cfg := &GCSConfig{bucket: server.Client().Bucket("my-bucket"), path: gcsPath}
+	_, err = cfg.DownloadKernelModule(ctx, module, tmpDir)
+	wantErrMessage := fmt.Sprintf("failed to download %s to %s. File already exists", module, cfg.path)
+	if err == nil || err.Error() != wantErrMessage {
+		t.Errorf("TestDownloadKernelModuleSuccessful: expected error: %s\t got error: %v", wantErrMessage, err.Error())
+	}
+}
+
+func TestDownloadKernelModuleDoesNotExist(t *testing.T) {
+	ctx := context.Background()
+	module := "module.ko"
+	tmpDir, err := os.MkdirTemp("", "test-gcs")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(tmpDir)
+	gcsPath := "path/to/module"
+	server := fakestorage.NewServer(
+		[]fakestorage.Object{
+			{ObjectAttrs: fakestorage.ObjectAttrs{
+				BucketName: "my-bucket", Name: "path/to/module/unknown.ko",
+			},
+				Content: []byte("content"),
+			},
+		})
+	defer server.Stop()
+	cfg := &GCSConfig{bucket: server.Client().Bucket("my-bucket"), path: gcsPath}
+	_, err = cfg.DownloadKernelModule(ctx, module, tmpDir)
+	if err == nil || !strings.HasPrefix(err.Error(), "failed to read from module") {
+		t.Errorf("TestDownloadKernelModuleDoesNotExist: expected error: failed to read from module \t got error: %v", err)
+	}
+}