diff --git a/go.mod b/go.mod index b527ed140..fcc965266 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module kusionstack.io/kusion go 1.22.1 require ( + cloud.google.com/go/secretmanager v1.14.2 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible github.com/Azure/go-autorest/autorest v0.11.29 github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 @@ -93,6 +94,8 @@ require ( ) require ( + cloud.google.com/go/auth v0.9.9 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/chainguard-dev/git-urls v1.0.2 // indirect github.com/containerd/errdefs v0.3.0 // indirect @@ -131,10 +134,10 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - cloud.google.com/go v0.112.1 // indirect + cloud.google.com/go v0.116.0 // indirect cloud.google.com/go/compute/metadata v0.5.2 // indirect - cloud.google.com/go/iam v1.1.6 // indirect - cloud.google.com/go/storage v1.38.0 // indirect + cloud.google.com/go/iam v1.2.1 // indirect + cloud.google.com/go/storage v1.43.0 dario.cat/mergo v1.0.1 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect @@ -228,11 +231,11 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect - github.com/google/s2a-go v0.1.7 // indirect + github.com/google/s2a-go v0.1.8 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.2 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect github.com/gorilla/mux v1.8.1 // indirect @@ -317,8 +320,8 @@ require ( github.com/xlab/treeprint v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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.53.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/otel v1.31.0 // indirect go.opentelemetry.io/otel/metric v1.31.0 // indirect go.opentelemetry.io/otel/trace v1.31.0 // indirect @@ -328,15 +331,15 @@ require ( golang.org/x/crypto v0.29.0 golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.31.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.9.0 golang.org/x/sys v0.27.0 // indirect golang.org/x/term v0.26.0 // indirect golang.org/x/text v0.20.0 // indirect - golang.org/x/time v0.6.0 // indirect + golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.26.0 // indirect - google.golang.org/api v0.169.0 // indirect - google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/api v0.203.0 + google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/grpc v1.69.0 diff --git a/go.sum b/go.sum index a9cfd3c7c..b8b836b58 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9 cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= -cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= @@ -54,6 +54,10 @@ cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjby cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ= +cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= +cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -115,12 +119,14 @@ cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y97 cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= -cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= +cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= +cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= +cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= @@ -160,6 +166,8 @@ cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92 cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.14.2 h1:2XscWCfy//l/qF96YE18/oUaNJynAx749Jg3u0CjQr8= +cloud.google.com/go/secretmanager v1.14.2/go.mod h1:Q18wAPMM6RXLC/zVpWTlqq2IBSbbm7pKBlM3lCKsmjw= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= @@ -177,8 +185,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg= -cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= +cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= +cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= @@ -671,8 +679,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -690,8 +698,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -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/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -701,8 +709,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -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/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -712,8 +720,8 @@ github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99 github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA= -github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= @@ -1139,10 +1147,10 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 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.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= @@ -1458,8 +1466,8 @@ golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.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= @@ -1527,8 +1535,6 @@ golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1577,8 +1583,8 @@ google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.169.0 h1:QwWPy71FgMWqJN/l6jVlFHUa29a7dcUy02I8o799nPY= -google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg= +google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU= +google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI= 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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1687,8 +1693,8 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE= google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= diff --git a/pkg/apis/api.kusion.io/v1/types.go b/pkg/apis/api.kusion.io/v1/types.go index 1199ca989..34b3d257d 100644 --- a/pkg/apis/api.kusion.io/v1/types.go +++ b/pkg/apis/api.kusion.io/v1/types.go @@ -15,8 +15,12 @@ package v1 import ( + "context" + "encoding/json" "time" + secretmanager "cloud.google.com/go/secretmanager/apiv1" + googleauth "golang.org/x/oauth2/google" v1 "k8s.io/api/core/v1" ) @@ -247,22 +251,25 @@ const ( BackendS3Region = "region" BackendS3ForcePathStyle = "forcePathStyle" - BackendTypeLocal = "local" - BackendTypeOss = "oss" - BackendTypeS3 = "s3" - - EnvOssAccessKeyID = "OSS_ACCESS_KEY_ID" - EnvOssAccessKeySecret = "OSS_ACCESS_KEY_SECRET" - EnvAwsAccessKeyID = "AWS_ACCESS_KEY_ID" - EnvAwsSecretAccessKey = "AWS_SECRET_ACCESS_KEY" - EnvAwsDefaultRegion = "AWS_DEFAULT_REGION" - EnvAwsRegion = "AWS_REGION" - EnvAlicloudAccessKey = "ALICLOUD_ACCESS_KEY" - EnvAlicloudSecretKey = "ALICLOUD_SECRET_KEY" - EnvAlicloudRegion = "ALICLOUD_REGION" - EnvViettelCloudCmpURL = "VIETTEL_CLOUD_CMP_URL" - EnvViettelCloudUserToken = "VIETTEL_CLOUD_USER_TOKEN" - EnvViettelCloudProjectID = "VIETTEL_CLOUD_PROJECT_ID" + BackendTypeLocal = "local" + BackendTypeOss = "oss" + BackendTypeS3 = "s3" + BackendTypeGoogle = "google" + + EnvOssAccessKeyID = "OSS_ACCESS_KEY_ID" + EnvOssAccessKeySecret = "OSS_ACCESS_KEY_SECRET" + EnvAwsAccessKeyID = "AWS_ACCESS_KEY_ID" + EnvAwsSecretAccessKey = "AWS_SECRET_ACCESS_KEY" + EnvAwsDefaultRegion = "AWS_DEFAULT_REGION" + EnvAwsRegion = "AWS_REGION" + EnvAlicloudAccessKey = "ALICLOUD_ACCESS_KEY" + EnvAlicloudSecretKey = "ALICLOUD_SECRET_KEY" + EnvAlicloudRegion = "ALICLOUD_REGION" + EnvViettelCloudCmpURL = "VIETTEL_CLOUD_CMP_URL" + EnvViettelCloudUserToken = "VIETTEL_CLOUD_USER_TOKEN" + EnvViettelCloudProjectID = "VIETTEL_CLOUD_PROJECT_ID" + EnvGoogleCloudCredentials = "GOOGLE_CLOUD_CREDENTIALS" + EnvGoogleCloudCredentialsPath = "GOOGLE_CLOUD_CREDENTIALS_PATH" FieldImportedResources = "importedResources" FieldHealthPolicy = "healthPolicy" @@ -312,6 +319,18 @@ type BackendS3Config struct { Region string `yaml:"region,omitempty" json:"region,omitempty"` } +// BackendGoogleConfig contains the config of using google as backend, which can be converted from BackendConfig +// if Type is BackendGoogleConfig. +type BackendGoogleConfig struct { + *GenericBackendObjectStorageConfig `yaml:",inline" json:",inline"` + + // Credentials of Google. + // Credentials string `yaml:"credentials,omitempty" json:"credentials,omitempty"` + Credentials *googleauth.Credentials `yaml:"credentials,omitempty" json:"credentials,omitempty"` + // Region of Google. + Region string `yaml:"region,omitempty" json:"region,omitempty"` +} + // GenericBackendObjectStorageConfig contains generic configs which can be reused by BackendOssConfig and // BackendS3Config. type GenericBackendObjectStorageConfig struct { @@ -394,6 +413,35 @@ func (b *BackendConfig) ToS3Backend() *BackendS3Config { } } +// ToGoogleBackend converts BackendConfig to structured BackendGoogleConfig, works only when the Type is +// BackendTypeGoogle, and the Configs are with correct type, or return nil. +func (b *BackendConfig) ToGoogleBackend() *BackendGoogleConfig { + if b.Type != BackendTypeGoogle { + return nil + } + var creds *googleauth.Credentials + bucket, _ := b.Configs[BackendGenericOssBucket].(string) + prefix, _ := b.Configs[BackendGenericOssPrefix].(string) + if credentialsJSON, ok := b.Configs["credentials"].(map[string]any); ok { + credentialsBytes, err := json.Marshal(credentialsJSON) + if err != nil { + return nil + } + ctx := context.Background() + creds, err = googleauth.CredentialsFromJSON(ctx, credentialsBytes, secretmanager.DefaultAuthScopes()...) + if err != nil { + return nil + } + } + return &BackendGoogleConfig{ + GenericBackendObjectStorageConfig: &GenericBackendObjectStorageConfig{ + Bucket: bucket, + Prefix: prefix, + }, + Credentials: creds, + } +} + // ModuleConfigs is a set of multiple ModuleConfig, whose key is the module name. type ModuleConfigs map[string]*ModuleConfig diff --git a/pkg/backend/backend.go b/pkg/backend/backend.go index 41a422acb..0f1ffed84 100644 --- a/pkg/backend/backend.go +++ b/pkg/backend/backend.go @@ -76,6 +76,13 @@ func NewBackend(name string) (Backend, error) { if err != nil { return nil, fmt.Errorf("new s3 storage of backend %s failed, %w", name, err) } + case v1.BackendTypeGoogle: + bkConfig := bkCfg.ToGoogleBackend() + // storages.CompleteGoogleConfig(bkConfig) + storage, err = storages.NewGoogleStorage(bkConfig) + if err != nil { + return nil, fmt.Errorf("new google storage of backend %s failed, %w", name, err) + } default: return nil, fmt.Errorf("invalid type %s of backend %s", bkCfg.Type, name) } diff --git a/pkg/backend/storages/google.go b/pkg/backend/storages/google.go new file mode 100644 index 000000000..29754ad0d --- /dev/null +++ b/pkg/backend/storages/google.go @@ -0,0 +1,58 @@ +package storages + +import ( + "context" + + google "cloud.google.com/go/storage" + "google.golang.org/api/option" + + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" + "kusionstack.io/kusion/pkg/engine/release" + releasestorages "kusionstack.io/kusion/pkg/engine/release/storages" + "kusionstack.io/kusion/pkg/engine/resource/graph" + graphstorages "kusionstack.io/kusion/pkg/engine/resource/graph/storages" + projectstorages "kusionstack.io/kusion/pkg/project/storages" + "kusionstack.io/kusion/pkg/workspace" + workspacestorages "kusionstack.io/kusion/pkg/workspace/storages" +) + +// GoogleStorage is an implementation of backend.Backend which uses google cloud as storage. +type GoogleStorage struct { + bucket *google.BucketHandle + + // prefix will be added to the object storage key, so that all the files are stored under the prefix. + prefix string +} + +func NewGoogleStorage(config *v1.BackendGoogleConfig) (*GoogleStorage, error) { + client, err := google.NewClient(context.Background(), option.WithCredentials(config.Credentials)) + if err != nil { + return nil, err + } + bucket := client.Bucket(config.Bucket) + + return &GoogleStorage{ + bucket: bucket, + prefix: config.Prefix, + }, nil +} + +func (s *GoogleStorage) WorkspaceStorage() (workspace.Storage, error) { + return workspacestorages.NewGoogleStorage(s.bucket, workspacestorages.GenGenericOssWorkspacePrefixKey(s.prefix)) +} + +func (s *GoogleStorage) ReleaseStorage(project, workspace string) (release.Storage, error) { + return releasestorages.NewGoogleStorage(s.bucket, releasestorages.GenGenericOssReleasePrefixKey(s.prefix, project, workspace)) +} + +func (s *GoogleStorage) StateStorageWithPath(path string) (release.Storage, error) { + return releasestorages.NewGoogleStorage(s.bucket, releasestorages.GenReleasePrefixKeyWithPath(s.prefix, path)) +} + +func (s *GoogleStorage) GraphStorage(project, workspace string) (graph.Storage, error) { + return graphstorages.NewGoogleStorage(s.bucket, graphstorages.GenGenericOssResourcePrefixKey(s.prefix, project, workspace)) +} + +func (s *GoogleStorage) ProjectStorage() (map[string][]string, error) { + return projectstorages.NewGoogleStorage(s.bucket, projectstorages.GenGenericOssReleasePrefixKey(s.prefix)).Get() +} diff --git a/pkg/backend/storages/google_test.go b/pkg/backend/storages/google_test.go new file mode 100644 index 000000000..2b4026cf3 --- /dev/null +++ b/pkg/backend/storages/google_test.go @@ -0,0 +1,68 @@ +package storages + +import ( + "context" + "testing" + + "cloud.google.com/go/storage" + "github.com/stretchr/testify/assert" + "golang.org/x/oauth2/google" + "google.golang.org/api/option" + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" +) + +func TestNewGoogleStorage(t *testing.T) { + tests := []struct { + name string + config *v1.BackendGoogleConfig + wantErr bool + }{ + { + name: "valid config", + config: &v1.BackendGoogleConfig{ + Credentials: &google.Credentials{ + JSON: []byte(`{ + "type": "service_account", + "project_id": "project-id", + "private_key_id": "private-key-id", + "private_key": "private_key", + "client_email": "client-email", + "client_id": "client-id", + "auth_uri": "auth-uri", + "token_uri": "token-uri", + "auth_provider_x509_cert_url": "auth-provider-x509-cert-url", + "client_x509_cert_url": "client-x509-cert-url" + }`), + }, + GenericBackendObjectStorageConfig: &v1.GenericBackendObjectStorageConfig{ + Bucket: "valid-bucket", + Prefix: "valid-prefix", + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := storage.NewClient(context.Background(), option.WithCredentialsJSON(tt.config.Credentials.JSON)) + if err != nil { + if !tt.wantErr { + t.Errorf("NewGoogleStorage() error = %v, wantErr %v", err, tt.wantErr) + } + return + } + defer client.Close() + + got, err := NewGoogleStorage(tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("NewGoogleStorage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + assert.NotNil(t, got) + assert.Equal(t, tt.config.Prefix, got.prefix) + } + }) + } +} diff --git a/pkg/engine/release/storages/google.go b/pkg/engine/release/storages/google.go new file mode 100644 index 000000000..e924a203e --- /dev/null +++ b/pkg/engine/release/storages/google.go @@ -0,0 +1,159 @@ +package storages + +import ( + "context" + "fmt" + "io" + + "gopkg.in/yaml.v3" + + googlestorage "cloud.google.com/go/storage" + + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" +) + +// GoogleStorage is an implementation of release.Storage which uses google cloud as storage. +type GoogleStorage struct { + bucket googlestorage.BucketHandle + + // The prefix to store the release files. + prefix string + + meta *releasesMetaData +} + +// NewGoogleStorage news google cloud release storage, and derives metadata. +func NewGoogleStorage(bucket *googlestorage.BucketHandle, prefix string) (*GoogleStorage, error) { + s := &GoogleStorage{ + bucket: *bucket, + prefix: prefix, + } + if err := s.readMeta(); err != nil { + return nil, err + } + return s, nil +} + +func (s *GoogleStorage) Get(revision uint64) (*v1.Release, error) { + ctx := context.Background() + if !checkRevisionExistence(s.meta, revision) { + return nil, ErrReleaseNotExist + } + + obj := s.bucket.Object(fmt.Sprintf("%s/%d%s", s.prefix, revision, yamlSuffix)) + reader, err := obj.NewReader(ctx) + if err != nil { + return nil, fmt.Errorf("get release from google storage failed: %w", err) + } + defer reader.Close() + content, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("read release failed: %w", err) + } + + rel := &v1.Release{} + if err = yaml.Unmarshal(content, rel); err != nil { + return nil, fmt.Errorf("yaml unmarshal release failed: %w", err) + } + return rel, nil +} + +func (s *GoogleStorage) GetRevisions() []uint64 { + return getRevisions(s.meta) +} + +func (s *GoogleStorage) GetStackBoundRevisions(stack string) []uint64 { + return getStackBoundRevisions(s.meta, stack) +} + +func (s *GoogleStorage) GetLatestRevision() uint64 { + return s.meta.LatestRevision +} + +func (s *GoogleStorage) Create(r *v1.Release) error { + if checkRevisionExistence(s.meta, r.Revision) { + return ErrReleaseAlreadyExist + } + + if err := s.writeRelease(r); err != nil { + return err + } + + addLatestReleaseMetaData(s.meta, r.Revision, r.Stack) + return s.writeMeta() +} + +func (s *GoogleStorage) Update(r *v1.Release) error { + if !checkRevisionExistence(s.meta, r.Revision) { + return ErrReleaseNotExist + } + + return s.writeRelease(r) +} + +func (s *GoogleStorage) readMeta() error { + ctx := context.Background() + obj := s.bucket.Object(s.prefix + "/" + metadataFile) + reader, err := obj.NewReader(ctx) + if err != nil { + if err == googlestorage.ErrObjectNotExist { + s.meta = &releasesMetaData{} + return nil + } + return fmt.Errorf("get releases metadata from google failed: %w", err) + } + defer reader.Close() + content, err := io.ReadAll(reader) + if err != nil { + return fmt.Errorf("read releases metadata failed: %w", err) + } + + if len(content) == 0 { + s.meta = &releasesMetaData{} + return nil + } + + meta := &releasesMetaData{} + if err = yaml.Unmarshal(content, meta); err != nil { + return fmt.Errorf("yaml unmarshal releases metadata failed: %w", err) + } + s.meta = meta + return nil +} + +func (s *GoogleStorage) writeMeta() error { + ctx := context.Background() + obj := s.bucket.Object(s.prefix + "/" + metadataFile) + content, err := yaml.Marshal(s.meta) + if err != nil { + return fmt.Errorf("yaml marshal releases metadata failed: %w", err) + } + + writer := obj.NewWriter(ctx) + if _, err = writer.Write(content); err != nil { + return fmt.Errorf("write releases metadata failed: %w", err) + } + + if err = writer.Close(); err != nil { + return fmt.Errorf("close writer failed: %w", err) + } + return nil +} + +func (s *GoogleStorage) writeRelease(r *v1.Release) error { + content, err := yaml.Marshal(r) + if err != nil { + return fmt.Errorf("yaml marshal release failed: %w", err) + } + + obj := s.bucket.Object(fmt.Sprintf("%s/%d%s", s.prefix, r.Revision, yamlSuffix)) + writer := obj.NewWriter(context.Background()) + if _, err = writer.Write(content); err != nil { + return fmt.Errorf("write release failed: %w", err) + } + + if err = writer.Close(); err != nil { + return fmt.Errorf("close writer failed: %w", err) + } + return nil +} diff --git a/pkg/engine/release/storages/google_test.go b/pkg/engine/release/storages/google_test.go new file mode 100644 index 000000000..32f770693 --- /dev/null +++ b/pkg/engine/release/storages/google_test.go @@ -0,0 +1,216 @@ +package storages + +import ( + "context" + "testing" + + googlestorage "cloud.google.com/go/storage" + "github.com/bytedance/mockey" + "github.com/stretchr/testify/assert" + googleauth "golang.org/x/oauth2/google" + "google.golang.org/api/option" + + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" +) + +func mockGoogleBucketHandle() *googlestorage.BucketHandle { + config := &v1.BackendGoogleConfig{ + Credentials: &googleauth.Credentials{ + JSON: []byte(`{ + "type": "service_account", + "project_id": "project-id", + "private_key_id": "private-key-id", + "private_key": "private_key", + "client_email": "client-email", + "client_id": "client-id", + "auth_uri": "auth-uri", + "token_uri": "token-uri", + "auth_provider_x509_cert_url": "auth-provider-x509-cert-url", + "client_x509_cert_url": "client-x509-cert-url" + }`), + }, + GenericBackendObjectStorageConfig: &v1.GenericBackendObjectStorageConfig{ + Bucket: "valid-bucket", + Prefix: "valid-prefix", + }, + } + client, err := googlestorage.NewClient(context.Background(), option.WithCredentials(config.Credentials)) + if err != nil { + return nil + } + bucket := client.Bucket(config.Bucket) + return bucket +} + +func mockGoogleStorage() *GoogleStorage { + return &GoogleStorage{ + bucket: *mockGoogleBucketHandle(), + prefix: "valid-prefix", + meta: mockReleasesMeta(), + } +} + +func mockGoogleStorageWriteMeta() { + mockey.Mock((*GoogleStorage).writeMeta).Return(nil).Build() +} + +func mockGoogleStorageWriteRelease() { + mockey.Mock((*GoogleStorage).writeRelease).Return(nil).Build() +} + +func TestNewGoogleStorage(t *testing.T) { + tests := []struct { + name string + bucket *googlestorage.BucketHandle + prefix string + wantErr bool + }{ + { + name: "valid bucket and prefix", + bucket: mockGoogleBucketHandle(), + prefix: "valid-prefix", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + mockey.Mock((*GoogleStorage).readMeta).Return(nil).Build() + got, err := NewGoogleStorage(tt.bucket, tt.prefix) + if (err != nil) != tt.wantErr { + t.Errorf("NewGoogleStorage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + assert.NotNil(t, got) + assert.Equal(t, tt.prefix, got.prefix) + } + }) + }) + } +} + +func TestGoogleStorage_GetRevisions(t *testing.T) { + testcases := []struct { + name string + expectedRevisions []uint64 + }{ + { + name: "get release revisions successfully", + expectedRevisions: []uint64{1, 2, 3}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + revisions := mockGoogleStorage().GetRevisions() + assert.Equal(t, tc.expectedRevisions, revisions) + }) + }) + } +} + +func TestGoogleStorage_GetStackBoundRevisions(t *testing.T) { + testcases := []struct { + name string + stack string + expectedRevisions []uint64 + }{ + { + name: "get stack bound release revisions successfully", + stack: "test_stack", + expectedRevisions: []uint64{1, 2, 3}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + revisions := mockGoogleStorage().GetStackBoundRevisions(tc.stack) + assert.Equal(t, tc.expectedRevisions, revisions) + }) + }) + } +} + +func TestGoogleStorage_GetLatestRevision(t *testing.T) { + testcases := []struct { + name string + expectedRevision uint64 + }{ + { + name: "get latest release revision successfully", + expectedRevision: 3, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + revision := mockGoogleStorage().GetLatestRevision() + assert.Equal(t, tc.expectedRevision, revision) + }) + }) + } +} + +func TestGoogleStorage_Create(t *testing.T) { + testcases := []struct { + name string + success bool + r *v1.Release + }{ + { + name: "create release successfully", + success: true, + r: mockRelease(4), + }, + { + name: "failed to create release already exist", + success: false, + r: mockRelease(3), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + mockGoogleStorageWriteMeta() + mockGoogleStorageWriteRelease() + err := mockGoogleStorage().Create(tc.r) + assert.Equal(t, tc.success, err == nil) + }) + }) + } +} + +func TestGoogleStorage_Update(t *testing.T) { + testcases := []struct { + name string + success bool + r *v1.Release + }{ + { + name: "update release successfully", + success: true, + r: mockRelease(3), + }, + { + name: "failed to update release not exist", + success: false, + r: mockRelease(4), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + mockGoogleStorageWriteRelease() + err := mockGoogleStorage().Update(tc.r) + assert.Equal(t, tc.success, err == nil) + }) + }) + } +} diff --git a/pkg/engine/resource/graph/storages/google.go b/pkg/engine/resource/graph/storages/google.go new file mode 100644 index 000000000..cf6b49915 --- /dev/null +++ b/pkg/engine/resource/graph/storages/google.go @@ -0,0 +1,120 @@ +package storages + +import ( + "context" + "encoding/json" + "fmt" + "io" + + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" + "kusionstack.io/kusion/pkg/engine/resource/graph" + + googlestorage "cloud.google.com/go/storage" +) + +// GoogleStorage is an implementation of graph.Storage which uses google as storage. +type GoogleStorage struct { + bucket googlestorage.BucketHandle + + // The prefix to store the release files. + prefix string +} + +// NewGoogleStorage news google cloud graph storage, and derives metadata. +func NewGoogleStorage(bucket *googlestorage.BucketHandle, prefix string) (*GoogleStorage, error) { + s := &GoogleStorage{ + bucket: *bucket, + prefix: prefix, + } + return s, nil +} + +// Get gets the graph from google. +func (s *GoogleStorage) Get() (*v1.Graph, error) { + output, _ := s.getGoogleStorageObject(s.prefix, graphFileName) + r := &v1.Graph{} + if err := json.Unmarshal(output, r); err != nil { + return nil, fmt.Errorf("json unmarshal graph failed: %w", err) + } + + // Index is not stored in google, so we need to rebuild it. + // Update resource index to use index in the memory. + graph.UpdateResourceIndex(r.Resources) + + return r, nil +} + +// Create creates the graph in google. +func (s *GoogleStorage) Create(r *v1.Graph) error { + output, _ := s.getGoogleStorageObject(s.prefix, graphFileName) + if output != nil { + return ErrGraphAlreadyExist + } + + return s.writeGraph(r) +} + +// Update updates the graph in google. +func (s *GoogleStorage) Update(r *v1.Graph) error { + _, err := s.getGoogleStorageObject(s.prefix, graphFileName) + if err != nil { + return ErrGraphNotExist + } + + return s.writeGraph(r) +} + +// Delete deletes the graph in google +func (s *GoogleStorage) Delete() error { + key := fmt.Sprintf("%s/%s", s.prefix, graphFileName) + obj := s.bucket.Object(key) + if err := obj.Delete(context.Background()); err != nil { + return fmt.Errorf("delete graph from google storage failed: %w", err) + } + + return nil +} + +// writeGraph writes the graph to google. +func (s *GoogleStorage) writeGraph(r *v1.Graph) error { + content, err := json.Marshal(r) + if err != nil { + return fmt.Errorf("json marshal graph failed: %w", err) + } + + obj := s.bucket.Object(fmt.Sprintf("%s/%s", s.prefix, graphFileName)) + writer := obj.NewWriter(context.Background()) + if _, err = writer.Write(content); err != nil { + return fmt.Errorf("write graph failed: %w", err) + } + if err = writer.Close(); err != nil { + return fmt.Errorf("close writer failed: %w", err) + } + return nil +} + +// CheckGraphStorageExistence checks whether the graph storage exists. +func (s *GoogleStorage) CheckGraphStorageExistence() bool { + if _, err := s.getGoogleStorageObject(s.prefix, graphFileName); err != nil { + return false + } + + return true +} + +// getGoogleStorageObject gets the graph object from google. +func (s *GoogleStorage) getGoogleStorageObject(prefix, graphFileName string) ([]byte, error) { + key := fmt.Sprintf("%s/%s", prefix, graphFileName) + ctx := context.Background() + obj := s.bucket.Object(key) + reader, err := obj.NewReader(ctx) + if err != nil { + return nil, fmt.Errorf("get release from google storage failed: %w", err) + } + defer reader.Close() + content, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("read release failed: %w", err) + } + return content, nil +} diff --git a/pkg/project/storages/google.go b/pkg/project/storages/google.go new file mode 100644 index 000000000..2b8beb921 --- /dev/null +++ b/pkg/project/storages/google.go @@ -0,0 +1,70 @@ +package storages + +import ( + "context" + "fmt" + "strings" + + "google.golang.org/api/iterator" + + googlestorage "cloud.google.com/go/storage" +) + +// GoogleStorage is an implementation of graph.Storage which uses google cloud as storage. +type GoogleStorage struct { + bucket googlestorage.BucketHandle + prefix string +} + +// NewGoogleStorage creates a new GoogleStorage instance. +func NewGoogleStorage(bucket *googlestorage.BucketHandle, prefix string) *GoogleStorage { + s := &GoogleStorage{ + bucket: *bucket, + prefix: prefix, + } + return s +} + +// Get returns a project map which key is workspace name and value is its belonged project list. +func (s *GoogleStorage) Get() (map[string][]string, error) { + ctx := context.Background() + projects := map[string][]string{} + projectQuery := &googlestorage.Query{ + Prefix: s.prefix + "/", + Delimiter: "/", + } + it := s.bucket.Objects(ctx, projectQuery) + for { + project, err := it.Next() + if err == iterator.Done { + break + } + if err != nil { + return nil, fmt.Errorf("list projects directory from google storage failed: %w", err) + } + projectDir := strings.TrimPrefix(project.Name, s.prefix+"/") + projectDir = strings.TrimSuffix(projectDir, "/") + + // List workspaces under the project prefix + wsQuery := &googlestorage.Query{ + Prefix: project.Prefix + "/", + Delimiter: "/", + } + wsIt := s.bucket.Objects(ctx, wsQuery) + for { + workspace, err := wsIt.Next() + if err == iterator.Done { + break + } + if err != nil { + return nil, fmt.Errorf("list project's workspaces directory from google storage failed: %w", err) + } + // Get each of the workspace name + workspaceDir := strings.TrimPrefix(workspace.Prefix, project.Prefix) + workspaceDir = strings.TrimSuffix(workspaceDir, "/") + // Store workspace name as key, project name as value + projects[workspaceDir] = append(projects[workspaceDir], projectDir) + } + } + return projects, nil +} diff --git a/pkg/project/storages/google_test.go b/pkg/project/storages/google_test.go new file mode 100644 index 000000000..342821fa3 --- /dev/null +++ b/pkg/project/storages/google_test.go @@ -0,0 +1,79 @@ +package storages + +import ( + "context" + "testing" + + googlestorage "cloud.google.com/go/storage" + "github.com/stretchr/testify/assert" + googleauth "golang.org/x/oauth2/google" + "google.golang.org/api/option" + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" +) + +func mockGoogleBucketHandle() *googlestorage.BucketHandle { + config := &v1.BackendGoogleConfig{ + Credentials: &googleauth.Credentials{ + JSON: []byte(`{ + "type": "service_account", + "project_id": "project-id", + "private_key_id": "private-key-id", + "private_key": "private_key", + "client_email": "client-email", + "client_id": "client-id", + "auth_uri": "auth-uri", + "token_uri": "token-uri", + "auth_provider_x509_cert_url": "auth-provider-x509-cert-url", + "client_x509_cert_url": "client-x509-cert-url" + }`), + }, + GenericBackendObjectStorageConfig: &v1.GenericBackendObjectStorageConfig{ + Bucket: "valid-bucket", + Prefix: "valid-prefix", + }, + } + client, err := googlestorage.NewClient(context.Background(), option.WithCredentials(config.Credentials)) + if err != nil { + return nil + } + bucket := client.Bucket(config.Bucket) + return bucket +} + +func TestGoogleStorage_Get(t *testing.T) { + tests := []struct { + name string + bucketName string + prefix string + objects []string + want map[string][]string + wantErr bool + }{ + { + name: "error listing objects", + bucketName: "error-bucket", + prefix: "error-prefix", + objects: nil, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bucket := mockGoogleBucketHandle() + gs := &GoogleStorage{ + bucket: *bucket, + prefix: tt.prefix, + } + got, err := gs.Get() + if (err != nil) != tt.wantErr { + t.Errorf("GoogleStorage.Get() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + assert.Equal(t, tt.want, got) + } + }) + } +} diff --git a/pkg/server/manager/workspace/util.go b/pkg/server/manager/workspace/util.go index 7dc3eca26..d9641d577 100644 --- a/pkg/server/manager/workspace/util.go +++ b/pkg/server/manager/workspace/util.go @@ -40,6 +40,12 @@ func NewBackendFromEntity(backendEntity entity.Backend) (backend.Backend, error) if err != nil { return nil, fmt.Errorf("new s3 storage of backend %s failed, %w", backendEntity.Name, err) } + case v1.BackendTypeGoogle: + bkConfig := backendEntity.BackendConfig.ToGoogleBackend() + storage, err = storages.NewGoogleStorage(bkConfig) + if err != nil { + return nil, fmt.Errorf("new google storage of backend %s failed, %w", backendEntity.Name, err) + } default: return nil, fmt.Errorf("invalid type %s of backend %s", backendEntity.BackendConfig.Type, backendEntity.Name) } diff --git a/pkg/workspace/storages/google.go b/pkg/workspace/storages/google.go new file mode 100644 index 000000000..f4f043b0b --- /dev/null +++ b/pkg/workspace/storages/google.go @@ -0,0 +1,197 @@ +package storages + +import ( + "context" + "fmt" + "io" + + "gopkg.in/yaml.v3" + + googlestorage "cloud.google.com/go/storage" + + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" +) + +// GoogleStorage is an implementation of workspace.Storage which uses google cloud as storage. +type GoogleStorage struct { + bucket googlestorage.BucketHandle + + // The prefix to store the workspaces files. + prefix string + + meta *workspacesMetaData +} + +// NewGoogleStorage news google cloud workspace storage and init default workspace. +func NewGoogleStorage(bucket *googlestorage.BucketHandle, prefix string) (*GoogleStorage, error) { + s := &GoogleStorage{ + bucket: *bucket, + prefix: prefix, + } + if err := s.readMeta(); err != nil { + return nil, err + } + return s, s.initDefaultWorkspaceIf() +} + +func (s *GoogleStorage) Get(name string) (*v1.Workspace, error) { + if name == "" { + name = s.meta.Current + } + if !checkWorkspaceExistence(s.meta, name) { + return nil, ErrWorkspaceNotExist + } + + obj := s.bucket.Object(s.prefix + "/" + name + yamlSuffix) + reader, err := obj.NewReader(context.Background()) + if err != nil { + return nil, fmt.Errorf("get workspace from google storage failed: %w", err) + } + defer reader.Close() + content, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("read workspace failed: %w", err) + } + + ws := &v1.Workspace{} + if err = yaml.Unmarshal(content, ws); err != nil { + return nil, fmt.Errorf("yaml unmarshal workspace failed: %w", err) + } + ws.Name = name + return ws, nil +} + +func (s *GoogleStorage) Create(ws *v1.Workspace) error { + if checkWorkspaceExistence(s.meta, ws.Name) { + return ErrWorkspaceAlreadyExist + } + + if err := s.writeWorkspace(ws); err != nil { + return err + } + + addAvailableWorkspaces(s.meta, ws.Name) + return s.writeMeta() +} + +func (s *GoogleStorage) Update(ws *v1.Workspace) error { + if ws.Name == "" { + ws.Name = s.meta.Current + } + if !checkWorkspaceExistence(s.meta, ws.Name) { + return ErrWorkspaceNotExist + } + + return s.writeWorkspace(ws) +} + +func (s *GoogleStorage) Delete(name string) error { + if !checkWorkspaceExistence(s.meta, name) { + return nil + } + + obj := s.bucket.Object(s.prefix + "/" + name + yamlSuffix) + if err := obj.Delete(context.Background()); err != nil { + return fmt.Errorf("remove workspace in google storage failed: %w", err) + } + + removeAvailableWorkspaces(s.meta, name) + return s.writeMeta() +} + +func (s *GoogleStorage) GetNames() ([]string, error) { + return s.meta.AvailableWorkspaces, nil +} + +func (s *GoogleStorage) GetCurrent() (string, error) { + return s.meta.Current, nil +} + +func (s *GoogleStorage) SetCurrent(name string) error { + if !checkWorkspaceExistence(s.meta, name) { + return ErrWorkspaceNotExist + } + s.meta.Current = name + return s.writeMeta() +} + +func (s *GoogleStorage) initDefaultWorkspaceIf() error { + if !checkWorkspaceExistence(s.meta, DefaultWorkspace) { + // if there is no default workspace, create one with empty workspace. + if err := s.writeWorkspace(&v1.Workspace{Name: DefaultWorkspace}); err != nil { + return err + } + addAvailableWorkspaces(s.meta, DefaultWorkspace) + } + + if s.meta.Current == "" { + s.meta.Current = DefaultWorkspace + } + return s.writeMeta() +} + +func (s *GoogleStorage) readMeta() error { + ctx := context.Background() + obj := s.bucket.Object(s.prefix + "/" + metadataFile) + reader, err := obj.NewReader(ctx) + if err != nil { + if err == googlestorage.ErrObjectNotExist { + s.meta = &workspacesMetaData{} + return nil + } + return fmt.Errorf("get workspaces metadata from google failed: %w", err) + } + defer reader.Close() + content, err := io.ReadAll(reader) + if err != nil { + return fmt.Errorf("read workspaces meta data failed: %w", err) + } + if len(content) == 0 { + s.meta = &workspacesMetaData{} + return nil + } + + meta := &workspacesMetaData{} + if err = yaml.Unmarshal(content, meta); err != nil { + return fmt.Errorf("yaml unmarshal workspaces metadata failed: %w", err) + } + s.meta = meta + return nil +} + +func (s *GoogleStorage) writeMeta() error { + ctx := context.Background() + obj := s.bucket.Object(s.prefix + "/" + metadataFile) + content, err := yaml.Marshal(s.meta) + if err != nil { + return fmt.Errorf("yaml marshal workspaces metadata failed: %w", err) + } + + writer := obj.NewWriter(ctx) + if _, err = writer.Write(content); err != nil { + return fmt.Errorf("write workspaces metadata failed: %w", err) + } + + if err = writer.Close(); err != nil { + return fmt.Errorf("close writer failed: %w", err) + } + return nil +} + +func (s *GoogleStorage) writeWorkspace(ws *v1.Workspace) error { + content, err := yaml.Marshal(ws) + if err != nil { + return fmt.Errorf("yaml marshal workspace failed: %w", err) + } + + obj := s.bucket.Object(s.prefix + "/" + ws.Name + yamlSuffix) + writer := obj.NewWriter(context.Background()) + if _, err = writer.Write(content); err != nil { + return fmt.Errorf("write workspace failed: %w", err) + } + + if err = writer.Close(); err != nil { + return fmt.Errorf("close writer failed: %w", err) + } + return nil +} diff --git a/pkg/workspace/storages/google_test.go b/pkg/workspace/storages/google_test.go new file mode 100644 index 000000000..1201d2f1b --- /dev/null +++ b/pkg/workspace/storages/google_test.go @@ -0,0 +1,224 @@ +package storages + +import ( + "context" + "testing" + + "github.com/bytedance/mockey" + "github.com/stretchr/testify/assert" + "google.golang.org/api/option" + + googlestorage "cloud.google.com/go/storage" + googleauth "golang.org/x/oauth2/google" + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" +) + +func mockGoogleBucketHandle() *googlestorage.BucketHandle { + config := &v1.BackendGoogleConfig{ + Credentials: &googleauth.Credentials{ + JSON: []byte(`{ + "type": "service_account", + "project_id": "project-id", + "private_key_id": "private-key-id", + "private_key": "private_key", + "client_email": "client-email", + "client_id": "client-id", + "auth_uri": "auth-uri", + "token_uri": "token-uri", + "auth_provider_x509_cert_url": "auth-provider-x509-cert-url", + "client_x509_cert_url": "client-x509-cert-url" + }`), + }, + GenericBackendObjectStorageConfig: &v1.GenericBackendObjectStorageConfig{ + Bucket: "valid-bucket", + Prefix: "valid-prefix", + }, + } + client, err := googlestorage.NewClient(context.Background(), option.WithCredentials(config.Credentials)) + if err != nil { + return nil + } + bucket := client.Bucket(config.Bucket) + return bucket +} + +func mockGoogleStorage() *GoogleStorage { + return &GoogleStorage{ + bucket: *mockGoogleBucketHandle(), + prefix: "valid-prefix", + meta: mockWorkspacesMetaData(), + } +} + +func mockGoogleStorageWriteMeta() { + mockey.Mock((*GoogleStorage).writeMeta).Return(nil).Build() +} + +func mockGoogleStorageWriteWorkspace() { + mockey.Mock((*GoogleStorage).writeWorkspace).Return(nil).Build() +} + +func TestGoogleStorage_Create(t *testing.T) { + testcases := []struct { + name string + success bool + workspace *v1.Workspace + }{ + { + name: "create workspace successfully", + success: true, + workspace: mockWorkspace("pre"), + }, + { + name: "failed to create workspace already exist", + success: false, + workspace: mockWorkspace("dev"), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + mockGoogleStorageWriteMeta() + mockGoogleStorageWriteWorkspace() + err := mockGoogleStorage().Create(tc.workspace) + assert.Equal(t, tc.success, err == nil) + }) + }) + } +} + +func TestGoogleStorage_Update(t *testing.T) { + testcases := []struct { + name string + success bool + workspace *v1.Workspace + }{ + { + name: "update workspace successfully", + success: true, + workspace: mockWorkspace("dev"), + }, + { + name: "failed to update workspace not exist", + success: false, + workspace: mockWorkspace("pre"), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + mockGoogleStorageWriteWorkspace() + err := mockGoogleStorage().Update(tc.workspace) + assert.Equal(t, tc.success, err == nil) + }) + }) + } +} + +func TestGoogleStorage_Delete(t *testing.T) { + testcases := []struct { + name string + success bool + wsName string + }{ + { + name: "delete workspace successfully", + success: true, + wsName: "dev", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + mockey.Mock((*googlestorage.ObjectHandle).Delete).Return(nil).Build() + mockGoogleStorageWriteMeta() + err := mockGoogleStorage().Delete(tc.wsName) + assert.Equal(t, tc.success, err == nil) + }) + }) + } +} + +func TestGoogleStorage_GetNames(t *testing.T) { + testcases := []struct { + name string + success bool + expectedNames []string + }{ + { + name: "get all workspace names successfully", + success: true, + expectedNames: []string{"default", "dev", "prod"}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + wsNames, err := mockGoogleStorage().GetNames() + assert.Equal(t, tc.success, err == nil) + if tc.success { + assert.Equal(t, tc.expectedNames, wsNames) + } + }) + }) + } +} + +func TestGoogleStorage_GetCurrent(t *testing.T) { + testcases := []struct { + name string + success bool + expectedCurrent string + }{ + { + name: "get current workspace successfully", + success: true, + expectedCurrent: "dev", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + current, err := mockGoogleStorage().GetCurrent() + assert.Equal(t, tc.success, err == nil) + if tc.success { + assert.Equal(t, tc.expectedCurrent, current) + } + }) + }) + } +} + +func TestGoogleStorage_SetCurrent(t *testing.T) { + testcases := []struct { + name string + success bool + current string + }{ + { + name: "set current workspace successfully", + success: true, + current: "prod", + }, + { + name: "failed to set current workspace not exist", + success: false, + current: "pre", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + mockey.PatchConvey("mock google storage operation", t, func() { + mockGoogleStorageWriteMeta() + err := mockGoogleStorage().SetCurrent(tc.current) + assert.Equal(t, tc.success, err == nil) + }) + }) + } +}