From 5900066f7b1930a3510171278785137d54c27e38 Mon Sep 17 00:00:00 2001 From: Julien Breux Date: Mon, 2 May 2022 16:24:39 +0200 Subject: [PATCH] chore(driver/gcs): add GCS driver to store releases Signed-off-by: Julien Breux --- cmd/helm/root.go | 4 +- go.mod | 25 +- go.sum | 52 ++++ pkg/action/action.go | 11 + pkg/storage/driver/gcs.go | 409 ++++++++++++++++++++++++++++++++ pkg/storage/driver/gcs_test.go | 262 ++++++++++++++++++++ pkg/storage/driver/mock_test.go | 16 ++ 7 files changed, 770 insertions(+), 9 deletions(-) create mode 100644 pkg/storage/driver/gcs.go create mode 100644 pkg/storage/driver/gcs_test.go diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 394f241d5..3c3dc2ad3 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -51,8 +51,10 @@ Environment variables: | $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. | | $HELM_DATA_HOME | set an alternative location for storing Helm data. | | $HELM_DEBUG | indicate whether or not Helm is running in Debug mode | -| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. | +| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql, gcs. | | $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. | +| $HELM_DRIVER_GCS_BUCKET | set the bucket identifier the GCS storage driver should use. | +| $HELM_DRIVER_GCS_PREFIX_PATH | set the prefix path the GCS storage driver should use. | | $HELM_MAX_HISTORY | set the maximum number of helm release history. | | $HELM_NAMESPACE | set the namespace used for the helm operations. | | $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. | diff --git a/go.mod b/go.mod index 4915d6057..35740c83a 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,10 @@ require ( ) require ( - cloud.google.com/go v0.99.0 // indirect + cloud.google.com/go v0.100.2 // indirect + cloud.google.com/go/compute v1.5.0 // indirect + cloud.google.com/go/iam v0.3.0 // indirect + cloud.google.com/go/storage v1.22.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.20 // indirect @@ -87,14 +90,17 @@ require ( github.com/go-openapi/swag v0.19.14 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.0.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/gomodule/redigo v1.8.2 // indirect github.com/google/btree v1.0.1 // indirect - github.com/google/go-cmp v0.5.6 // indirect + github.com/google/go-cmp v0.5.7 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.2.0 // indirect + github.com/googleapis/gax-go/v2 v2.2.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect + github.com/googleapis/go-type-adapters v1.0.0 // indirect github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect @@ -138,17 +144,20 @@ require ( github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 // indirect github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 // indirect github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f // indirect + go.opencensus.io v0.23.0 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect - golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d // indirect - golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect + golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/api v0.74.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect - google.golang.org/grpc v1.43.0 // indirect - google.golang.org/protobuf v1.27.1 // indirect + google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect + google.golang.org/grpc v1.45.0 // indirect + google.golang.org/protobuf v1.28.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/go.sum b/go.sum index 4a31b2cae..ad66eedf9 100644 --- a/go.sum +++ b/go.sum @@ -29,15 +29,23 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiLM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -47,6 +55,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.0 h1:NUV0NNp9nkBuW66BFRLuMgldN60C57ET3dhbwLIYio8= +cloud.google.com/go/storage v1.22.0/go.mod h1:GbaLEoMqbVm6sx3Z0R++gSiBlgMv6yUi2q1DeGFKQgE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -564,6 +574,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -599,10 +611,14 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ 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= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0 h1:s7jOdKSaksJVOxE0Y/S32otcfiP+UQ0cL8/GTKaONwE= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= @@ -1243,6 +1259,10 @@ golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d h1:62NvYBuaanGXR2ZOfwDFkhhl6X1DUgf8qg3GuQvxZsE= golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1261,6 +1281,9 @@ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= 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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1378,8 +1401,14 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 h1:eJv7u3ksNXoLbGSKuv2s/SIO4tJVxc/A+MTpzxDgz/Q= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1510,6 +1539,12 @@ google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqiv google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0 h1:ExR2D+5TYIrMphWgs5JCgwRhEDlPDXXrLwHHMgPHTXE= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= 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= @@ -1565,6 +1600,7 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= @@ -1585,8 +1621,18 @@ google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 h1:Et6SkiuvnBn+SgrSYXs/BrUpGB4mbdwt4R3vaPIlicA= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf h1:JTjwKJX9erVpsw17w+OIPP7iAgEkN/r8urhWSunEDTs= +google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1616,9 +1662,13 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= 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= @@ -1634,6 +1684,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/action/action.go b/pkg/action/action.go index 82760250f..af4c02e69 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -407,6 +407,17 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp panic(fmt.Sprintf("Unable to instantiate SQL driver: %v", err)) } store = storage.Init(d) + case "gcs": + d, err := driver.NewGCS( + os.Getenv("HELM_DRIVER_GCS_BUCKET"), + namespace, + os.Getenv("HELM_DRIVER_GCS_PREFIX_PATH"), + log, + ) + if err != nil { + panic(fmt.Sprintf("Unable to instantiate GCS driver: %v", err)) + } + store = storage.Init(d) default: // Not sure what to do here. panic("Unknown driver in HELM_DRIVER: " + helmDriver) diff --git a/pkg/storage/driver/gcs.go b/pkg/storage/driver/gcs.go new file mode 100644 index 000000000..b368bd908 --- /dev/null +++ b/pkg/storage/driver/gcs.go @@ -0,0 +1,409 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package driver + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" + + "cloud.google.com/go/storage" + "google.golang.org/api/googleapi" + "google.golang.org/api/iterator" + rspb "helm.sh/helm/v3/pkg/release" +) + +const ( + // GCSDriverName is the string name of this driver. + GCSDriverName = "GCS" + + gcsReleaseNameMetadata = "name" + gcsReleaseNamespaceMetadata = "namespace" + gcsReleaseVersionMetadata = "version" + gcsReleaseStatusMetadata = "status" + gcsReleaseOwnerColumn = "owner" + gcsReleaseCreatedAtMetadata = "createdAt" + gcsReleaseModifiedAtMetadata = "modifiedAt" +) + +// GCS is the GCS storage driver implementation. +type GCS struct { + client *storage.Client + + bucket string + pathPrefix string + + namespace string + + Log func(string, ...interface{}) +} + +// NewGCS initializes a new GCS driver. +func NewGCS(bucket, pathPrefix, namespace string, logger func(string, ...interface{})) (*GCS, error) { + ctx := context.Background() + client, err := storage.NewClient(ctx) + if err != nil { + return nil, err + } + + driver := &GCS{ + client: client, + + bucket: bucket, + pathPrefix: pathPrefix, + namespace: namespace, + + Log: logger, + } + + return driver, nil +} + +// Name returns the name of the driver. +func (s *GCS) Name() string { + return GCSDriverName +} + +// Get returns the release named by key or returns ErrReleaseNotFound. +func (s *GCS) Get(key string) (*rspb.Release, error) { + rel, _, err := s.readRelease(s.fullPathName(key, s.namespace), false) + return rel, err +} + +// List returns the list of all releases such that filter(release) == true +func (s *GCS) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { + namespaces, err := s.listNamespaces() + if err != nil { + s.Log("list: failed to list: %v", err) + return nil, err + } + + var list []*rspb.Release + + for _, namespace := range namespaces { + releases, err := s.listReleases(namespace) + if err != nil { + s.Log("list: failed to list releases in namespace: %v", namespace, err) + continue + } + + for _, key := range releases { + release, _, err := s.readRelease(key, false) + if err != nil { + s.Log("list: failed to read release: %v: %v", release, err) + continue + } + if filter(release) { + list = append(list, release) + } + } + } + + return list, nil +} + +// Query returns the set of releases that match the provided set of labels +func (s *GCS) Query(labels map[string]string) ([]*rspb.Release, error) { + // List namespaces + namespaces, err := s.listNamespaces() + if err != nil { + s.Log("list: failed to list: %v", err) + return nil, err + } + + // Create filter to compare labels and metadata + filter := func(metadata map[string]string) bool { + for key, val := range labels { + if metadataVal, ok := metadata[key]; !ok || metadataVal != val { + return false + } + } + + return true + } + + // List the releases + var list []*rspb.Release + for _, namespace := range namespaces { + releases, err := s.listReleases(namespace) + if err != nil { + s.Log("list: failed to list releases in namespace: %v", namespace, err) + continue + } + + for _, key := range releases { + release, metadata, err := s.readRelease(key, true) + if err != nil { + s.Log("list: failed to read release: %v: %v", release, err) + continue + } + if filter(metadata) { + list = append(list, release) + } + } + } + + if len(list) == 0 { + return nil, ErrReleaseNotFound + } + + return list, nil +} + +// Create creates a new release. +func (s *GCS) Create(key string, rls *rspb.Release) error { + release, err := encodeRelease(rls) + if err != nil { + s.Log("failed to encode release: %v", err) + return err + } + + ctx := context.Background() + obj := s.client.Bucket(s.bucket). + Object(s.fullPathName(key, rls.Namespace)). + If(storage.Conditions{DoesNotExist: true}). + NewWriter(ctx) + obj.Metadata = s.metadata(rls, true) + + if _, err := obj.Write([]byte(release)); err != nil { + s.Log("failed to write object: %v", err) + return err + } + + if err := obj.Close(); err != nil { + switch e := err.(type) { + case *googleapi.Error: + if e.Code == http.StatusPreconditionFailed { + s.Log("release %s already exists", key) + return ErrReleaseExists + } + default: + s.Log("failed to close bucket: %v", err) + return err + } + } + + return nil +} + +// Update updates a release. +func (s *GCS) Update(key string, rls *rspb.Release) error { + release, err := encodeRelease(rls) + if err != nil { + s.Log("failed to encode release: %v", err) + return err + } + + ctx := context.Background() + obj := s.client.Bucket(s.bucket).Object(s.fullPathName(key, rls.Namespace)).NewWriter(ctx) + obj.Metadata = s.metadata(rls, false) + if _, err := obj.Write([]byte(release)); err != nil { + s.Log("failed to write object: %v", err) + return err + } + + if err := obj.Close(); err != nil { + s.Log("failed to close object: %v", err) + return err + } + + return nil +} + +// Delete deletes a release or returns ErrReleaseNotFound. +func (s *GCS) Delete(key string) (*rspb.Release, error) { + ctx := context.Background() + obj := s.client.Bucket(s.bucket).Object(s.fullPathName(key, s.namespace)) + objRdr, err := obj.NewReader(ctx) + if err != nil { + if errors.Is(err, storage.ErrObjectNotExist) { + err = ErrReleaseNotFound + } + return nil, err + } + + record, err := ioutil.ReadAll(objRdr) + objRdr.Close() + if err != nil { + return nil, err + } + + release, err := decodeRelease(string(record)) + if err != nil { + s.Log("get: failed to decode data %q: %v", key, err) + return nil, err + } + + if obj.Delete(ctx); err != nil { + s.Log("failed to delete object: %v", err) + return nil, err + } + + return release, nil +} + +// DeletePrefixedReleases deletes all prefixed releases +func (s *GCS) DeletePrefixedReleases() error { + query := storage.Query{ + Prefix: fmt.Sprintf("%s/", s.pathPrefix), + } + ctx := context.Background() + objs := s.client.Bucket(s.bucket).Objects(ctx, &query) + for { + objAttrs, err := objs.Next() + if err == iterator.Done { + break + } + if err != nil { + s.Log("unable to delete objects", err) + return err + } + obj := s.client.Bucket(s.bucket).Object(objAttrs.Name) + if obj.Delete(ctx); err != nil { + s.Log("failed to delete object: (%s) %v", objAttrs.Name, err) + } + s.Log("object deleted successfully: (%s)", objAttrs.Name) + } + + return nil +} + +func (s *GCS) fullPathName(name, namespace string) string { + if namespace == "" { + namespace = defaultNamespace + } + return strings.TrimLeft( + fmt.Sprintf("%s/%s/%s", s.pathPrefix, namespace, name), + "/", + ) +} + +func (s *GCS) metadata(rls *rspb.Release, isCreation bool) map[string]string { + ts := strconv.FormatInt(time.Now().UTC().UnixNano(), 10) + md := map[string]string{ + gcsReleaseNameMetadata: rls.Name, + gcsReleaseNamespaceMetadata: rls.Namespace, + gcsReleaseStatusMetadata: rls.Info.Status.String(), + gcsReleaseVersionMetadata: strconv.Itoa(rls.Version), + gcsReleaseOwnerColumn: "helm", + gcsReleaseModifiedAtMetadata: ts, + } + if isCreation { + md[gcsReleaseCreatedAtMetadata] = ts + } + return md +} + +func (s *GCS) readRelease(key string, withMetadata bool) (*rspb.Release, map[string]string, error) { + metadata := make(map[string]string) + ctx := context.Background() + objHandle := s.client.Bucket(s.bucket).Object(key) + obj, err := objHandle.NewReader(ctx) + if err != nil { + if errors.Is(err, storage.ErrObjectNotExist) { + err = ErrReleaseNotFound + } + return nil, metadata, err + } + + record, err := ioutil.ReadAll(obj) + obj.Close() + if err != nil { + return nil, metadata, err + } + + release, err := decodeRelease(string(record)) + if err != nil { + s.Log("read release: failed to decode data %q: %v", key, err) + return nil, metadata, err + } + + if withMetadata { + objAttrs, err := objHandle.Attrs(ctx) + if err != nil { + s.Log("read release: failed to read metadata %q: %v", key, err) + return nil, metadata, err + } + metadata = objAttrs.Metadata + } + + return release, metadata, nil +} + +func (s *GCS) listNamespaces() ([]string, error) { + if s.namespace != "" { + return []string{s.namespace}, nil + } + + objectsName := make(map[string]string) + + prefix := fmt.Sprintf("%s/", strings.TrimRight(s.pathPrefix, "/")) + query := storage.Query{ + StartOffset: prefix, + Prefix: prefix, + } + ctx := context.Background() + objs := s.client.Bucket(s.bucket).Objects(ctx, &query) + for { + objAttrs, err := objs.Next() + if err == iterator.Done { + break + } + if err != nil { + s.Log("unable to list objects", err) + return []string{}, err + } + objectName, _, _ := strings.Cut(strings.TrimPrefix(objAttrs.Name, prefix), "/") + objectsName[objectName] = objectName + } + + namespaces := []string{} + for objectName := range objectsName { + namespaces = append(namespaces, objectName) + } + + return namespaces, nil +} + +func (s *GCS) listReleases(namespace string) ([]string, error) { + releases := []string{} + + query := storage.Query{ + Prefix: strings.TrimRight(fmt.Sprintf("%s/%s", s.pathPrefix, namespace), "/"), + } + ctx := context.Background() + objs := s.client.Bucket(s.bucket).Objects(ctx, &query) + for { + objAttrs, err := objs.Next() + if err == iterator.Done { + break + } + if err != nil { + s.Log("unable to list objects", err) + return []string{}, err + } + + releases = append(releases, objAttrs.Name) + } + + return releases, nil +} diff --git a/pkg/storage/driver/gcs_test.go b/pkg/storage/driver/gcs_test.go new file mode 100644 index 000000000..e6bba54c7 --- /dev/null +++ b/pkg/storage/driver/gcs_test.go @@ -0,0 +1,262 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package driver + +import ( + "fmt" + "os" + "reflect" + "testing" + + rspb "helm.sh/helm/v3/pkg/release" +) + +var prefixes = []string{"helm-releases", "helm-releases-list"} + +func TestMain(m *testing.M) { + removeReleases() + + retCode := m.Run() + + // removeReleases() + + os.Exit(retCode) +} + +func removeReleases() { + for _, prefix := range prefixes { + gcsDriver := newTestFixtureGCS(prefix, "default") + if err := gcsDriver.DeletePrefixedReleases(); err != nil { + fmt.Printf("Expected error on setup tests: %v", err) + os.Exit(1) + } + } +} + +func TestGCSName(t *testing.T) { + gcsDriver := newTestFixtureGCS("helm-releases", "default") + if gcsDriver.Name() != GCSDriverName { + t.Errorf("Expected name to be %s, got %s", GCSDriverName, gcsDriver.Name()) + } +} + +func TestGCSGet(t *testing.T) { + vers := int(1) + name := "gcs-test-get" + key := testKey(name, vers) + rel := releaseStub(name, vers, "default", rspb.StatusDeployed) + + gcsDriver := newTestFixtureGCS("helm-releases", "default") + + // create stub + if err := gcsDriver.Create(key, rel); err != nil { + t.Fatalf("failed to create release with key %s: %v", key, err) + } + + // test get release + got, err := gcsDriver.Get(key) + if err != nil { + t.Fatalf("Failed to get release: %v", err) + } + if !reflect.DeepEqual(rel, got) { + t.Errorf("Expected release {%v}, got {%v}", rel, got) + } +} + +func TestGCSGetNotExist(t *testing.T) { + vers := int(1) + name := "gcs-test-get-not-exists" + key := testKey(name, vers) + + gcsDriver := newTestFixtureGCS("helm-releases", "default") + + got, err := gcsDriver.Get(key) + if err == nil || got != nil { + t.Fatal("Release must be not found") + } +} + +func TestGCSCreate(t *testing.T) { + vers := 1 + name := "gcs-test" + key := testKey(name, vers) + rel := releaseStub(name, vers, "default", rspb.StatusDeployed) + + gcsDriver := newTestFixtureGCS("helm-releases", "default") + + if err := gcsDriver.Create(key, rel); err != nil { + t.Fatalf("failed to create release with key %s: %v", key, err) + } +} + +func TestGCSUpdate(t *testing.T) { + vers := 1 + name := "gcs-test-update" + key := testKey(name, vers) + rel := releaseStub(name, vers, "default", rspb.StatusDeployed) + + gcsDriver := newTestFixtureGCS("helm-releases", "default") + + // create stub + if err := gcsDriver.Create(key, rel); err != nil { + t.Fatalf("failed to create release with key %s: %v", key, err) + } + + // test update release + if err := gcsDriver.Update(key, rel); err != nil { + t.Fatalf("failed to update release with key %s: %v", key, err) + } +} + +func TestGCSDelete(t *testing.T) { + vers := 1 + name := "gcs-test-delete" + key := testKey(name, vers) + rel := releaseStub(name, vers, "default", rspb.StatusDeployed) + + gcsDriver := newTestFixtureGCS("helm-releases", "default") + + // create stub + if err := gcsDriver.Create(key, rel); err != nil { + t.Fatalf("failed to create release with key %s: %v", key, err) + } + + // test delete release + deletedRelease, err := gcsDriver.Delete(key) + if err != nil { + t.Fatalf("failed to delete release with key %s: %v", key, err) + } + + if !reflect.DeepEqual(rel, deletedRelease) { + t.Errorf("Expected release {%v}, got {%v}", rel, deletedRelease) + } +} + +func TestGCSDeleteNotFound(t *testing.T) { + vers := 1 + name := "gcs-test-delete-not-found" + key := testKey(name, vers) + + gcsDriver := newTestFixtureGCS("helm-releases", "default") + + if _, err := gcsDriver.Delete(key); err == nil { + t.Fatalf("release found with key %s: %v", key, err) + } +} + +func TestGCSList(t *testing.T) { + namespaceA := "list-a" + namespaceB := "list-b" + + tests := []struct { + key string + namespace string + status rspb.Status + }{ + {"gcs-test-list-key-1", namespaceA, rspb.StatusUninstalled}, + {"gcs-test-list-key-2", namespaceA, rspb.StatusUninstalled}, + {"gcs-test-list-key-3", namespaceA, rspb.StatusDeployed}, + {"gcs-test-list-key-4", namespaceB, rspb.StatusDeployed}, + {"gcs-test-list-key-5", namespaceB, rspb.StatusSuperseded}, + {"gcs-test-list-key-6", namespaceB, rspb.StatusSuperseded}, + } + + gcsDriver := newTestFixtureGCS("helm-releases-list", "") + + // create stubs + for _, tt := range tests { + rel := releaseStub(tt.key, 1, tt.namespace, tt.status) + if err := gcsDriver.Create(tt.key, rel); err != nil { + t.Fatalf("failed to create release with key %s: %v", tt.key, err) + } + } + + // list all deleted releases + del, err := gcsDriver.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusUninstalled + }) + // check + if err != nil { + t.Errorf("Failed to list deleted: %v", err) + } + if len(del) != 2 { + t.Errorf("Expected 2 deleted, got %d:\n%v\n", len(del), del) + } + + // list all deployed releases + dpl, err := gcsDriver.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusDeployed + }) + // check + if err != nil { + t.Errorf("Failed to list deployed: %v", err) + } + if len(dpl) != 2 { + t.Errorf("Expected 2 deployed, got %d:\n%+v\n", len(dpl), dpl) + } + + // list all superseded releases + ssd, err := gcsDriver.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusSuperseded + }) + // check + if err != nil { + t.Errorf("Failed to list superseded: %v", err) + } + if len(ssd) != 2 { + t.Errorf("Expected 2 superseded, got %d:\n%v\n", len(ssd), ssd) + } +} + +func TestGCSQuery(t *testing.T) { + namespace := "default" + tests := []struct { + key string + namespace string + status rspb.Status + }{ + {"gcs-test-list-key-1", namespace, rspb.StatusUninstalled}, + {"gcs-test-list-key-2", namespace, rspb.StatusUninstalled}, + {"gcs-test-list-key-3", namespace, rspb.StatusDeployed}, + {"gcs-test-list-key-4", namespace, rspb.StatusDeployed}, + {"gcs-test-list-key-5", namespace, rspb.StatusSuperseded}, + {"gcs-test-list-key-6", namespace, rspb.StatusSuperseded}, + } + + gcsDriver := newTestFixtureGCS("helm-releases-query", "") + + // create stubs + for _, tt := range tests { + rel := releaseStub(tt.key, 1, tt.namespace, tt.status) + if err := gcsDriver.Create(tt.key, rel); err != nil { + t.Fatalf("failed to create release with key %s: %v", tt.key, err) + } + } + + rls, err := gcsDriver.Query(map[string]string{"status": "deployed"}) + if err != nil { + t.Fatalf("Failed to query: %s", err) + } + if len(rls) != 2 { + t.Fatalf("Expected 2 results, actual %d", len(rls)) + } + + _, err = gcsDriver.Query(map[string]string{"name": "notExist"}) + if err != ErrReleaseNotFound { + t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) + } +} diff --git a/pkg/storage/driver/mock_test.go b/pkg/storage/driver/mock_test.go index c0236ece8..6d2a33c13 100644 --- a/pkg/storage/driver/mock_test.go +++ b/pkg/storage/driver/mock_test.go @@ -21,6 +21,7 @@ import ( "fmt" "testing" + "cloud.google.com/go/storage" sqlmock "github.com/DATA-DOG/go-sqlmock" sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" @@ -263,3 +264,18 @@ func newTestFixtureSQL(t *testing.T, releases ...*rspb.Release) (*SQL, sqlmock.S statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar), }, mock } + +// newTestFixtureGCS mocks the GCS (for testing purposes) +func newTestFixtureGCS(prefix, namespace string) *GCS { + client, _ := storage.NewClient(context.Background()) + + return &GCS{ + client: client, + + bucket: "helm-tests", + pathPrefix: prefix, + namespace: namespace, + + Log: func(a string, b ...interface{}) {}, + } +}