From 2e36993fad932082f99bbab908ac24360d4739a0 Mon Sep 17 00:00:00 2001 From: Tomy GUICHARD Date: Mon, 6 Nov 2023 11:39:10 +0100 Subject: [PATCH 1/2] feat: add sbs-migration tool --- Dockerfile | 2 + cmd/sbs-migration/README.md | 37 +++++++++ cmd/sbs-migration/main.go | 50 ++++++++++++ go.mod | 30 ++++++- go.sum | 94 +++++++++++++++++++++- pkg/migration/migration.go | 151 ++++++++++++++++++++++++++++++++++++ pkg/scaleway/errors.go | 6 ++ pkg/scaleway/helpers.go | 1 + pkg/scaleway/instance.go | 44 +++++++++++ 9 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 cmd/sbs-migration/README.md create mode 100644 cmd/sbs-migration/main.go create mode 100644 pkg/migration/migration.go diff --git a/Dockerfile b/Dockerfile index c4d48e4..275f43d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,9 +15,11 @@ ARG TAG ARG COMMIT_SHA ARG BUILD_DATE RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -ldflags "-w -s -X github.com/scaleway/scaleway-csi/driver.driverVersion=${TAG} -X github.com/scaleway/scaleway-csi/driver.buildDate=${BUILD_DATE} -X github.com/scaleway/scaleway-csi/driver.gitCommit=${COMMIT_SHA} " -o scaleway-csi ./cmd/scaleway-csi +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -ldflags "-w -s -X github.com/scaleway/scaleway-csi/driver.driverVersion=${TAG} -X github.com/scaleway/scaleway-csi/driver.buildDate=${BUILD_DATE} -X github.com/scaleway/scaleway-csi/driver.gitCommit=${COMMIT_SHA} " -o sbs-migration ./cmd/sbs-migration FROM alpine:3.15 RUN apk update && apk add --no-cache e2fsprogs e2fsprogs-extra xfsprogs xfsprogs-extra cryptsetup ca-certificates blkid && update-ca-certificates WORKDIR / COPY --from=builder /go/src/github.com/scaleway/scaleway-csi/scaleway-csi . +COPY --from=builder /go/src/github.com/scaleway/scaleway-csi/sbs-migration . ENTRYPOINT ["/scaleway-csi"] diff --git a/cmd/sbs-migration/README.md b/cmd/sbs-migration/README.md new file mode 100644 index 0000000..9b4b7e2 --- /dev/null +++ b/cmd/sbs-migration/README.md @@ -0,0 +1,37 @@ +# sbs-migration + +The `sbs-migration` tool migrates your Kubernetes `PersistentVolumes` from [Instance](https://console.scaleway.com/instance/volumes) +to the new [Block Storage](https://console.scaleway.com/block-storage/volumes) product. There is no downtime during the migration. +Your Snapshots will also be migrated. + +**You must run this tool during the `v0.{1,2}.X` to `v0.3.X` upgrade of the Scaleway CSI.** + +> [!CAUTION] +> This tool is intended to be used by customers who manage their own Kubernetes cluster. +> +> **DO NOT USE THIS TOOL IF YOUR CLUSTER IS MANAGED BY SCALEWAY (e.g. Kapsule / Kosmos)**. + +## Requirements + +- The kubeconfig of a Kubernetes cluster that you manage, with the Scaleway CSI installed (version `v0.1.X` or `v0.2.X`). +- [Go 1.20+](https://go.dev/dl/). +- Kubectl CLI. + +## Usage + +Please read all the steps before executing any command. + +1. Stop the CSI controller: `$ kubectl scale deployment scaleway-csi-controller -n kube-system --replicas=0`. +2. Set the following environment variables: + + ```bash + export SCW_DEFAULT_ZONE=fr-par-1 + export SCW_DEFAULT_PROJECT_ID=11111111-1111-1111-1111-111111111111 + export SCW_ACCESS_KEY=SCW123456789ABCDE + export SCW_SECRET_KEY=11111111-1111-1111-1111-111111111111 + ``` + +3. Run the `sbs-migration` tool with dry-run enabled: `$ go run cmd/sbs-migration/main.go -kubeconfig= -dry-run`. +4. If you are happy with the dry-run result, run the `sbs-migration` with dry-run disabled + to effectively migrate your volumes and snapshots: `$ go run cmd/sbs-migration/main.go -kubeconfig=`. +5. Upgrade the CSI to `v0.3.1` or higher. diff --git a/cmd/sbs-migration/main.go b/cmd/sbs-migration/main.go new file mode 100644 index 0000000..487cb52 --- /dev/null +++ b/cmd/sbs-migration/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "flag" + "path/filepath" + + "github.com/scaleway/scaleway-csi/pkg/driver" + "github.com/scaleway/scaleway-csi/pkg/migration" + "github.com/scaleway/scaleway-csi/pkg/scaleway" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" + "k8s.io/klog/v2" +) + +func main() { + ctx := context.Background() + + var kubeconfig *string + if home := homedir.HomeDir(); home != "" { + kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") + } else { + kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") + } + dryRun := flag.Bool("dry-run", false, "When set to true, volumes and snapshots will not be migrated") + flag.Parse() + + // Create Kubernetes client. + config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) + if err != nil { + klog.Fatal(err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + klog.Fatal(err) + } + + // Create Scaleway client. + scw, err := scaleway.New(driver.UserAgent()) + if err != nil { + klog.Fatal(err) + } + + // Migrate volumes and snapshots from Instance to Block API. + if err := migration.New(clientset, scw, *dryRun).Do(ctx); err != nil { + klog.Fatal(err) + } +} diff --git a/go.mod b/go.mod index c76955f..f220038 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/scaleway/scaleway-csi go 1.22.2 require ( + github.com/avast/retry-go/v4 v4.6.0 github.com/container-storage-interface/spec v1.9.0 github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 @@ -16,6 +17,8 @@ require ( golang.org/x/sys v0.19.0 google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.33.0 + k8s.io/apimachinery v0.30.0 + k8s.io/client-go v0.30.0 k8s.io/klog/v2 v2.120.1 k8s.io/mount-utils v0.30.0 k8s.io/utils v0.0.0-20240310230437-4693a0247e57 @@ -23,17 +26,42 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/go-logr/logr v1.4.1 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kr/fs v0.1.0 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.24.0 // indirect + golang.org/x/oauth2 v0.17.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.20.0 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.30.0 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index d59b2c4..b5cab07 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA= +github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -9,29 +11,68 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubernetes-csi/csi-test/v5 v5.2.0 h1:Z+sdARWC6VrONrxB24clCLCmnqCnZF7dzXtzx8eM35o= github.com/kubernetes-csi/csi-test/v5 v5.2.0/go.mod h1:o/c5w+NU3RUNE+DbVRhEUTmkQVBGk+tFOB2yPXT8teo= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= @@ -44,32 +85,52 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26 h1:F+GIVtGqCFxPxO46ujf8cEOP574MBoRm3gNbPXECbxs= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -85,34 +146,63 @@ golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= +k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= +k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/mount-utils v0.30.0 h1:EceYTNYVabfpdtIAHC4KgMzoZkm1B8ovZ1J666mYZQI= k8s.io/mount-utils v0.30.0/go.mod h1:9sCVmwGLcV1MPvbZ+rToMDnl1QcGozy+jBPd0MsQLIo= k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oya.to/namedlocker v1.0.0 h1:+HKiEvhKSzBtboeqiT8vK10aVUTASnneKpw+j79FQwA= oya.to/namedlocker v1.0.0/go.mod h1:+eBYtjcKHBxsdm/HAofHTTnSq6H96THcxHYG1A6WKX0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/migration/migration.go b/pkg/migration/migration.go new file mode 100644 index 0000000..9d7b59e --- /dev/null +++ b/pkg/migration/migration.go @@ -0,0 +1,151 @@ +// Package migration handles the migration of volumes and snapshots from the +// Instance API to the new Block API. +package migration + +import ( + "context" + "errors" + "fmt" + "os" + "time" + + "github.com/avast/retry-go/v4" + "github.com/scaleway/scaleway-csi/pkg/driver" + "github.com/scaleway/scaleway-csi/pkg/scaleway" + "github.com/scaleway/scaleway-sdk-go/api/instance/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" +) + +// retryOpts are the default options for retrying requests to Scaleway API. +var retryOpts = []retry.Option{ + retry.RetryIf(scaleway.IsTooManyRequestsError), + retry.Delay(1 * time.Second), +} + +// The Migration struct holds a Kubernetes and Scaleway client. +type Migration struct { + k8s kubernetes.Interface + scw *scaleway.Scaleway + dryRun bool +} + +// New returns a new instance of Migration with the specified k8s/scw clients. +func New(k8s kubernetes.Interface, scw *scaleway.Scaleway, dryRun bool) *Migration { + return &Migration{k8s, scw, dryRun} +} + +// listHandles lists handles of PersistentVolumes managed by the Scaleway CSI driver. +func (m *Migration) listHandles(ctx context.Context) ([]string, error) { + volumes, err := m.k8s.CoreV1().PersistentVolumes().List(ctx, v1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to list k8s PersistentVolumes: %w", err) + } + + var handles []string + + for _, vol := range volumes.Items { + if vol.Spec.CSI != nil && vol.Spec.CSI.Driver == driver.DriverName { + handles = append(handles, vol.Spec.CSI.VolumeHandle) + } + } + + return handles, nil +} + +// migrateHandle migrates a Scaleway volume using the provided handle. If the +// handle is invalid, it is skipped. If the volume does not exist in the Instance +// API, we assume it was already migrated. The first return value is true if the +// volume was effectively migrated. +func (m *Migration) migrateHandle(ctx context.Context, handle string) (bool, error) { + id, zone, err := driver.ExtractIDAndZone(handle) + if err != nil { + // Skip migration if handle is not valid. + klog.Warningf("Failed to extract ID and Zone from handle %q, it will not be migrated: %s", handle, err) + return false, nil + } + + if _, err = retry.DoWithData(func() (*instance.Volume, error) { + return m.scw.GetLegacyVolume(ctx, id, zone) //nolint:wrapcheck + }, retryOpts...); err != nil { + // If legacy volume does not exist, we assume it was already migrated. + if scaleway.IsNotFoundError(err) { + return false, nil + } + + return false, fmt.Errorf("failed to get legacy volume: %w", err) + } + + if err := retry.Do(func() error { + return m.scw.MigrateLegacyVolume(ctx, id, zone, m.dryRun) //nolint:wrapcheck + }, retryOpts...); err != nil { + return false, fmt.Errorf("could not migrate volume with handle %q: %w", handle, err) + } + + return true, nil +} + +// isManagedCluster returns true if a Scaleway managed node is found in the k8s cluster. +func (m *Migration) isManagedCluster(ctx context.Context) (bool, error) { + nodes, err := m.k8s.CoreV1().Nodes().List(ctx, v1.ListOptions{}) + if err != nil { + return false, fmt.Errorf("failed to list nodes: %w", err) + } + + for _, node := range nodes.Items { + if _, ok := node.Labels["k8s.scaleway.com/managed"]; ok { + return true, nil + } + } + + return false, nil +} + +// Do runs the migration of all Scaleway PersistentVolumes from the Instance API +// to the new Block API. +func (m *Migration) Do(ctx context.Context) error { + if m.dryRun { + klog.Infof("Dry run enabled, volumes and snapshots will not be migrated") + } + + // Quick check to make sure this tool is not run on a managed cluster. + if os.Getenv("IGNORE_MANAGED_CLUSTER") == "" { + managed, err := m.isManagedCluster(ctx) + if err != nil { + return fmt.Errorf("failed to check if cluster is managed: %w", err) + } + + if managed { + return errors.New( + "this tool does not supported managed clusters (e.g. Kapsule / Kosmos). " + + "If this is a false alert, you can bypass this verification by setting this " + + "environment variable: IGNORE_MANAGED_CLUSTER=true", + ) + } + } + + handles, err := m.listHandles(ctx) + if err != nil { + return fmt.Errorf("could not list handles: %w", err) + } + + klog.Infof("Found %d Scaleway PersistentVolumes in the cluster", len(handles)) + + for _, handle := range handles { + klog.Infof("Migrating volume with handle %s", handle) + + ok, err := m.migrateHandle(ctx, handle) + if err != nil { + return fmt.Errorf("failed to migrate handle %s: %w", handle, err) + } + + if ok { + klog.Infof("Volume with handle %s was successfully migrated", handle) + } else { + klog.Infof("Volume with handle %s was not migrated", handle) + } + } + + return nil +} diff --git a/pkg/scaleway/errors.go b/pkg/scaleway/errors.go index 2ca10d6..bfa7331 100644 --- a/pkg/scaleway/errors.go +++ b/pkg/scaleway/errors.go @@ -43,3 +43,9 @@ func IsGoneError(err error) bool { var internal *scw.ResponseError return errors.As(err, &internal) && internal.StatusCode == http.StatusGone } + +// IsTooManyRequestsError returns true if an error is a 429 error. +func IsTooManyRequestsError(err error) bool { + var respErr *scw.ResponseError + return errors.As(err, &respErr) && respErr.StatusCode == http.StatusTooManyRequests +} diff --git a/pkg/scaleway/helpers.go b/pkg/scaleway/helpers.go index c52bee9..274550f 100644 --- a/pkg/scaleway/helpers.go +++ b/pkg/scaleway/helpers.go @@ -71,6 +71,7 @@ func min(a, b int) int { } // clientZones returns the zones of the region where the client is configured. +// TODO: be able to handle multiple regions. func clientZones(client *scw.Client) ([]scw.Zone, error) { if defaultZone, ok := client.GetDefaultZone(); ok { region, err := defaultZone.Region() diff --git a/pkg/scaleway/instance.go b/pkg/scaleway/instance.go index 77187aa..56c2eb8 100644 --- a/pkg/scaleway/instance.go +++ b/pkg/scaleway/instance.go @@ -2,7 +2,9 @@ package scaleway import ( "context" + "errors" "fmt" + "time" block "github.com/scaleway/scaleway-sdk-go/api/block/v1alpha1" "github.com/scaleway/scaleway-sdk-go/api/instance/v1" @@ -116,3 +118,45 @@ func (s *Scaleway) GetLegacyVolume(ctx context.Context, volumeID string, zone sc return resp.Volume, nil } + +// MigrateLegacyVolume migrates a volume from the instance API to the SBS API. +func (s *Scaleway) MigrateLegacyVolume(ctx context.Context, volumeID string, zone scw.Zone, dryRun bool) error { + plan, err := s.instance.PlanBlockMigration(&instance.PlanBlockMigrationRequest{ + VolumeID: scw.StringPtr(volumeID), + Zone: zone, + }, scw.WithContext(ctx)) + if err != nil { + return fmt.Errorf("failed to plan block migration: %w", err) + } + + if !dryRun { + if err := s.instance.ApplyBlockMigration(&instance.ApplyBlockMigrationRequest{ + VolumeID: scw.StringPtr(volumeID), + ValidationKey: plan.ValidationKey, + Zone: zone, + }, scw.WithContext(ctx)); err != nil { + return fmt.Errorf("failed to apply block migration: %w", err) + } + + // Wait for volume be effectively migrated. + for range 60 { + time.Sleep(1 * time.Second) + + _, err := s.block.GetVolume(&block.GetVolumeRequest{ + VolumeID: volumeID, + Zone: zone, + }, scw.WithContext(ctx)) + if err == nil { + return nil + } + + if !IsNotFoundError(err) { + return fmt.Errorf("failed to get block volume: %w", err) + } + } + + return errors.New("block volume was not migrated in time") + } + + return nil +} From 3a7745299a0958c4fee8bf6e916c5e7bc47fc56e Mon Sep 17 00:00:00 2001 From: Tomy GUICHARD Date: Fri, 26 Jul 2024 13:00:50 +0200 Subject: [PATCH 2/2] migrate snapshots --- Dockerfile | 2 +- cmd/sbs-migration/main.go | 34 ++++-- go.mod | 1 + go.sum | 2 + pkg/migration/migration.go | 241 +++++++++++++++++++++++++++++-------- pkg/scaleway/errors.go | 6 + pkg/scaleway/instance.go | 80 +++++++++--- 7 files changed, 289 insertions(+), 77 deletions(-) diff --git a/Dockerfile b/Dockerfile index 275f43d..6885df6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22-alpine as builder +FROM golang:1.22-alpine AS builder RUN apk update && apk add --no-cache git ca-certificates && update-ca-certificates diff --git a/cmd/sbs-migration/main.go b/cmd/sbs-migration/main.go index 487cb52..d8eed6d 100644 --- a/cmd/sbs-migration/main.go +++ b/cmd/sbs-migration/main.go @@ -3,27 +3,26 @@ package main import ( "context" "flag" - "path/filepath" "github.com/scaleway/scaleway-csi/pkg/driver" "github.com/scaleway/scaleway-csi/pkg/migration" "github.com/scaleway/scaleway-csi/pkg/scaleway" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/util/homedir" "k8s.io/klog/v2" ) func main() { - ctx := context.Background() - - var kubeconfig *string - if home := homedir.HomeDir(); home != "" { - kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") - } else { - kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") - } - dryRun := flag.Bool("dry-run", false, "When set to true, volumes and snapshots will not be migrated") + var ( + ctx = context.Background() + + // Flags + kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") + disableVolumeMigration = flag.Bool("disable-volume-migration", false, "Disables listing volumes and migrating them") + disableSnapshotMigration = flag.Bool("disable-snapshot-migration", false, "Disables listing snapshots and migrating them") + dryRun = flag.Bool("dry-run", false, "Simulates the volume and snapshot migration process") + ) flag.Parse() // Create Kubernetes client. @@ -37,6 +36,11 @@ func main() { klog.Fatal(err) } + dynClient, err := dynamic.NewForConfig(config) + if err != nil { + klog.Fatal(err) + } + // Create Scaleway client. scw, err := scaleway.New(driver.UserAgent()) if err != nil { @@ -44,7 +48,13 @@ func main() { } // Migrate volumes and snapshots from Instance to Block API. - if err := migration.New(clientset, scw, *dryRun).Do(ctx); err != nil { + opts := &migration.Options{ + DryRun: *dryRun, + DisableVolumeMigration: *disableVolumeMigration, + DisableSnapshotMigration: *disableSnapshotMigration, + } + + if err := migration.New(clientset, dynClient, scw, opts).Do(ctx); err != nil { klog.Fatal(err) } } diff --git a/go.mod b/go.mod index f220038..513e92f 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26 golang.org/x/crypto v0.22.0 golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f + golang.org/x/sync v0.7.0 golang.org/x/sys v0.19.0 google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.33.0 diff --git a/go.sum b/go.sum index b5cab07..7749d8d 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/migration/migration.go b/pkg/migration/migration.go index 9d7b59e..2f4e143 100644 --- a/pkg/migration/migration.go +++ b/pkg/migration/migration.go @@ -12,28 +12,64 @@ import ( "github.com/avast/retry-go/v4" "github.com/scaleway/scaleway-csi/pkg/driver" "github.com/scaleway/scaleway-csi/pkg/scaleway" - "github.com/scaleway/scaleway-sdk-go/api/instance/v1" + "golang.org/x/sync/errgroup" + kerror "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" ) -// retryOpts are the default options for retrying requests to Scaleway API. -var retryOpts = []retry.Option{ - retry.RetryIf(scaleway.IsTooManyRequestsError), - retry.Delay(1 * time.Second), +const ( + maxParallelMigrations = 3 + retryDelay = 1 * time.Second +) + +var ( + volSnapContentRes = schema.GroupVersionResource{Group: "snapshot.storage.k8s.io", Version: "v1", Resource: "volumesnapshotcontents"} + + // retryOpts are the default options for retrying requests to Scaleway API. + retryOpts = []retry.Option{ + retry.RetryIf(isRetryable), + retry.Delay(retryDelay), + } +) + +type Options struct { + DryRun bool + DisableVolumeMigration bool + DisableSnapshotMigration bool } // The Migration struct holds a Kubernetes and Scaleway client. type Migration struct { k8s kubernetes.Interface + k8sDyn *dynamic.DynamicClient scw *scaleway.Scaleway - dryRun bool + opts *Options } // New returns a new instance of Migration with the specified k8s/scw clients. -func New(k8s kubernetes.Interface, scw *scaleway.Scaleway, dryRun bool) *Migration { - return &Migration{k8s, scw, dryRun} +func New(k8s kubernetes.Interface, k8sDyn *dynamic.DynamicClient, scw *scaleway.Scaleway, opts *Options) *Migration { + return &Migration{k8s, k8sDyn, scw, opts} +} + +// isManagedCluster returns true if a Scaleway managed node is found in the k8s cluster. +func (m *Migration) isManagedCluster(ctx context.Context) (bool, error) { + nodes, err := m.k8s.CoreV1().Nodes().List(ctx, v1.ListOptions{}) + if err != nil { + return false, fmt.Errorf("failed to list nodes: %w", err) + } + + for _, node := range nodes.Items { + if _, ok := node.Labels["k8s.scaleway.com/managed"]; ok { + return true, nil + } + } + + return false, nil } // listHandles lists handles of PersistentVolumes managed by the Scaleway CSI driver. @@ -54,11 +90,57 @@ func (m *Migration) listHandles(ctx context.Context) ([]string, error) { return handles, nil } -// migrateHandle migrates a Scaleway volume using the provided handle. If the -// handle is invalid, it is skipped. If the volume does not exist in the Instance -// API, we assume it was already migrated. The first return value is true if the -// volume was effectively migrated. -func (m *Migration) migrateHandle(ctx context.Context, handle string) (bool, error) { +// listSnapshotHandles lists handles of VolumentSnapshotContents managed by the Scaleway CSI driver. +func (m *Migration) listSnapshotHandles(ctx context.Context) ([]string, error) { + volsnapcontents, err := m.k8sDyn.Resource(volSnapContentRes).List(ctx, v1.ListOptions{}) + if err != nil { + if kerror.IsNotFound(err) { + klog.Warningf("Could not list VolumeSnapshotContents, CRD is probably missing: %s", err) + return nil, nil + } + + return nil, fmt.Errorf("failed to list k8s VolumeSnapshotContents: %w", err) + } + + handles := make([]string, 0, len(volsnapcontents.Items)) + + for _, v := range volsnapcontents.Items { + d, ok, err := unstructured.NestedString(v.Object, "spec", "driver") + if err != nil { + return nil, fmt.Errorf("failed to get driver for %s: %w", v.GetName(), err) + } + // Skip snapshots not managed by this driver. + if !ok || d != driver.DriverName { + continue + } + + h, ok, err := unstructured.NestedString(v.Object, "status", "snapshotHandle") + if err != nil { + return nil, fmt.Errorf("failed to get snapshotHandle for %s: %w", v.GetName(), err) + } + // Skip snapshots with missing handle. + if !ok { + continue + } + + handles = append(handles, h) + } + + return handles, nil +} + +type migrateKind string + +const ( + volumeMigrateKind migrateKind = "volume" + snapshotMigrateKind migrateKind = "snapshot" +) + +// migrateHandle migrates a Scaleway volume or snapshot using the provided handle. +// If the handle is invalid, it is skipped. If the volume or snapshot does not exist +// in the Instance API, we assume it was already migrated. The first return value is +// true if the volume or snapshot was effectively migrated. +func (m *Migration) migrateHandle(ctx context.Context, kind migrateKind, handle string) (bool, error) { id, zone, err := driver.ExtractIDAndZone(handle) if err != nil { // Skip migration if handle is not valid. @@ -66,51 +148,107 @@ func (m *Migration) migrateHandle(ctx context.Context, handle string) (bool, err return false, nil } - if _, err = retry.DoWithData(func() (*instance.Volume, error) { - return m.scw.GetLegacyVolume(ctx, id, zone) //nolint:wrapcheck + if err = retry.Do(func() (err error) { + switch kind { + case volumeMigrateKind: + _, err = m.scw.GetLegacyVolume(ctx, id, zone) + case snapshotMigrateKind: + _, err = m.scw.GetLegacySnapshot(ctx, id, zone) + default: + panic(fmt.Sprintf("unknown kind: %s", kind)) + } + return }, retryOpts...); err != nil { - // If legacy volume does not exist, we assume it was already migrated. + // If legacy resource does not exist, we assume it was already migrated. if scaleway.IsNotFoundError(err) { return false, nil } - return false, fmt.Errorf("failed to get legacy volume: %w", err) + return false, fmt.Errorf("failed to get legacy %s: %w", kind, err) } if err := retry.Do(func() error { - return m.scw.MigrateLegacyVolume(ctx, id, zone, m.dryRun) //nolint:wrapcheck + switch kind { + case volumeMigrateKind: + return m.scw.MigrateLegacyVolume(ctx, id, zone, m.opts.DryRun) //nolint:wrapcheck + case snapshotMigrateKind: + return m.scw.MigrateLegacySnapshot(ctx, id, zone, m.opts.DryRun) //nolint:wrapcheck + default: + panic(fmt.Sprintf("unknown kind: %s", kind)) + } }, retryOpts...); err != nil { - return false, fmt.Errorf("could not migrate volume with handle %q: %w", handle, err) + return false, fmt.Errorf("could not migrate %s with handle %q: %w", kind, handle, err) } return true, nil } -// isManagedCluster returns true if a Scaleway managed node is found in the k8s cluster. -func (m *Migration) isManagedCluster(ctx context.Context) (bool, error) { - nodes, err := m.k8s.CoreV1().Nodes().List(ctx, v1.ListOptions{}) +func (m *Migration) migrate(ctx context.Context, kind migrateKind) error { + // List handles. + var ( + handles []string + err error + ) + switch kind { + case snapshotMigrateKind: + handles, err = m.listSnapshotHandles(ctx) + case volumeMigrateKind: + handles, err = m.listHandles(ctx) + default: + panic(fmt.Sprintf("unknown kind: %s", kind)) + } if err != nil { - return false, fmt.Errorf("failed to list nodes: %w", err) + return fmt.Errorf("could not list handles: %w", err) } - for _, node := range nodes.Items { - if _, ok := node.Labels["k8s.scaleway.com/managed"]; ok { - return true, nil + switch kind { + case snapshotMigrateKind: + klog.Infof("Found %d Scaleway VolumeSnapshotContents in the cluster", len(handles)) + case volumeMigrateKind: + klog.Infof("Found %d Scaleway PersistentVolumes in the cluster", len(handles)) + default: + panic(fmt.Sprintf("unknown kind: %s", kind)) + } + + // Run handle migrations in parallel. + eg, ctx := errgroup.WithContext(ctx) + eg.SetLimit(maxParallelMigrations) + + for _, handle := range handles { + select { + case <-ctx.Done(): + default: + eg.Go(func() error { + klog.Infof("Migrating %s with handle %s", kind, handle) + + ok, err := m.migrateHandle(ctx, kind, handle) + if err != nil { + return fmt.Errorf("failed to migrate handle %s: %w", handle, err) + } + + if ok { + klog.Infof("%s with handle %s was successfully migrated", kind, handle) + } else { + klog.Infof("%s with handle %s was not migrated", kind, handle) + } + + return nil + }) } } - return false, nil + if err := eg.Wait(); err != nil { + return fmt.Errorf("%s migration failed: %w", kind, err) + } + + return nil } -// Do runs the migration of all Scaleway PersistentVolumes from the Instance API -// to the new Block API. +// Do runs the migration of all Scaleway PersistentVolumes and VolumeSnapshotContents +// from the Instance API to the new Block API. func (m *Migration) Do(ctx context.Context) error { - if m.dryRun { - klog.Infof("Dry run enabled, volumes and snapshots will not be migrated") - } - // Quick check to make sure this tool is not run on a managed cluster. - if os.Getenv("IGNORE_MANAGED_CLUSTER") == "" { + if os.Getenv("IGNORE_MANAGED_CLUSTER") != "true" { managed, err := m.isManagedCluster(ctx) if err != nil { return fmt.Errorf("failed to check if cluster is managed: %w", err) @@ -125,27 +263,30 @@ func (m *Migration) Do(ctx context.Context) error { } } - handles, err := m.listHandles(ctx) - if err != nil { - return fmt.Errorf("could not list handles: %w", err) - } - - klog.Infof("Found %d Scaleway PersistentVolumes in the cluster", len(handles)) - - for _, handle := range handles { - klog.Infof("Migrating volume with handle %s", handle) + // Migrate volumes first. + if !m.opts.DisableVolumeMigration { + if m.opts.DryRun { + klog.Infof("Dry run enabled, volumes and their associated snapshots will not be migrated") + } - ok, err := m.migrateHandle(ctx, handle) - if err != nil { - return fmt.Errorf("failed to migrate handle %s: %w", handle, err) + if err := m.migrate(ctx, volumeMigrateKind); err != nil { + return err } + } - if ok { - klog.Infof("Volume with handle %s was successfully migrated", handle) - } else { - klog.Infof("Volume with handle %s was not migrated", handle) + // Migrate snapshots. + if !m.opts.DisableSnapshotMigration { + if m.opts.DryRun { + klog.Infof("Dry run enabled, snapshots and their associated volumes will not be migrated") + } + if err := m.migrate(ctx, snapshotMigrateKind); err != nil { + return err } } return nil } + +func isRetryable(err error) bool { + return scaleway.IsTooManyRequestsError(err) || scaleway.IsInternalServerError(err) +} diff --git a/pkg/scaleway/errors.go b/pkg/scaleway/errors.go index bfa7331..96e258a 100644 --- a/pkg/scaleway/errors.go +++ b/pkg/scaleway/errors.go @@ -49,3 +49,9 @@ func IsTooManyRequestsError(err error) bool { var respErr *scw.ResponseError return errors.As(err, &respErr) && respErr.StatusCode == http.StatusTooManyRequests } + +// IsInternalServerError returns true if an error is a 500 error. +func IsInternalServerError(err error) bool { + var respErr *scw.ResponseError + return errors.As(err, &respErr) && respErr.StatusCode == http.StatusInternalServerError +} diff --git a/pkg/scaleway/instance.go b/pkg/scaleway/instance.go index 56c2eb8..7df89a1 100644 --- a/pkg/scaleway/instance.go +++ b/pkg/scaleway/instance.go @@ -119,43 +119,95 @@ func (s *Scaleway) GetLegacyVolume(ctx context.Context, volumeID string, zone sc return resp.Volume, nil } +// GetLegacySnapshot gets an Instance snapshot by its ID and zone. +func (s *Scaleway) GetLegacySnapshot(ctx context.Context, snapshotID string, zone scw.Zone) (*instance.Snapshot, error) { + resp, err := s.instance.GetSnapshot(&instance.GetSnapshotRequest{ + SnapshotID: snapshotID, + Zone: zone, + }, scw.WithContext(ctx)) + if err != nil { + return nil, fmt.Errorf("failed to get legacy snapshot: %w", err) + } + + return resp.Snapshot, nil +} + // MigrateLegacyVolume migrates a volume from the instance API to the SBS API. func (s *Scaleway) MigrateLegacyVolume(ctx context.Context, volumeID string, zone scw.Zone, dryRun bool) error { - plan, err := s.instance.PlanBlockMigration(&instance.PlanBlockMigrationRequest{ + return s.migrateBlock(ctx, &instance.PlanBlockMigrationRequest{ + Zone: zone, VolumeID: scw.StringPtr(volumeID), + }, &instance.ApplyBlockMigrationRequest{ Zone: zone, - }, scw.WithContext(ctx)) + VolumeID: scw.StringPtr(volumeID), + }, func(ctx context.Context) error { + if _, err := s.block.GetVolume(&block.GetVolumeRequest{ + VolumeID: volumeID, + Zone: zone, + }, scw.WithContext(ctx)); err != nil { + return fmt.Errorf("failed to get volume: %w", err) + } + + return nil + }, dryRun) +} + +// MigrateLegacySnapshot migrates a snapshot from the instance API to the SBS API. +func (s *Scaleway) MigrateLegacySnapshot(ctx context.Context, snapshotID string, zone scw.Zone, dryRun bool) error { + return s.migrateBlock(ctx, &instance.PlanBlockMigrationRequest{ + Zone: zone, + SnapshotID: scw.StringPtr(snapshotID), + }, &instance.ApplyBlockMigrationRequest{ + Zone: zone, + SnapshotID: scw.StringPtr(snapshotID), + }, func(ctx context.Context) error { + if _, err := s.block.GetSnapshot(&block.GetSnapshotRequest{ + SnapshotID: snapshotID, + Zone: zone, + }, scw.WithContext(ctx)); err != nil { + return fmt.Errorf("failed to get snapshot: %w", err) + } + + return nil + }, dryRun) +} + +func (s *Scaleway) migrateBlock( + ctx context.Context, + planReq *instance.PlanBlockMigrationRequest, + applyReq *instance.ApplyBlockMigrationRequest, + getter func(ctx context.Context) error, + dryRun bool, +) error { + plan, err := s.instance.PlanBlockMigration(planReq, scw.WithContext(ctx)) if err != nil { return fmt.Errorf("failed to plan block migration: %w", err) } if !dryRun { - if err := s.instance.ApplyBlockMigration(&instance.ApplyBlockMigrationRequest{ - VolumeID: scw.StringPtr(volumeID), - ValidationKey: plan.ValidationKey, - Zone: zone, - }, scw.WithContext(ctx)); err != nil { + applyReq.ValidationKey = plan.ValidationKey + + // ApplyBlockMigration sometimes return a 500 error due to a serialization error, + // caller should simply retry. + if err := s.instance.ApplyBlockMigration(applyReq, scw.WithContext(ctx)); err != nil { return fmt.Errorf("failed to apply block migration: %w", err) } - // Wait for volume be effectively migrated. + // Wait for block to be effectively migrated. for range 60 { time.Sleep(1 * time.Second) - _, err := s.block.GetVolume(&block.GetVolumeRequest{ - VolumeID: volumeID, - Zone: zone, - }, scw.WithContext(ctx)) + err = getter(ctx) if err == nil { return nil } if !IsNotFoundError(err) { - return fmt.Errorf("failed to get block volume: %w", err) + return err } } - return errors.New("block volume was not migrated in time") + return errors.New("block was not migrated in time") } return nil