diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ef453b2..5bacf1c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,8 +2,12 @@ include: - project: 'crusoeenergy/tools' file: '/templates/go.gitlab-ci.yml' +default: + tags: + - gitlab-runner-gcp + variables: - CI_IMAGE: registry.gitlab.com/crusoeenergy/tools/go-ci-1.22 + CI_IMAGE: registry.gitlab.com/crusoeenergy/tools/go-ci-1.23 test_and_lint: rules: @@ -39,4 +43,4 @@ code_intelligence: pages: rules: - - when: never \ No newline at end of file + - when: never diff --git a/.golangci.yml b/.golangci.yml index 8b5b3c8..2111b04 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,7 +9,7 @@ linters-settings: # default is false: such cases aren't reported by default. check-blank: true govet: - check-shadowing: true + enable-all: true gci: sections: - standard @@ -26,9 +26,6 @@ linters-settings: - commentedOutCode whitespace: multi-if: true # Enforces newlines (or comments) after every multi-line if statement - gosec: - global: - audit: enabled # Run extra checks that might be "nosy" gomoddirectives: replace-allow-list: - github.com/crusoecloud/client-go @@ -37,19 +34,18 @@ linters: disable-all: true enable: - asciicheck - - bodyclose + - copyloopvar - cyclop - dogsled - dupl - durationcheck + - err113 - errcheck - errorlint - exhaustive - - exportloopref - forbidigo - forcetypeassert - funlen - - gci - gochecknoinits - gochecknoglobals - gocognit @@ -57,12 +53,9 @@ linters: - gocritic - gocyclo - godot - - goerr113 - - gofmt - gofumpt - goheader - goimports - - gomnd - gomoddirectives - gomodguard - goprintffuncname @@ -73,6 +66,7 @@ linters: - lll - makezero - misspell + - mnd - nakedret - nestif - nilerr @@ -114,5 +108,3 @@ run: # golangci.com configuration # https://github.com/golangci/golangci/wiki/Configuration -service: - golangci-lint-version: 1.50.1 # use a fixed version for consistent results diff --git a/Dockerfile b/Dockerfile index f2a0583..756d212 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # STEP 1: build crusoe-csi-driver binary # ########################################## -FROM golang:1.22.9 AS builder +FROM golang:1.23.3 AS builder ARG CRUSOE_CSI_DRIVER_NAME ENV CRUSOE_CSI_DRIVER_NAME=$CRUSOE_CSI_DRIVER_NAME @@ -10,6 +10,12 @@ ARG CRUSOE_CSI_DRIVER_VERSION ENV CRUSOE_CSI_DRIVER_VERSION=$CRUSOE_CSI_DRIVER_VERSION WORKDIR /build + +COPY go.mod . +COPY go.sum . + +RUN go mod download + COPY . . RUN make cross @@ -17,12 +23,13 @@ RUN make cross ################################################################ # STEP 2: build a small image and run crusoe-csi-driver binary # ################################################################ -FROM alpine +FROM alpine:3.20.3 # Need to get these updates for k8s mount-utils library to work properly RUN apk update && \ - apk add --no-cache e2fsprogs && \ - apk add --no-cache blkid && \ + apk add --no-cache e2fsprogs-extra~=1.47.0 && \ + apk add --no-cache blkid~=2.40.1 && \ + apk add --no-cache xfsprogs-extra~=6.8.0 && \ rm -rf /var/cache/apk/* COPY --from=builder /build/dist/crusoe-csi-driver /usr/local/go/bin/crusoe-csi-driver diff --git a/Makefile b/Makefile index 781b4be..2c38627 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ BUILDDIR := ${PREFIX}/dist # Set any default go build tags BUILDTAGS := -GOLANGCI_VERSION = v1.55.2 +GOLANGCI_VERSION = v1.62.0 GO_ACC_VERSION = latest GOTESTSUM_VERSION = latest GOCOVER_VERSION = latest diff --git a/cmd/crusoe-csi-driver/main.go b/cmd/crusoe-csi-driver/main.go index 924dc0d..bcae318 100644 --- a/cmd/crusoe-csi-driver/main.go +++ b/cmd/crusoe-csi-driver/main.go @@ -3,24 +3,68 @@ package main import ( "fmt" "os" - - "github.com/spf13/cobra" + "strings" "github.com/crusoecloud/crusoe-csi-driver/internal" - "github.com/crusoecloud/crusoe-csi-driver/internal/config" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/thediveo/enumflag/v2" ) -// Start executing the Crusoe CSI driver. -func main() { - rootCmd := &cobra.Command{ - Use: "crusoe-csi-driver", - Short: "Crusoe Container Storage Interface (CSI) driver", - Args: cobra.NoArgs, - RunE: internal.RunDriver, +//nolint:gochecknoglobals // Global command instance +var rootCmd = &cobra.Command{ + Use: "crusoe-csi-driver", + Short: "Crusoe Container Storage Interface (CSI) driver", + RunE: internal.RunMain, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) } - config.AddFlags(rootCmd) - if err := rootCmd.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) +} + +func setFlags() { + var err error + viper.AutomaticEnv() + + // Use underscores in env var names + replacer := strings.NewReplacer("-", "_") + viper.SetEnvKeyReplacer(replacer) + + rootCmd.Flags().String(internal.CrusoeAPIEndpointFlag, internal.CrusoeAPIEndpointDefault, "help for api endpoint") + rootCmd.Flags().String(internal.CrusoeAccessKeyFlag, "", "help for access key") + rootCmd.Flags().String(internal.CrusoeSecretKeyFlag, "", "help for secret key") + rootCmd.Flags().String(internal.CrusoeProjectIDFlag, "", "help for project id") + rootCmd.Flags().Var( + enumflag.New(&internal.SelectedCSIDriverType, + internal.CSIDriverTypeFlag, + internal.CSIDriverTypeNames, + true), + internal.CSIDriverTypeFlag, + "help for driver type") + rootCmd.Flags().Var( + enumflag.NewSlice(&internal.Services, + internal.ServicesFlag, + internal.ServiceTypeNames, + true), + internal.ServicesFlag, + "help for services") + rootCmd.Flags().String(internal.NodeNameFlag, "", "help for kubernetes node name") + rootCmd.Flags().String(internal.SocketAddressFlag, internal.SocketAddressDefault, "help for socket address") + + err = viper.BindPFlags(rootCmd.Flags()) + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } } + +func main() { + setFlags() + Execute() +} diff --git a/go.mod b/go.mod index ea57000..6d79990 100644 --- a/go.mod +++ b/go.mod @@ -1,55 +1,75 @@ module github.com/crusoecloud/crusoe-csi-driver -go 1.22.0 +go 1.23 require ( github.com/antihax/optional v1.0.0 - github.com/container-storage-interface/spec v1.9.0 + github.com/container-storage-interface/spec v1.10.0 github.com/crusoecloud/client-go v0.1.59 - github.com/google/uuid v1.6.0 github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + github.com/thediveo/enumflag/v2 v2.0.5 google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.34.2 k8s.io/apimachinery v0.31.2 k8s.io/client-go v0.31.2 k8s.io/klog/v2 v2.130.1 k8s.io/mount-utils v0.31.2 - k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 ) require ( + github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // 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.4 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // 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/uuid v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/moby/sys/mountinfo v0.7.2 // indirect - github.com/moby/sys/userns v0.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/sys/mountinfo v0.7.1 // 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/opencontainers/runc v1.2.1 // indirect + github.com/opencontainers/runc v1.1.13 // indirect + github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/net v0.30.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/time v0.3.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect - google.golang.org/protobuf v1.35.1 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.31.2 // indirect diff --git a/go.sum b/go.sum index 6d84db4..44290a4 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,9 @@ github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/container-storage-interface/spec v1.9.0 h1:zKtX4STsq31Knz3gciCYCi1SXtO2HJDecIjDVboYavY= -github.com/container-storage-interface/spec v1.9.0/go.mod h1:ZfDu+3ZRyeVqxZM0Ds19MVLkN2d1XJ5MAfi1L3VjlT0= +github.com/container-storage-interface/spec v1.10.0 h1:YkzWPV39x+ZMTa6Ax2czJLLwpryrQ+dPesB34mrRMXA= +github.com/container-storage-interface/spec v1.10.0/go.mod h1:DtUvaQszPml1YJfIK7c00mlv6/g4wNMLanLgiUbKFRI= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crusoecloud/client-go v0.1.59 h1:iI+017cYXQ/z/TJVCliERameEJpx5Tac0nl1tloaLyc= @@ -12,6 +14,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= @@ -23,8 +29,12 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/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/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -41,6 +51,8 @@ github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2 github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= 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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -56,12 +68,14 @@ 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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= -github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/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= @@ -71,46 +85,77 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/opencontainers/runc v1.2.1 h1:mQkmeFSUxqFaVmvIn1VQPeQIKpHFya5R07aJw0DKQa8= -github.com/opencontainers/runc v1.2.1/go.mod h1:/PXzF0h531HTMsYQnmxXkBD7YaGShm/2zcRB79dksUc= +github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= +github.com/onsi/gomega v1.28.1/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= +github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= +github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78 h1:R5M2qXZiK/mWPMT4VldCOiSL9HIAMuxQZWdG0CSM5+4= +github.com/opencontainers/runtime-spec v1.0.3-0.20220909204839-494a5a6aca78/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/thediveo/enumflag/v2 v2.0.5 h1:VJjvlAqUb6m6mxOrB/0tfBJI0Kvi9wJ8ulh38xK87i8= +github.com/thediveo/enumflag/v2 v2.0.5/go.mod h1:0NcG67nYgwwFsAvoQCmezG0J0KaIxZ0f7skg9eLq1DA= +github.com/thediveo/success v1.0.1 h1:NVwUOwKUwaN8szjkJ+vsiM2L3sNBFscldoDJ2g2tAPg= +github.com/thediveo/success v1.0.1/go.mod h1:AZ8oUArgbIsCuDEWrzWNQHdKnPbDOLQsWOFj9ynwLt0= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 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/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= -golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 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/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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -119,16 +164,18 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -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/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -139,17 +186,19 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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= @@ -168,8 +217,8 @@ k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7F k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/mount-utils v0.31.2 h1:Q0ygX92Lj9d1wcObAzj+JZ4oE7CNKZrqSOn1XcIS+y4= k8s.io/mount-utils v0.31.2/go.mod h1:HV/VYBUGqYUj4vt82YltzpWvgv8FPg0G9ItyInT3NPU= -k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 h1:jGnCPejIetjiy2gqaJ5V0NLwTpF4wbQ6cZIItJCSHno= -k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 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= diff --git a/internal/cli.go b/internal/cli.go new file mode 100644 index 0000000..225014e --- /dev/null +++ b/internal/cli.go @@ -0,0 +1,51 @@ +package internal + +import ( + "github.com/thediveo/enumflag/v2" +) + +type ServiceType enumflag.Flag + +const ( + ServiceTypeIdentity ServiceType = iota + ServiceTypeController + ServiceTypeNode +) + +var ServiceTypeNames = map[ServiceType][]string{ //nolint:gochecknoglobals // can't construct const map + ServiceTypeIdentity: {"identity"}, + ServiceTypeController: {"controller"}, + ServiceTypeNode: {"node"}, +} + +var Services = []ServiceType{ServiceTypeIdentity} //nolint:gochecknoglobals // flag variable + +type CSIDriverType enumflag.Flag + +const ( + CSIDriverTypeSSD CSIDriverType = iota + CSIDriverTypeFS +) + +var CSIDriverTypeNames = map[CSIDriverType][]string{ //nolint:gochecknoglobals // can't construct const map + CSIDriverTypeSSD: {"ssd"}, + CSIDriverTypeFS: {"fs"}, +} + +var SelectedCSIDriverType = CSIDriverTypeSSD //nolint:gochecknoglobals // flag variable + +const ( + CrusoeAPIEndpointFlag = "crusoe-api-endpoint" + CrusoeAccessKeyFlag = "crusoe-csi-access-key" + CrusoeSecretKeyFlag = "crusoe-csi-secret-key" //nolint:gosec // false positive, this is a flag name + CrusoeProjectIDFlag = "crusoe-project-id" + CSIDriverTypeFlag = "crusoe-csi-driver-type" + ServicesFlag = "services" + NodeNameFlag = "node-name" + SocketAddressFlag = "socket-address" +) + +const ( + CrusoeAPIEndpointDefault = "https://api.crusoecloud.com/v1alpha5" + SocketAddressDefault = "unix:/tmp/csi.sock" +) diff --git a/internal/common/capabilities.go b/internal/common/capabilities.go new file mode 100644 index 0000000..d5530e8 --- /dev/null +++ b/internal/common/capabilities.go @@ -0,0 +1,93 @@ +package common + +import "github.com/container-storage-interface/spec/lib/go/csi" + +//nolint:gochecknoglobals // can't construct const slice +var BaseIdentityCapabilities = []*csi.PluginCapability{ + { + Type: &csi.PluginCapability_Service_{ + Service: &csi.PluginCapability_Service{ + Type: csi.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS, + }, + }, + }, +} + +//nolint:gochecknoglobals // can't construct const slice +var BaseControllerCapabilities = []*csi.ControllerServiceCapability{ + { + Type: &csi.ControllerServiceCapability_Rpc{ + Rpc: &csi.ControllerServiceCapability_RPC{ + Type: csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME, + }, + }, + }, + { + Type: &csi.ControllerServiceCapability_Rpc{ + Rpc: &csi.ControllerServiceCapability_RPC{ + Type: csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME, + }, + }, + }, + { + Type: &csi.ControllerServiceCapability_Rpc{ + Rpc: &csi.ControllerServiceCapability_RPC{ + Type: csi.ControllerServiceCapability_RPC_EXPAND_VOLUME, + }, + }, + }, + { + Type: &csi.ControllerServiceCapability_Rpc{ + Rpc: &csi.ControllerServiceCapability_RPC{ + Type: csi.ControllerServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER, + }, + }, + }, +} + +//nolint:gochecknoglobals // can't construct const slice +var BaseNodeCapabilities = []*csi.NodeServiceCapability{ + { + Type: &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: csi.NodeServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER, + }, + }, + }, +} + +//nolint:gochecknoglobals // can't construct const struct +var PluginCapabilityControllerService = csi.PluginCapability{ + Type: &csi.PluginCapability_Service_{ + Service: &csi.PluginCapability_Service{ + Type: csi.PluginCapability_Service_CONTROLLER_SERVICE, + }, + }, +} + +//nolint:gochecknoglobals // can't construct const struct +var PluginCapabilityVolumeExpansionOnline = csi.PluginCapability{ + Type: &csi.PluginCapability_VolumeExpansion_{ + VolumeExpansion: &csi.PluginCapability_VolumeExpansion{ + Type: csi.PluginCapability_VolumeExpansion_ONLINE, + }, + }, +} + +//nolint:gochecknoglobals // can't construct const struct +var PluginCapabilityVolumeExpansionOffline = csi.PluginCapability{ + Type: &csi.PluginCapability_VolumeExpansion_{ + VolumeExpansion: &csi.PluginCapability_VolumeExpansion{ + Type: csi.PluginCapability_VolumeExpansion_OFFLINE, + }, + }, +} + +//nolint:gochecknoglobals // can't construct const struct +var NodeCapabilityExpandVolume = csi.NodeServiceCapability{ + Type: &csi.NodeServiceCapability_Rpc{ + Rpc: &csi.NodeServiceCapability_RPC{ + Type: csi.NodeServiceCapability_RPC_EXPAND_VOLUME, + }, + }, +} diff --git a/internal/common/constants.go b/internal/common/constants.go new file mode 100644 index 0000000..3e80e09 --- /dev/null +++ b/internal/common/constants.go @@ -0,0 +1,53 @@ +package common + +import "time" + +// Numeric constants. +const ( + NumBytesInGiB = 1024 * 1024 * 1024 + NumGiBInTiB = 1024 + BlockSizeSSD = 4096 + MinSSDSizeGiB = 1 + MaxSSDSizeGiB = NumGiBInTiB * 10 + MinFSSizeGiB = NumGiBInTiB + MaxFSSizeGiB = NumGiBInTiB * 1000 +) + +// Map keys. +const ( + TopologyLocationKey = "location" + TopologySupportsSharedDisksKey = "supports-shared-disks" + + VolumeContextDiskSerialNumberKey = "csi.crusoe.ai/serial-number" + VolumeContextDiskNameKey = "csi.crusoe.ai/disk-name" +) + +// Enums. +const ( + // DiskTypeSSD and DiskTypeFS names correspond to the Crusoe API enum values. + DiskTypeSSD DiskType = "persistent-ssd" + DiskTypeFS DiskType = "shared-volume" +) + +// Plugin metadata. +const ( + SSDPluginName = "ssd.csi.crusoe.ai" + SSDPluginVersion = "0.1.0" + + FSPluginName = "fs.csi.crusoe.ai" + FSPluginVersion = "0.1.0" +) + +// Runtime options. +const ( + // OperationTimeout is the maximum time the Crusoe CSI driver will wait for an asynchronous operation to complete. + OperationTimeout = 5 * time.Minute + + // MaxVolumesPerNode refers to the maximum number of disks that can be attached to a VM + // ref: https://docs.crusoecloud.com/storage/disks/overview#persistent-disks + MaxVolumesPerNode = 15 + + // MaxDiskNameLength refers to the maximum permissible length of a Crusoe disk name + // ref: https://docs.crusoecloud.com/storage/disks/overview#persistent-disks + MaxDiskNameLength = 63 +) diff --git a/internal/common/errors.go b/internal/common/errors.go new file mode 100644 index 0000000..de9d159 --- /dev/null +++ b/internal/common/errors.go @@ -0,0 +1,8 @@ +package common + +import "errors" + +var ( + ErrNotImplemented = errors.New("not implemented") + ErrTimeout = errors.New("timeout") +) diff --git a/internal/common/types.go b/internal/common/types.go new file mode 100644 index 0000000..6df217a --- /dev/null +++ b/internal/common/types.go @@ -0,0 +1,3 @@ +package common + +type DiskType string diff --git a/internal/common/util.go b/internal/common/util.go new file mode 100644 index 0000000..881a6d5 --- /dev/null +++ b/internal/common/util.go @@ -0,0 +1,188 @@ +package common + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math" + "net/http" + "strings" + "time" + + "github.com/container-storage-interface/spec/lib/go/csi" + + crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" +) + +type OpStatus string + +type OpResultErr struct { + Code string `json:"code"` + Message string `json:"message"` +} + +const ( + PollInterval = 1 * time.Second + OpSucceeded OpStatus = "SUCCEEDED" + OpInProgress OpStatus = "IN_PROGRESS" + OpFailed OpStatus = "FAILED" +) + +const numExpectedComponents = 2 + +var ( + ErrUnableToGetOpRes = errors.New("failed to get result of operation") + ErrUnexpectedOperationState = errors.New("unexpected operation state") + ErrNoSizeRequested = errors.New("no disk size requested") +) + +func CancellableSleep(ctx context.Context, duration time.Duration) error { + t := time.NewTimer(duration) + select { + case <-ctx.Done(): + t.Stop() + + return ErrTimeout + case <-t.C: + } + + return nil +} + +func OpResultToError(res interface{}) (expectedErr, unexpectedErr error) { + b, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("unable to marshal operation error: %w", err) + } + resultError := OpResultErr{} + err = json.Unmarshal(b, &resultError) + if err != nil { + return nil, fmt.Errorf("op result type not error as expected: %w", err) + } + + //nolint:goerr113 // error is intentionally dynamic + return fmt.Errorf("%s", resultError.Message), nil +} + +// AwaitOperation polls an async API operation until it resolves into a success or failure state. +func AwaitOperation(ctx context.Context, op *crusoeapi.Operation, projectID string, + getOp func(ctx context.Context, projectID string, operationID string) (crusoeapi.Operation, *http.Response, error), +) ( + *crusoeapi.Operation, error, +) { + timeoutCtx, cancel := context.WithTimeout(ctx, OperationTimeout) + defer cancel() + + for op.State == string(OpInProgress) { + updatedOp, _, err := getOp(timeoutCtx, projectID, op.OperationId) + if err != nil { + return nil, fmt.Errorf("error getting operation with id %s: %w", op.OperationId, err) + } + + op = &updatedOp + + err = CancellableSleep(timeoutCtx, PollInterval) + if err != nil { + return nil, err + } + } + + switch op.State { + case string(OpSucceeded): + return op, nil + case string(OpFailed): + opError, err := OpResultToError(op.Result) + if err != nil { + return nil, err + } + + return nil, fmt.Errorf("operation failed: %w", opError) + default: + + return nil, fmt.Errorf("%w: %s", ErrUnexpectedOperationState, op.State) + } +} + +func GetAsyncOperationResult[T any](ctx context.Context, op *crusoeapi.Operation, projectID string, + getOp func(ctx context.Context, projectID string, operationID string) (crusoeapi.Operation, *http.Response, error), +) (*T, *crusoeapi.Operation, error) { + completedOp, err := AwaitOperation(ctx, op, projectID, getOp) + if err != nil { + return nil, nil, err + } + + b, err := json.Marshal(completedOp.Result) + if err != nil { + return nil, completedOp, fmt.Errorf("%w: could not marshal operation result: %w", ErrUnableToGetOpRes, err) + } + + var result T + err = json.Unmarshal(b, &result) + if err != nil { + return nil, completedOp, fmt.Errorf("%w: could not unmarshal operation result: %w", ErrUnableToGetOpRes, err) + } + + return &result, completedOp, nil +} + +// UnpackSwaggerErr takes a swagger error and safely attempts to extract the +// additional information which is present in the response. The error +// is returned unchanged if it cannot be unpacked. +func UnpackSwaggerErr(original error) error { + swagErr := &crusoeapi.GenericSwaggerError{} + if ok := errors.As(original, swagErr); !ok { + return original + } + + var model crusoeapi.ErrorBody + err := json.Unmarshal(swagErr.Body(), &model) + if err != nil { + return original + } + + // some error messages are of the format "rpc code = ... desc = ..." + // in those cases, we extract the description and return it + components := strings.Split(model.Message, " desc = ") + if len(components) == numExpectedComponents { + //nolint:goerr113 // error is intentionally dynamic + return fmt.Errorf("%s", components[1]) + } + + //nolint:goerr113 // error is intentionally dynamic + return fmt.Errorf("%s", model.Message) +} + +func RequestSizeToBytes(capacityRange *csi.CapacityRange) (int64, error) { + var requestSizeBytes int64 + + switch { + case capacityRange.GetRequiredBytes() != 0: + requestSizeBytes = capacityRange.GetRequiredBytes() + case capacityRange.GetLimitBytes() != 0: + requestSizeBytes = capacityRange.GetLimitBytes() + default: + return 0, ErrNoSizeRequested + } + + return requestSizeBytes, nil +} + +func RequestSizeToGiB(capacityRange *csi.CapacityRange) (int, error) { + requestSizeBytes, err := RequestSizeToBytes(capacityRange) + if err != nil { + return 0, err + } + + requestSizeGiB := int(math.Ceil(float64(requestSizeBytes) / float64(NumBytesInGiB))) + + return requestSizeGiB, nil +} + +func GetTopologyKey(pluginName, key string) string { + return fmt.Sprintf("%s/%s", pluginName, key) +} + +func TrimPVCPrefix(pvcName string) string { + return strings.TrimPrefix(pvcName, "pvc-") +} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index 31ec3fe..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,21 +0,0 @@ -package config - -import "github.com/spf13/cobra" - -const ( - APIEndpointFlag = "api-endpoint" - APIEndpointDefault = "https://api.crusoecloud.com/v1alpha5" - SocketAddressFlag = "socket-address" - SocketAddressDefault = "unix:/tmp/csi.sock" - ServicesFlag = "services" -) - -// AddFlags attaches the CLI flags the CSI Driver needs to the provided command. -func AddFlags(cmd *cobra.Command) { - cmd.Flags().String(APIEndpointFlag, APIEndpointDefault, - "Crusoe API Endpoint") - cmd.Flags().String(SocketAddressFlag, SocketAddressDefault, - "Socket which the gRPC server will listen on") - cmd.Flags().StringSlice(ServicesFlag, []string{}, - "CSI Driver services to return") -} diff --git a/internal/controller/controller.go b/internal/controller/controller.go new file mode 100644 index 0000000..eab1013 --- /dev/null +++ b/internal/controller/controller.go @@ -0,0 +1,447 @@ +package controller + +import ( + "context" + "errors" + "fmt" + "math" + + "github.com/crusoecloud/crusoe-csi-driver/internal/crusoe" + + "github.com/crusoecloud/crusoe-csi-driver/internal/common" + + "github.com/container-storage-interface/spec/lib/go/csi" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/wrapperspb" + "k8s.io/klog/v2" + + crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" +) + +type DefaultController struct { + csi.UnimplementedControllerServer + CrusoeClient *crusoeapi.APIClient + HostInstance *crusoeapi.InstanceV1Alpha5 + DiskType common.DiskType + PluginName string + PluginVersion string + Capabilities []*csi.ControllerServiceCapability +} + +//nolint:funlen,cyclop // function is already fairly clean +func (d *DefaultController) CreateVolume(ctx context.Context, request *csi.CreateVolumeRequest) ( + *csi.CreateVolumeResponse, + error, +) { + klog.Infof("Received request to create volume: %+v", request) + + err := validateDiskRequest(request, d.DiskType) + if err != nil { + return nil, err // validateDiskRequest returns only status.Errors so we can return the error directly + } + + // Trim the PVC prefix from the request name + // Crusoe Shared Disks have a limit of 36 characters on the name field + request.Name = common.TrimPVCPrefix(request.GetName()) + if len(request.Name) > common.MaxDiskNameLength { + request.Name = request.Name[:common.MaxDiskNameLength] + } + + // Check if a volume already exists with the provided name + existingDisk, err := crusoe.FindDiskByNameFallible(ctx, d.CrusoeClient, d.HostInstance.ProjectId, request.GetName()) + if err != nil { + if !errors.Is(err, crusoe.ErrDiskNotFound) { + return nil, status.Errorf(codes.Internal, "failed to check if disk exists: %s", err) + } + } + + diskLocation, requireSupportsFS := parseRequiredTopology(request, d.DiskType, d.PluginName, d.HostInstance) + if d.DiskType == common.DiskTypeFS && !requireSupportsFS { + return nil, status.Errorf(codes.ResourceExhausted, + "shared disk requested but could not find topology constraint with %s and %s segments", + common.GetTopologyKey(d.PluginName, common.TopologyLocationKey), + common.GetTopologyKey(d.PluginName, common.TopologySupportsSharedDisksKey)) + } + + diskRequest, err := crusoe.GetCreateDiskRequest(request, diskLocation, d.DiskType) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to get create disk request: %s", err) + } + + var disk *crusoeapi.DiskV1Alpha5 + + if existingDisk != nil { + // Check if existing existingDisk matches what we want + if diskMatchErr := crusoe.CheckDiskMatchesRequest(existingDisk, + request, + d.HostInstance.Location, + d.DiskType, + ); diskMatchErr != nil { + // Disk does not match + // To be safe, do not modify or delete existing disk and return error + return nil, status.Errorf(codes.AlreadyExists, + "disk %s already exists but does not match request: %s", + request.GetName(), + diskMatchErr) + } + + klog.Infof("Disk %s already exists, skipping creation", request.GetName()) + + disk = existingDisk + } else { + // Create the disk + op, _, createErr := d.CrusoeClient.DisksApi.CreateDisk(ctx, *diskRequest, d.HostInstance.ProjectId) + if createErr != nil { + return nil, status.Errorf(codes.Internal, "failed to create disk: %s", common.UnpackSwaggerErr(createErr)) + } + + // Get the created disk + newDisk, _, getResultErr := common.GetAsyncOperationResult[crusoeapi.DiskV1Alpha5](ctx, + op.Operation, + d.HostInstance.ProjectId, + d.CrusoeClient.DiskOperationsApi.GetStorageDisksOperation) + if getResultErr != nil { + return nil, status.Errorf(codes.Internal, + "failed to get result of disk creation: %s", + common.UnpackSwaggerErr(getResultErr)) + } + + disk = newDisk + } + + volume, convertDiskErr := crusoe.GetVolumeFromDisk(disk, d.PluginName, diskLocation, d.DiskType) + if convertDiskErr != nil { + return nil, status.Errorf(codes.Internal, "failed to convert crusoe disk to kubernetes volume: %s", convertDiskErr) + } + + klog.Infof("Created volume: %+v", volume) + + return &csi.CreateVolumeResponse{ + Volume: volume, + }, nil +} + +func (d *DefaultController) DeleteVolume(ctx context.Context, + request *csi.DeleteVolumeRequest) ( + *csi.DeleteVolumeResponse, + error, +) { + klog.Infof("Received request to delete volume: %+v", request) + + // Check if the disk exists + existingDisk, err := crusoe.FindDiskByIDFallible(ctx, d.CrusoeClient, d.HostInstance.ProjectId, request.GetVolumeId()) + if errors.Is(err, crusoe.ErrDiskNotFound) { + // Disk does not exist + klog.Infof("Disk %s is already deleted, skipping deletion", request.GetVolumeId()) + + return &csi.DeleteVolumeResponse{}, nil + } else if err != nil { + return nil, status.Errorf(codes.FailedPrecondition, "failed to check if disk exists: %s", err) + } + + if len(existingDisk.AttachedTo) > 0 { + return nil, status.Errorf(codes.FailedPrecondition, + "disk %s is still attached to instance(s): %v", + request.GetVolumeId(), + existingDisk.AttachedTo) + } + + op, _, err := d.CrusoeClient.DisksApi.DeleteDisk(ctx, d.HostInstance.ProjectId, request.GetVolumeId()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete disk: %s", common.UnpackSwaggerErr(err)) + } + + _, awaitErr := common.AwaitOperation(ctx, + op.Operation, + d.HostInstance.ProjectId, + d.CrusoeClient.DiskOperationsApi.GetStorageDisksOperation) + if awaitErr != nil { + return nil, status.Errorf(codes.Internal, + "failed to get result of disk deletion: %s", + common.UnpackSwaggerErr(awaitErr)) + } + + klog.Infof("Deleted volume: %+v", request) + + return &csi.DeleteVolumeResponse{}, nil +} + +func (d *DefaultController) ControllerPublishVolume(ctx context.Context, + request *csi.ControllerPublishVolumeRequest) ( + *csi.ControllerPublishVolumeResponse, + error, +) { + klog.Infof("Received request to publish volume: %+v", request) + + // Check if the disk is already attached to the instance + attached, err := crusoe.CheckDiskAttached(ctx, + d.CrusoeClient, + request.GetVolumeId(), + request.GetNodeId(), + d.HostInstance.ProjectId) + if err != nil { + return nil, status.Errorf(codes.NotFound, "failed to check if disk is attached to instance: %s", err) + } + + if attached { + klog.Infof("Disk %s is already attached to instance %s, skipping publish", request.GetVolumeId(), request.GetNodeId()) + + return &csi.ControllerPublishVolumeResponse{}, nil + } + + accessMode := request.VolumeCapability.GetAccessMode().Mode + mode := "read-write" + if accessMode == csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY || + accessMode == csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY { + + mode = "read-only" + } + + op, _, err := d.CrusoeClient.VMsApi.UpdateInstanceAttachDisks(ctx, crusoeapi.InstancesAttachDiskPostRequestV1Alpha5{ + AttachDisks: []crusoeapi.DiskAttachment{ + { + AttachmentType: "data", + DiskId: request.GetVolumeId(), + Mode: mode, + }, + }, + }, d.HostInstance.ProjectId, request.GetNodeId()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to attach disk: %s", err) + } + + _, err = common.AwaitOperation(ctx, + op.Operation, + d.HostInstance.ProjectId, + d.CrusoeClient.DiskOperationsApi.GetStorageDisksOperation) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get result of disk attachment: %s", err) + } + + klog.Infof("Published volume: %+v", request) + + return &csi.ControllerPublishVolumeResponse{}, nil +} + +func (d *DefaultController) ControllerUnpublishVolume(ctx context.Context, + request *csi.ControllerUnpublishVolumeRequest) ( + *csi.ControllerUnpublishVolumeResponse, + error, +) { + klog.Infof("Received request to unpublish volume: %+v", request) + + // Check if the disk is already detached from the instance + attached, err := crusoe.CheckDiskAttached(ctx, + d.CrusoeClient, + request.GetVolumeId(), + request.GetNodeId(), + d.HostInstance.ProjectId) + if err != nil { + return nil, status.Errorf(codes.NotFound, "failed to check if disk is attached to instance: %s", err) + } + + if !attached { + klog.Infof( + "Disk %s is already detached from instance %s, skipping unpublish", + request.GetVolumeId(), + request.GetNodeId()) + + return &csi.ControllerUnpublishVolumeResponse{}, nil + } + + op, _, err := d.CrusoeClient.VMsApi.UpdateInstanceDetachDisks(ctx, crusoeapi.InstancesDetachDiskPostRequest{ + DetachDisks: []string{ + request.GetVolumeId(), + }, + }, d.HostInstance.ProjectId, request.GetNodeId()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to detach disk: %s", err) + } + + _, err = common.AwaitOperation(ctx, + op.Operation, + d.HostInstance.ProjectId, + d.CrusoeClient.DiskOperationsApi.GetStorageDisksOperation) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get result of disk detachment: %s", err) + } + + klog.Infof("Unpublished volume: %+v", request) + + return &csi.ControllerUnpublishVolumeResponse{}, nil +} + +func (d *DefaultController) ValidateVolumeCapabilities(_ context.Context, + request *csi.ValidateVolumeCapabilitiesRequest) ( + *csi.ValidateVolumeCapabilitiesResponse, + error, +) { + for _, capability := range request.GetVolumeCapabilities() { + err := supportsCapability(capability, d.DiskType) + if err != nil { + //nolint:nilerr // An incompatible volume capability is not an error + return &csi.ValidateVolumeCapabilitiesResponse{Message: err.Error()}, nil + } + } + + return &csi.ValidateVolumeCapabilitiesResponse{ + Confirmed: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{ + VolumeContext: request.GetVolumeContext(), + VolumeCapabilities: request.GetVolumeCapabilities(), + Parameters: request.GetParameters(), + MutableParameters: request.GetMutableParameters(), + }, + }, nil +} + +func (d *DefaultController) ListVolumes(_ context.Context, _ *csi.ListVolumesRequest) ( + *csi.ListVolumesResponse, + error, +) { + return nil, status.Errorf(codes.Unimplemented, "%s: ListVolumes", common.ErrNotImplemented) +} + +func (d *DefaultController) GetCapacity(_ context.Context, _ *csi.GetCapacityRequest) ( + *csi.GetCapacityResponse, + error, +) { + maxSize, minSize := getCapacity(d.DiskType) + + return &csi.GetCapacityResponse{ + // We don't know how much space is available, so return MaxInt64 + AvailableCapacity: math.MaxInt64, + MaximumVolumeSize: wrapperspb.Int64(maxSize), + MinimumVolumeSize: wrapperspb.Int64(minSize), + }, nil +} + +func (d *DefaultController) ControllerGetCapabilities(_ context.Context, _ *csi.ControllerGetCapabilitiesRequest) ( + *csi.ControllerGetCapabilitiesResponse, + error, +) { + return &csi.ControllerGetCapabilitiesResponse{ + Capabilities: d.Capabilities, + }, nil +} + +func (d *DefaultController) CreateSnapshot(_ context.Context, _ *csi.CreateSnapshotRequest) ( + *csi.CreateSnapshotResponse, + error, +) { + return nil, status.Errorf(codes.Unimplemented, "%s: CreateSnapshot", common.ErrNotImplemented) +} + +func (d *DefaultController) DeleteSnapshot(_ context.Context, _ *csi.DeleteSnapshotRequest) ( + *csi.DeleteSnapshotResponse, + error, +) { + return nil, status.Errorf(codes.Unimplemented, "%s: DeleteSnapshot", common.ErrNotImplemented) +} + +func (d *DefaultController) ListSnapshots(_ context.Context, _ *csi.ListSnapshotsRequest) ( + *csi.ListSnapshotsResponse, + error, +) { + return nil, status.Errorf(codes.Unimplemented, "%s: ListSnapshots", common.ErrNotImplemented) +} + +//nolint:cyclop,funlen // error handling +func (d *DefaultController) ControllerExpandVolume(ctx context.Context, request *csi.ControllerExpandVolumeRequest) ( + *csi.ControllerExpandVolumeResponse, + error, +) { + klog.Infof("Received request to expand volume: %+v", request) + + // Find the existing disk + existingDisk, err := crusoe.FindDiskByIDFallible(ctx, d.CrusoeClient, d.HostInstance.ProjectId, request.GetVolumeId()) + if err != nil { + return nil, status.Errorf(codes.NotFound, "failed to find disk: %s", err) + } + + // Only common.DiskTypeFS volumes can be expanded online + if d.DiskType != common.DiskTypeFS && len(existingDisk.AttachedTo) != 0 { + return nil, status.Errorf( + codes.FailedPrecondition, + "volume %s is attached to node %s", + request.GetVolumeId(), + existingDisk.AttachedTo[0]) + } + + existingSizeGiB, err := crusoe.NormalizeDiskSizeToGiB(existingDisk) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to normalize disk size: %s", err) + } + + // We compare the exact requestSizeBytes to min/maxSizeBytes to avoid strange behaviour when rounding + // and to return an accurate error message + requestSizeBytes, err := common.RequestSizeToBytes(request.GetCapacityRange()) + if err != nil { + return nil, status.Errorf(codes.OutOfRange, "failed to get request size: %s", err) + } + requestSizeGiB, err := common.RequestSizeToGiB(request.GetCapacityRange()) + if err != nil { + return nil, status.Errorf(codes.OutOfRange, "failed to get request size: %s", err) + } + + maxSizeBytes, minSizeBytes := getCapacity(d.DiskType) + + if requestSizeBytes > maxSizeBytes { + return nil, status.Errorf(codes.OutOfRange, "%s: maximum size: %d, requested size: %d", + errDiskTooLarge, maxSizeBytes, requestSizeBytes) + } + + if requestSizeBytes < minSizeBytes { + return nil, status.Errorf(codes.OutOfRange, "%s: minimum size: %d, requested size: %d", + errDiskTooSmall, minSizeBytes, requestSizeBytes) + } + + // requestSizeGiB is the actual size that is sent to the Crusoe API in the resize request + existingSizeBytes := int64(existingSizeGiB) * common.NumBytesInGiB + if existingSizeBytes >= requestSizeBytes { + klog.Infof("Disk %s is already at or above the requested size %d GiB, skipping resize", + request.GetVolumeId(), + request.GetCapacityRange().GetRequiredBytes()/common.NumBytesInGiB) + + return &csi.ControllerExpandVolumeResponse{ + CapacityBytes: existingSizeBytes, + NodeExpansionRequired: false, + }, nil + } + + op, _, err := d.CrusoeClient.DisksApi.ResizeDisk(ctx, crusoeapi.DisksPatchRequest{ + Size: fmt.Sprintf("%dGiB", requestSizeGiB), + }, d.HostInstance.ProjectId, request.GetVolumeId()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to resize disk: %s", common.UnpackSwaggerErr(err)) + } + + _, err = common.AwaitOperation(ctx, + op.Operation, + d.HostInstance.ProjectId, + d.CrusoeClient.DiskOperationsApi.GetStorageDisksOperation) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get result of disk resize: %s", common.UnpackSwaggerErr(err)) + } + + klog.Infof("Resized disk %s to %d GiB", request.GetVolumeId(), requestSizeGiB) + + return &csi.ControllerExpandVolumeResponse{ + CapacityBytes: int64(requestSizeGiB) * common.NumBytesInGiB, + NodeExpansionRequired: false, + }, nil +} + +func (d *DefaultController) ControllerGetVolume(_ context.Context, _ *csi.ControllerGetVolumeRequest) ( + *csi.ControllerGetVolumeResponse, + error, +) { + return nil, status.Errorf(codes.Unimplemented, "%s: ControllerGetVolume", common.ErrNotImplemented) +} + +func (d *DefaultController) ControllerModifyVolume(_ context.Context, _ *csi.ControllerModifyVolumeRequest) ( + *csi.ControllerModifyVolumeResponse, + error, +) { + return nil, status.Errorf(codes.Unimplemented, "%s: ControllerModifyVolume", common.ErrNotImplemented) +} diff --git a/internal/controller/util.go b/internal/controller/util.go new file mode 100644 index 0000000..6475889 --- /dev/null +++ b/internal/controller/util.go @@ -0,0 +1,200 @@ +package controller + +import ( + "errors" + "fmt" + "strconv" + + "github.com/crusoecloud/crusoe-csi-driver/internal/common" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/container-storage-interface/spec/lib/go/csi" + + crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" +) + +var ( + //nolint:gochecknoglobals // can't construct const map + ssdAllowedAccessModes = map[csi.VolumeCapability_AccessMode_Mode]struct{}{ + csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER: {}, + csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY: {}, + csi.VolumeCapability_AccessMode_SINGLE_NODE_SINGLE_WRITER: {}, + csi.VolumeCapability_AccessMode_SINGLE_NODE_MULTI_WRITER: {}, + } + //nolint:gochecknoglobals // can't construct const map + fsAllowedAccessModes = map[csi.VolumeCapability_AccessMode_Mode]struct{}{ + csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER: {}, + csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY: {}, + csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY: {}, + csi.VolumeCapability_AccessMode_MULTI_NODE_SINGLE_WRITER: {}, + csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER: {}, + csi.VolumeCapability_AccessMode_SINGLE_NODE_SINGLE_WRITER: {}, + csi.VolumeCapability_AccessMode_SINGLE_NODE_MULTI_WRITER: {}, + } +) + +var ( + errNoSizeRequested = errors.New("no size requested") + errDiskTooSmall = errors.New("disk size too small") + errDiskTooLarge = errors.New("disk size too large") + errUnsupportedAccessMode = errors.New("access mode not supported") + errUnsupportedAccessType = errors.New("access type not supported") +) + +func supportsAccessMode(volumeCapability *csi.VolumeCapability, diskType common.DiskType) bool { + switch diskType { + case common.DiskTypeSSD: + if _, ok := ssdAllowedAccessModes[volumeCapability.GetAccessMode().GetMode()]; ok { + return true + } + case common.DiskTypeFS: + if _, ok := fsAllowedAccessModes[volumeCapability.GetAccessMode().GetMode()]; ok { + return true + } + default: + panic(fmt.Sprintf("unexpected disk type: %s", diskType)) + } + + return false +} + +func supportsAccessType(volumeCapability *csi.VolumeCapability, diskType common.DiskType) bool { + switch diskType { + case common.DiskTypeSSD: + return volumeCapability.GetBlock() != nil || volumeCapability.GetMount() != nil + case common.DiskTypeFS: + return volumeCapability.GetBlock() == nil && volumeCapability.GetMount() != nil + default: + panic(fmt.Sprintf("unexpected disk type: %s", diskType)) + } +} + +func supportsCapability(volumeCapability *csi.VolumeCapability, diskType common.DiskType) error { + supportsMode := supportsAccessMode(volumeCapability, diskType) + supportsType := supportsAccessType(volumeCapability, diskType) + + if !supportsMode { + return status.Errorf( + codes.InvalidArgument, + "%s: %s", + errUnsupportedAccessMode, + volumeCapability.GetAccessMode().GetMode()) + } + + if !supportsType { + var accessType string + + switch { + case volumeCapability.GetMount() != nil: + accessType = "mount" + case volumeCapability.GetBlock() != nil: + accessType = "block" + default: + accessType = "unknown" + } + + return status.Errorf(codes.InvalidArgument, "%s: %s", errUnsupportedAccessType, accessType) + } + + return nil +} + +//nolint:gocritic // don't combine parameter types +func getCapacity(diskType common.DiskType) (maxSize int64, minSize int64) { + switch diskType { + case common.DiskTypeSSD: + maxSize = common.MaxSSDSizeGiB * common.NumBytesInGiB + minSize = common.MinSSDSizeGiB * common.NumBytesInGiB + case common.DiskTypeFS: + maxSize = common.MaxFSSizeGiB * common.NumBytesInGiB + minSize = common.MinFSSizeGiB * common.NumBytesInGiB + default: + panic(fmt.Sprintf("unexpected disk type: %s", diskType)) + } + + return maxSize, minSize +} + +func validateDiskRequest(request *csi.CreateVolumeRequest, diskType common.DiskType) error { + capacityRange := request.GetCapacityRange() + if capacityRange == nil { + return status.Errorf(codes.InvalidArgument, "%s", errNoSizeRequested) + } + + requestedSizeBytes, err := common.RequestSizeToBytes(request.GetCapacityRange()) + if err != nil { + return status.Errorf(codes.InvalidArgument, "%s", err) + } + + maxSize, minSize := getCapacity(diskType) + if requestedSizeBytes > maxSize { + return status.Errorf(codes.OutOfRange, + "%s: maximum size: %d, requested size: %d", + errDiskTooLarge, + maxSize, + requestedSizeBytes) + } + + if requestedSizeBytes < minSize { + return status.Errorf(codes.OutOfRange, + "%s: minimum size: %d, requested size: %d", + errDiskTooSmall, + minSize, + requestedSizeBytes) + } + + for _, capability := range request.GetVolumeCapabilities() { + capabilityErr := supportsCapability(capability, diskType) + if capabilityErr != nil { + return status.Errorf(codes.InvalidArgument, "%s", capabilityErr) + } + } + + return nil +} + +//nolint:cyclop // not that complex +func parseRequiredTopology(request *csi.CreateVolumeRequest, + diskType common.DiskType, + pluginName string, + hostInstance *crusoeapi.InstanceV1Alpha5) ( + location string, + requireSupportsFS bool, +) { + // All provisioned volumes should be accessible from a single topology segment + + switch diskType { + case common.DiskTypeSSD: + // If the request is for a persistent disk, we can ignore the "supports-shared-disks" topology key + // and get the first segment with a location + var ok bool + for _, topology := range request.GetAccessibilityRequirements().GetRequisite() { + if location, ok = topology.Segments[common.GetTopologyKey(pluginName, common.TopologyLocationKey)]; ok { + return location, requireSupportsFS + } + } + + // Otherwise, we default to the location of the controller + return hostInstance.Location, requireSupportsFS + case common.DiskTypeFS: + // If the request is for a shared disk, we require a segment with + // a location and a "supports-shared-disks" topology key + + //nolint:lll // long names + for _, topology := range request.GetAccessibilityRequirements().GetRequisite() { + segmentLocation, locationOk := topology.Segments[common.GetTopologyKey(pluginName, common.TopologyLocationKey)] + segmentSupportsFS, supportsFSOk := topology.Segments[common.GetTopologyKey(pluginName, common.TopologySupportsSharedDisksKey)] + segmentSupportsFSBool, parseErr := strconv.ParseBool(segmentSupportsFS) + if locationOk && supportsFSOk && parseErr == nil && segmentSupportsFSBool { + return segmentLocation, segmentSupportsFSBool + } + } + + // We did not find a topology segment with a location and a "supports-shared-disks" topology key + return "", false + default: + panic(fmt.Sprintf("unexpected disk type: %s", diskType)) + } +} diff --git a/internal/crusoe-csi-driver.go b/internal/crusoe-csi-driver.go deleted file mode 100644 index 6f1511b..0000000 --- a/internal/crusoe-csi-driver.go +++ /dev/null @@ -1,218 +0,0 @@ -package internal - -import ( - "context" - "errors" - "fmt" - "io/fs" - "net" - "net/url" - "os" - "os/signal" - "sync" - "syscall" - - "github.com/spf13/cobra" - "google.golang.org/grpc" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/klog/v2" - - crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" - "github.com/crusoecloud/crusoe-csi-driver/internal/config" - "github.com/crusoecloud/crusoe-csi-driver/internal/driver" -) - -const ( - maxRetries = 10 - retryIntervalSeconds = 5 - identityArg = "identity" - controllerArg = "controller" - nodeArg = "node" -) - -var ( - errAccessKeyEmpty = errors.New("access key is empty") - errSecretKeyEmpty = errors.New("secret key is empty") - errNoServicesProvided = errors.New("cannot initialize CSI driver with no services") -) - -type service interface { - Init(apiClient *crusoeapi.APIClient, driver *driver.Config, services []driver.Service) error - RegisterServer(srv *grpc.Server) error -} - -// RunDriver starts up and runs the Crusoe Cloud CSI Driver. -// -//nolint:funlen,cyclop // a lot of statements here because all set up is done here, already factored -func RunDriver(cmd *cobra.Command, _ /*args*/ []string) error { - // Listen for interrupt signals. - interrupt := make(chan os.Signal, 1) - // Ctrl-C - signal.Notify(interrupt, os.Interrupt) - // this is what docker sends when shutting down a container - signal.Notify(interrupt, syscall.SIGTERM) - var wg sync.WaitGroup - wg.Add(1) - ctx, cancel := context.WithCancel(context.Background()) - go func() { - select { - case <-ctx.Done(): - return - - case <-interrupt: - wg.Done() - cancel() - } - }() - - requestedServices, accessKey, secretKey, socketAddress, apiEndpoint, parseErr := parseAndValidateArguments(cmd) - if parseErr != nil { - return fmt.Errorf("failed to parse arguments: %w", parseErr) - } - - // get endpoint from flags - endpointURL, err := url.Parse(socketAddress) - if err != nil { - return fmt.Errorf("failed to parse socket address (%s): %w", endpointURL, err) - } - - listener, listenErr := startListener(endpointURL) - if listenErr != nil { - return fmt.Errorf("failed to start listener on provided socket url: %w", listenErr) - } - - klog.Infof("Started listener on: %s (scheme: %s)", endpointURL.Path, endpointURL.Scheme) - - srv := grpc.NewServer() - - var grpcServers []service - for _, grpcSrvc := range requestedServices { - switch grpcSrvc { - case driver.ControllerService: - grpcServers = append(grpcServers, driver.NewControllerServer()) - case driver.NodeService: - grpcServers = append(grpcServers, driver.NewNodeServer()) - case driver.IdentityService: - grpcServers = append(grpcServers, driver.NewIdentityServer()) - } - } - - if len(grpcServers) == 0 { - return errNoServicesProvided - } - - apiClient := driver.NewAPIClient(apiEndpoint, accessKey, secretKey, - fmt.Sprintf("%s/%s", driver.GetVendorName(), driver.GetVendorVersion())) - - kubeClientConfig, err := rest.InClusterConfig() - if err != nil { - return fmt.Errorf("could not get kube client config: %w", err) - } - - kubeClient, err := kubernetes.NewForConfig(kubeClientConfig) - if err != nil { - return fmt.Errorf("could not get kube client: %w", err) - } - - instanceID, projectID, location, err := driver.GetInstanceInfo(ctx, apiClient, kubeClient) - if err != nil { - return fmt.Errorf("failed to get instance id of node: %w", err) - } - klog.Infof("Found instance id of node: %s", instanceID) - - crusoeDriver := &driver.Config{ - VendorName: driver.GetVendorName(), - VendorVersion: driver.GetVendorVersion(), - NodeID: instanceID, - NodeProject: projectID, - NodeLocation: location, - } - - // Initialize gRPC services and register with the gRPC servers - for _, server := range grpcServers { - initErr := server.Init(apiClient, crusoeDriver, requestedServices) - if initErr != nil { - return fmt.Errorf("failed to initialize server: %w", initErr) - } - - registerErr := server.RegisterServer(srv) - if registerErr != nil { - return fmt.Errorf("failed to register server: %w", registerErr) - } - } - - go func() { - err = srv.Serve(listener) - }() - - wg.Wait() - - srv.GracefulStop() - - return nil -} - -//nolint:gocritic,cyclop // there are a lot of returned variables here because we parse all args here -func parseAndValidateArguments(cmd *cobra.Command) ( - requestedServices []driver.Service, - accessKey, secretKey, socketAddress, apiEndpoint string, - err error, -) { - accessKey = driver.GetCrusoeAccessKey() - if accessKey == "" { - return nil, "", "", "", "", errAccessKeyEmpty - } - secretKey = driver.GetCrusoeSecretKey() - if secretKey == "" { - return nil, "", "", "", "", errSecretKeyEmpty - } - - services, err := cmd.Flags().GetStringSlice(config.ServicesFlag) - if err != nil { - return nil, "", "", "", "", - fmt.Errorf("failed to get services flag: %w", err) - } - requestedServices = []driver.Service{} - for _, reqService := range services { - switch reqService { - case identityArg: - requestedServices = append(requestedServices, driver.IdentityService) - case controllerArg: - requestedServices = append(requestedServices, driver.ControllerService) - case nodeArg: - requestedServices = append(requestedServices, driver.NodeService) - default: - //nolint:goerr113 // use dynamic errors for more informative error handling - return nil, "", "", "", "", - fmt.Errorf("received unknown service type: %s", reqService) - } - } - socketAddress, err = cmd.Flags().GetString(config.SocketAddressFlag) - if err != nil { - return nil, "", "", "", "", - fmt.Errorf("failed to get socket address flag: %w", err) - } - apiEndpoint, err = cmd.Flags().GetString(config.APIEndpointFlag) - if err != nil { - return nil, "", "", "", "", - fmt.Errorf("failed to get api endpoint flag: %w", err) - } - - return requestedServices, accessKey, secretKey, socketAddress, apiEndpoint, nil -} - -func startListener(endpointURL *url.URL) (net.Listener, error) { - removeErr := os.Remove(endpointURL.Path) - if removeErr != nil { - if !errors.Is(removeErr, fs.ErrNotExist) { - return nil, fmt.Errorf("failed to remove socket file %s: %w", endpointURL.Path, removeErr) - } - } - listener, listenErr := net.Listen(endpointURL.Scheme, endpointURL.Path) - if listenErr != nil { - return nil, fmt.Errorf("failed to start listener on provided socket url: %w", listenErr) - } - - return listener, nil -} diff --git a/internal/driver/auth.go b/internal/crusoe/auth.go similarity index 96% rename from internal/driver/auth.go rename to internal/crusoe/auth.go index a4d0e0a..83d46a3 100644 --- a/internal/driver/auth.go +++ b/internal/crusoe/auth.go @@ -1,4 +1,4 @@ -package driver +package crusoe import ( "crypto/hmac" @@ -18,9 +18,9 @@ import ( // AuthenticatingTransport is a struct implementing http.Roundtripper // that authenticates a request to Crusoe Cloud before sending it out. type AuthenticatingTransport struct { + http.RoundTripper keyID string secretKey string - http.RoundTripper } func NewAuthenticatingTransport(r http.RoundTripper, keyID, secretKey string) AuthenticatingTransport { @@ -182,8 +182,8 @@ func encodeQuery(values map[string][]string) string { return buf.String() } -// NewAPIClient initializes a new Crusoe API client with the given configuration. -func NewAPIClient(host, key, secret, userAgent string) *crusoeapi.APIClient { +// NewCrusoeClient initializes a new Crusoe API client with the given configuration. +func NewCrusoeClient(host, key, secret, userAgent string) *crusoeapi.APIClient { cfg := crusoeapi.NewConfiguration() cfg.UserAgent = userAgent cfg.BasePath = host diff --git a/internal/crusoe/disk.go b/internal/crusoe/disk.go new file mode 100644 index 0000000..16286cf --- /dev/null +++ b/internal/crusoe/disk.go @@ -0,0 +1,205 @@ +package crusoe + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/container-storage-interface/spec/lib/go/csi" + crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" + "github.com/crusoecloud/crusoe-csi-driver/internal/common" +) + +var ( + ErrUnknownDiskSizeSuffix = errors.New("unknown disk size suffix") + + ErrDiskNotFound = errors.New("disk not found") + ErrDiskDifferentSize = errors.New("disk has different size") + ErrDiskDifferentName = errors.New("disk has different name") + ErrDiskDifferentLocation = errors.New("disk has different location") + ErrDiskDifferentBlockSize = errors.New("disk has different block size") + ErrDiskDifferentType = errors.New("disk has different type") +) + +func NormalizeDiskSizeToGiB(disk *crusoeapi.DiskV1Alpha5) (int, error) { + if strings.HasSuffix(disk.Size, "GiB") { + sizeGiB, err := strconv.Atoi(strings.TrimSuffix(disk.Size, "GiB")) + if err != nil { + return 0, fmt.Errorf("failed to parse disk size: %w", err) + } + + return sizeGiB, nil + } else if strings.HasSuffix(disk.Size, "TiB") { + sizeTiB, err := strconv.Atoi(strings.TrimSuffix(disk.Size, "TiB")) + if err != nil { + return 0, fmt.Errorf("failed to parse disk size: %w", err) + } + + return sizeTiB * common.NumGiBInTiB, nil + } + + return 0, fmt.Errorf("%w: %s", ErrUnknownDiskSizeSuffix, disk.Size) +} + +func FindDiskByNameFallible(ctx context.Context, + crusoeClient *crusoeapi.APIClient, + projectID string, + name string, +) (*crusoeapi.DiskV1Alpha5, error) { + disks, _, listErr := crusoeClient.DisksApi.ListDisks(ctx, projectID) + if listErr != nil { + return nil, fmt.Errorf("failed to list disks: %w", common.UnpackSwaggerErr(listErr)) + } + + // indexing is used to avoid a copy + for i := range disks.Items { + currDisk := disks.Items[i] + if currDisk.Name == name { + return &currDisk, nil + } + } + + return nil, ErrDiskNotFound +} + +func FindDiskByIDFallible(ctx context.Context, + crusoeClient *crusoeapi.APIClient, + projectID string, + diskID string, +) (*crusoeapi.DiskV1Alpha5, error) { + disks, _, listErr := crusoeClient.DisksApi.ListDisks(ctx, projectID) + if listErr != nil { + return nil, fmt.Errorf("failed to list disks: %w", common.UnpackSwaggerErr(listErr)) + } + + // indexing is used to avoid a copy + for i := range disks.Items { + currDisk := disks.Items[i] + if currDisk.Id == diskID { + return &currDisk, nil + } + } + + return nil, ErrDiskNotFound +} + +func GetCreateDiskRequest(request *csi.CreateVolumeRequest, + location string, + diskType common.DiskType, +) (*crusoeapi.DisksPostRequestV1Alpha5, error) { + requestSizeGiB, err := common.RequestSizeToGiB(request.GetCapacityRange()) + if err != nil { + return nil, fmt.Errorf("failed to parse request size: %w", err) + } + + var blockSize int64 + + if diskType == common.DiskTypeSSD { + blockSize = common.BlockSizeSSD // TODO: Support different block sizes + } + + return &crusoeapi.DisksPostRequestV1Alpha5{ + BlockSize: blockSize, + Location: location, + Name: request.GetName(), + Size: fmt.Sprintf("%dGiB", requestSizeGiB), + Type_: string(diskType), + }, nil +} + +func CheckDiskMatchesRequest(disk *crusoeapi.DiskV1Alpha5, + request *csi.CreateVolumeRequest, + expectedLocation string, + expectedType common.DiskType, +) error { + if disk.Name != request.GetName() { + // This should never happen because we fetch the disk by name + return ErrDiskDifferentName + } + + // TODO: Support different block sizes + if disk.Type_ == string(common.DiskTypeSSD) && disk.BlockSize != common.BlockSizeSSD { + return ErrDiskDifferentBlockSize + } + + diskSizeGiB, err := NormalizeDiskSizeToGiB(disk) + if err != nil { + return err + } + requestSizeGiB, err := common.RequestSizeToGiB(request.GetCapacityRange()) + if err != nil { + return fmt.Errorf("failed to parse request size: %w", err) + } + + if diskSizeGiB != requestSizeGiB { + return ErrDiskDifferentSize + } + + if disk.Location != expectedLocation { + return ErrDiskDifferentLocation + } + + if disk.Type_ != string(expectedType) { + return ErrDiskDifferentType + } + + return nil +} + +func GetVolumeFromDisk(disk *crusoeapi.DiskV1Alpha5, + + pluginName, + location string, + diskType common.DiskType) ( + *csi.Volume, + error, +) { + diskSizeGiB, err := NormalizeDiskSizeToGiB(disk) + if err != nil { + return nil, fmt.Errorf("failed to parse disk size: %w", err) + } + + segments := map[string]string{ + fmt.Sprintf("%s/location", pluginName): location, + } + + if diskType == common.DiskTypeFS { + segments[common.GetTopologyKey(pluginName, common.TopologySupportsSharedDisksKey)] = strconv.FormatBool(true) + } + + return &csi.Volume{ + CapacityBytes: int64(common.NumBytesInGiB) * int64(diskSizeGiB), + VolumeId: disk.Id, + VolumeContext: map[string]string{ + common.VolumeContextDiskSerialNumberKey: disk.SerialNumber, + common.VolumeContextDiskNameKey: disk.Name, + }, + AccessibleTopology: []*csi.Topology{ + { + Segments: segments, + }, + }, + }, nil +} + +func CheckDiskAttached(ctx context.Context, + crusoeClient *crusoeapi.APIClient, + diskID, + instanceID, + projectID string, +) (bool, error) { + instance, _, err := crusoeClient.VMsApi.GetInstance(ctx, projectID, instanceID) + if err != nil { + return false, fmt.Errorf("failed to get instance: %w", err) + } + + for i := range instance.Disks { + if instance.Disks[i].Id == diskID { + return true, nil + } + } + + return false, nil +} diff --git a/internal/driver/controller.go b/internal/driver/controller.go deleted file mode 100644 index 589f059..0000000 --- a/internal/driver/controller.go +++ /dev/null @@ -1,313 +0,0 @@ -package driver - -import ( - "context" - "errors" - - "github.com/container-storage-interface/spec/lib/go/csi" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "k8s.io/klog/v2" - - crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" -) - -//nolint:gochecknoglobals // we will use this slice to determine what the controller service supports -var controllerServerCapabilities = []csi.ControllerServiceCapability_RPC_Type{ - csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME, - csi.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME, - csi.ControllerServiceCapability_RPC_EXPAND_VOLUME, -} - -const diskUnsatisfactoryMsg = "disk does not satisfied the required capability" - -var ( - errRPCUnimplemented = errors.New("this RPC is currently not implemented") - errExpandVolumeWithSmallerSize = errors.New("disk currently has larger size than expand volume request") -) - -type ControllerServer struct { - apiClient *crusoeapi.APIClient - driver *Config -} - -func NewControllerServer() *ControllerServer { - return &ControllerServer{} -} - -func (c *ControllerServer) Init(apiClient *crusoeapi.APIClient, driver *Config, _ []Service) error { - c.driver = driver - c.apiClient = apiClient - - return nil -} - -func (c *ControllerServer) RegisterServer(srv *grpc.Server) error { - csi.RegisterControllerServer(srv, c) - - return nil -} - -func (c *ControllerServer) CreateVolume(ctx context.Context, - request *csi.CreateVolumeRequest, -) (*csi.CreateVolumeResponse, error) { - klog.Infof("Received request to create volume: %+v", request) - - capabilities := request.GetVolumeCapabilities() - if capErr := validateVolumeCapabilities(capabilities); capErr != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid volume capabilities: %s", capErr.Error()) - } - - reqCapacity := parseCapacity(request.GetCapacityRange()) - createReq, err := getCreateDiskRequest(request.GetName(), reqCapacity, c.driver.GetNodeLocation(), - capabilities, request.GetParameters()) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid arguments to create volume: %s", err.Error()) - } - - // We will check if a disk already exists with the provided name - foundDisk, findErr := findDisk(ctx, c.apiClient, c.driver.GetNodeProject(), request.GetName()) - if findErr != nil { - return nil, status.Errorf(codes.FailedPrecondition, - "failed to validate disk if disk already exists: %s", findErr.Error()) - } - var disk *crusoeapi.DiskV1Alpha5 - // If a disk already exists, make sure that it lines up with what we want - if foundDisk != nil { - verifyErr := verifyExistingDisk(foundDisk, createReq) - if verifyErr != nil { - return nil, status.Errorf(codes.AlreadyExists, - "failed to validate disk if disk already exists: %s", verifyErr.Error()) - } - disk = foundDisk - } else { - // Create the disk if it does not already exist - createdDisk, createErr := createDisk(ctx, c.apiClient, c.driver.GetNodeProject(), createReq) - if createErr != nil { - return nil, status.Errorf(codes.Internal, "failed to create disk: %s", createErr.Error()) - } - disk = createdDisk - } - - volume, parseErr := getVolumeFromDisk(disk) - if parseErr != nil { - return nil, status.Errorf(codes.Internal, - "failed to convert crusoe disk to csi volume: %s", parseErr.Error()) - } - - klog.Infof("Successfully created volume with name: %s and capacity: %s", request.GetName(), reqCapacity) - - return &csi.CreateVolumeResponse{ - Volume: volume, - }, nil -} - -func (c *ControllerServer) ControllerExpandVolume(ctx context.Context, - request *csi.ControllerExpandVolumeRequest, -) (*csi.ControllerExpandVolumeResponse, error) { - klog.Infof("Received request to expand volume: %+v", request) - capacityRange := request.GetCapacityRange() - - reqCapacity := parseCapacity(capacityRange) - - disk, getErr := getDisk(ctx, c.apiClient, c.driver.GetNodeProject(), request.GetVolumeId()) - if getErr != nil { - return nil, status.Errorf(codes.FailedPrecondition, "failed to get existing disk: %s", - getErr.Error()) - } - - if disk.Size > reqCapacity { - return nil, status.Errorf(codes.InvalidArgument, "invalid expand volume request: %s", - errExpandVolumeWithSmallerSize.Error()) - } - - patchReq := &crusoeapi.DisksPatchRequest{ - Size: reqCapacity, - } - - volumeID := request.GetVolumeId() - - updatedDisk, updateErr := updateDisk(ctx, c.apiClient, c.driver.GetNodeProject(), volumeID, patchReq) - if updateErr != nil { - return nil, updateErr - } - - newBytes, err := convertStorageUnitToBytes(updatedDisk.Size) - if err != nil { - return nil, err - } - - klog.Infof("Successfully expanded volume with ID: %s", request.GetVolumeId()) - - return &csi.ControllerExpandVolumeResponse{ - CapacityBytes: newBytes, - NodeExpansionRequired: false, - }, nil -} - -func (c *ControllerServer) DeleteVolume(ctx context.Context, - request *csi.DeleteVolumeRequest, -) (*csi.DeleteVolumeResponse, error) { - err := deleteDisk(ctx, c.apiClient, c.driver.GetNodeProject(), request.GetVolumeId()) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to delete disk: %s", err.Error()) - } - - return &csi.DeleteVolumeResponse{}, nil -} - -func (c *ControllerServer) ControllerPublishVolume(ctx context.Context, - request *csi.ControllerPublishVolumeRequest, -) (*csi.ControllerPublishVolumeResponse, error) { - klog.Infof("Received request to publish volume: %+v", request) - diskID := request.GetVolumeId() - instanceID := request.GetNodeId() - attachmentMode, err := getAttachmentTypeFromVolumeCapability(request.GetVolumeCapability()) - if err != nil { - return nil, status.Errorf(codes.InvalidArgument, "received unexpected capability: %s", err.Error()) - } - - attachment := crusoeapi.DiskAttachment{ - AttachmentType: dataDiskAttachmentType, - DiskId: diskID, - Mode: attachmentMode, - } - - attachReq := &crusoeapi.InstancesAttachDiskPostRequestV1Alpha5{ - AttachDisks: []crusoeapi.DiskAttachment{attachment}, - } - - attachErr := attachDisk(ctx, c.apiClient, c.driver.GetNodeProject(), instanceID, attachReq) - if attachErr != nil { - return nil, status.Errorf(codes.Internal, "failed to attach disk to node: %s", attachErr.Error()) - } - - klog.Infof("Successfully published volume with ID: %s to node: %s", - request.GetVolumeId(), request.GetNodeId()) - - return &csi.ControllerPublishVolumeResponse{ - PublishContext: nil, - }, nil -} - -func (c *ControllerServer) ControllerUnpublishVolume(ctx context.Context, - request *csi.ControllerUnpublishVolumeRequest, -) (*csi.ControllerUnpublishVolumeResponse, error) { - klog.Infof("Received request to unpublish volume: %+v", request) - diskID := request.GetVolumeId() - instanceID := request.GetNodeId() - - detachReq := &crusoeapi.InstancesDetachDiskPostRequest{ - DetachDisks: []string{diskID}, - } - - detachErr := detachDisk(ctx, c.apiClient, c.driver.GetNodeProject(), instanceID, detachReq) - if detachErr != nil { - return nil, status.Errorf(codes.Internal, "failed to detach disk from vm: %s", detachErr.Error()) - } - - return &csi.ControllerUnpublishVolumeResponse{}, nil -} - -func (c *ControllerServer) ValidateVolumeCapabilities(ctx context.Context, - request *csi.ValidateVolumeCapabilitiesRequest, -) (*csi.ValidateVolumeCapabilitiesResponse, error) { - klog.Infof("Received request to validate volume capabilities: %+v", request) - capabilities := request.GetVolumeCapabilities() - if capErr := validateVolumeCapabilities(capabilities); capErr != nil { - return nil, status.Errorf(codes.InvalidArgument, "invalid volume capabilities: %s", capErr.Error()) - } - - disk, getErr := getDisk(ctx, c.apiClient, c.driver.GetNodeProject(), request.GetVolumeId()) - if getErr != nil { - return nil, status.Errorf(codes.FailedPrecondition, "failed to get existing disk %s", getErr.Error()) - } - - desiredType := getDiskTypeFromVolumeType(capabilities) - // as part of the CSI specification, if the set of capabilities is not supported, the Confirmed field of the - // response should be empty – when Confirmed is empty, we can optionally include a message for K8s to report - // why the capabilities are unsupported - if desiredType != disk.Type_ { - return &csi.ValidateVolumeCapabilitiesResponse{ - Message: diskUnsatisfactoryMsg, - }, nil - } - - return &csi.ValidateVolumeCapabilitiesResponse{ - Confirmed: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{ - VolumeContext: request.GetVolumeContext(), - VolumeCapabilities: request.GetVolumeCapabilities(), - Parameters: request.GetParameters(), - }, - }, nil -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (c *ControllerServer) ListVolumes(_ context.Context, - _ *csi.ListVolumesRequest, -) (*csi.ListVolumesResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (c *ControllerServer) ControllerGetVolume(_ context.Context, - _ *csi.ControllerGetVolumeRequest, -) (*csi.ControllerGetVolumeResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (c *ControllerServer) GetCapacity(_ context.Context, - _ *csi.GetCapacityRequest, -) (*csi.GetCapacityResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (c *ControllerServer) CreateSnapshot(_ context.Context, - _ *csi.CreateSnapshotRequest, -) (*csi.CreateSnapshotResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (c *ControllerServer) DeleteSnapshot(_ context.Context, - _ *csi.DeleteSnapshotRequest, -) (*csi.DeleteSnapshotResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (c *ControllerServer) ListSnapshots(_ context.Context, - _ *csi.ListSnapshotsRequest, -) (*csi.ListSnapshotsResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (c *ControllerServer) ControllerModifyVolume(_ context.Context, - _ *csi.ControllerModifyVolumeRequest, -) (*csi.ControllerModifyVolumeResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -func (c *ControllerServer) ControllerGetCapabilities(_ context.Context, - _ *csi.ControllerGetCapabilitiesRequest, -) (*csi.ControllerGetCapabilitiesResponse, error) { - controllerCapabilities := make([]*csi.ControllerServiceCapability, 0, len(controllerServerCapabilities)) - - for _, capability := range controllerServerCapabilities { - controllerCapabilities = append(controllerCapabilities, &csi.ControllerServiceCapability{ - Type: &csi.ControllerServiceCapability_Rpc{ - Rpc: &csi.ControllerServiceCapability_RPC{ - Type: capability, - }, - }, - }) - } - - return &csi.ControllerGetCapabilitiesResponse{ - Capabilities: controllerCapabilities, - }, nil -} diff --git a/internal/driver/controller_util.go b/internal/driver/controller_util.go deleted file mode 100644 index 8409e45..0000000 --- a/internal/driver/controller_util.go +++ /dev/null @@ -1,355 +0,0 @@ -package driver - -import ( - "context" - "errors" - "fmt" - "strconv" - - "github.com/container-storage-interface/spec/lib/go/csi" - - crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" -) - -const ( - BytesInGiB = 1024 * 1024 * 1024 - BytesInTiB = 1024 * 1024 * 1024 * 1024 - blockVolumeDiskType = "persistent-ssd" - mountVolumeDiskType = "shared-volume" - dataDiskAttachmentType = "data" - readOnlyDiskMode = "read-only" - readWriteDiskMode = "read-write" - BlockSizeParam = "csi.crusoe.ai/block-size" -) - -var ( - errUnsupportedVolumeAccessMode = errors.New("unsupported access mode for volume") - - errUnexpectedVolumeCapability = errors.New("unknown volume capability") - errDiskDifferentSize = errors.New("disk has different size") - errDiskDifferentName = errors.New("disk has different name") - errDiskDifferentLocation = errors.New("disk has different location") - errDiskDifferentBlockSize = errors.New("disk has different block size") - errDiskDifferentType = errors.New("disk has different type") - errUnsupportedMountAccessMode = errors.New("unsupported access mode for mount volume") - errUnsupportedBlockAccessMode = errors.New("unsupported access mode for block volume") - errNoCapabilitiesSpecified = errors.New("neither block nor mount capability specified") - errBlockAndMountSpecified = errors.New("both block and mount capabilities specified") - errInvalidBlockSize = errors.New("invalid block size specified: must be 512 or 4096") - - //nolint:gochecknoglobals // use this map to determine what capabilities are supported - supportedBlockVolumeAccessMode = map[csi.VolumeCapability_AccessMode_Mode]struct{}{ - csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER: {}, - csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY: {}, - csi.VolumeCapability_AccessMode_SINGLE_NODE_SINGLE_WRITER: {}, - } - //nolint:gochecknoglobals // use this map to determine what capabilities are supported - supportedMountVolumeAccessMode = map[csi.VolumeCapability_AccessMode_Mode]struct{}{ - csi.VolumeCapability_AccessMode_SINGLE_NODE_MULTI_WRITER: {}, - } -) - -func createDisk(ctx context.Context, apiClient *crusoeapi.APIClient, - projectID string, createReq *crusoeapi.DisksPostRequestV1Alpha5, -) (*crusoeapi.DiskV1Alpha5, error) { - dataResp, httpResp, err := apiClient.DisksApi.CreateDisk(ctx, *createReq, projectID) - if err != nil { - return nil, fmt.Errorf("failed to start a create disk operation: %w", err) - } - defer httpResp.Body.Close() - - disk, _, err := awaitOperationAndResolve[crusoeapi.DiskV1Alpha5](ctx, dataResp.Operation, projectID, - apiClient.DiskOperationsApi.GetStorageDisksOperation) - if err != nil { - return nil, fmt.Errorf("failed to create disk: %w", err) - } - - return disk, nil -} - -func attachDisk(ctx context.Context, apiClient *crusoeapi.APIClient, projectID, vmID string, - attachReq *crusoeapi.InstancesAttachDiskPostRequestV1Alpha5, -) error { - dataResp, httpResp, err := apiClient.VMsApi.UpdateInstanceAttachDisks(ctx, *attachReq, projectID, vmID) - if err != nil { - return fmt.Errorf("failed to start an attach disk operation: %w", err) - } - defer httpResp.Body.Close() - - _, err = awaitOperation(ctx, dataResp.Operation, projectID, - apiClient.VMOperationsApi.GetComputeVMsInstancesOperation) - if err != nil { - return fmt.Errorf("failed to attach disk: %w", err) - } - - return nil -} - -func detachDisk(ctx context.Context, apiClient *crusoeapi.APIClient, projectID, vmID string, - detachReq *crusoeapi.InstancesDetachDiskPostRequest, -) error { - dataResp, httpResp, err := apiClient.VMsApi.UpdateInstanceDetachDisks(ctx, *detachReq, projectID, vmID) - if err != nil { - return fmt.Errorf("failed to start a detach disk operation: %w", err) - } - defer httpResp.Body.Close() - - _, err = awaitOperation(ctx, dataResp.Operation, projectID, - apiClient.VMOperationsApi.GetComputeVMsInstancesOperation) - if err != nil { - return fmt.Errorf("failed to detach disk: %w", err) - } - - return nil -} - -func updateDisk(ctx context.Context, apiClient *crusoeapi.APIClient, - projectID, diskID string, updateReq *crusoeapi.DisksPatchRequest, -) (*crusoeapi.DiskV1Alpha5, error) { - dataResp, httpResp, err := apiClient.DisksApi.ResizeDisk(ctx, *updateReq, projectID, diskID) - if err != nil { - return nil, fmt.Errorf("failed to start a create disk operation: %w", err) - } - defer httpResp.Body.Close() - - disk, _, err := awaitOperationAndResolve[crusoeapi.DiskV1Alpha5](ctx, dataResp.Operation, projectID, - apiClient.DiskOperationsApi.GetStorageDisksOperation) - if err != nil { - return nil, fmt.Errorf("failed to create disk: %w", err) - } - - return disk, nil -} - -func deleteDisk(ctx context.Context, apiClient *crusoeapi.APIClient, projectID, diskID string) error { - dataResp, httpResp, err := apiClient.DisksApi.DeleteDisk(ctx, projectID, diskID) - if err != nil { - return fmt.Errorf("failed to start a delete disk operation: %w", err) - } - defer httpResp.Body.Close() - - _, err = awaitOperation(ctx, dataResp.Operation, projectID, apiClient.DiskOperationsApi.GetStorageDisksOperation) - if err != nil { - return fmt.Errorf("failed to delete disk: %w", err) - } - - return nil -} - -func findDisk(ctx context.Context, apiClient *crusoeapi.APIClient, - projectID, name string, -) (*crusoeapi.DiskV1Alpha5, error) { - disks, httpResp, listErr := apiClient.DisksApi.ListDisks(ctx, projectID) - if listErr != nil { - return nil, fmt.Errorf("error checking if volume exists: %w", listErr) - } - defer httpResp.Body.Close() - var foundDisk *crusoeapi.DiskV1Alpha5 - for i := range disks.Items { - currDisk := disks.Items[i] - if currDisk.Name == name { - foundDisk = &currDisk - - break - } - } - - return foundDisk, nil -} - -func getDisk(ctx context.Context, apiClient *crusoeapi.APIClient, - projectID, diskID string, -) (*crusoeapi.DiskV1Alpha5, error) { - disk, httpResp, listErr := apiClient.DisksApi.GetDisk(ctx, projectID, diskID) - if listErr != nil { - return nil, fmt.Errorf("error checking if volume exists: %w", listErr) - } - defer httpResp.Body.Close() - - return &disk, nil -} - -func convertStorageUnitToBytes(storageStr string) (int64, error) { - valueStr := storageStr[:len(storageStr)-3] - unit := storageStr[len(storageStr)-3:] - - value, err := strconv.Atoi(valueStr) - if err != nil { - return 0, fmt.Errorf("invalid numeric value: %w", err) - } - - var totalBytes int64 - switch unit { - case "GiB": - totalBytes = int64(value * BytesInGiB) - case "TiB": - totalBytes = int64(value * BytesInTiB) - default: - //nolint:goerr113 // use dynamic errors for more informative error handling - return 0, fmt.Errorf("received invalid unit: %s", unit) - } - - return totalBytes, nil -} - -// convertBytesToStorageUnit converts bytes to a specified unit (GiB or TiB) and returns the result as a string. -func convertBytesToStorageUnit(bytes int64) string { - var size int64 - var unit string - - if unitsTiB := bytes / BytesInTiB; unitsTiB > 1 { - size = unitsTiB - unit = "TiB" - } else { - size = bytes / BytesInGiB - unit = "GiB" - } - - return fmt.Sprintf("%d%s", size, unit) -} - -func getVolumeFromDisk(disk *crusoeapi.DiskV1Alpha5) (*csi.Volume, error) { - volBytes, err := convertStorageUnitToBytes(disk.Size) - if err != nil { - return nil, fmt.Errorf("failed to parse disk storage: %w", err) - } - - // The disk is only attachable to instances in its location - accessibleTopology := &csi.Topology{ - Segments: map[string]string{ - TopologyLocationKey: disk.Location, - }, - } - - volumeContext := map[string]string{ - VolumeContextDiskTypeKey: disk.Type_, - VolumeContextDiskSerialNumberKey: disk.SerialNumber, - } - - return &csi.Volume{ - CapacityBytes: volBytes, - VolumeId: disk.Id, - VolumeContext: volumeContext, - ContentSource: nil, - AccessibleTopology: []*csi.Topology{accessibleTopology}, - }, nil -} - -//nolint:cyclop // complexity comes from argument validation -func validateVolumeCapabilities(capabilities []*csi.VolumeCapability) error { - for _, capability := range capabilities { - if capability.GetBlock() != nil && capability.GetMount() != nil { - return errBlockAndMountSpecified - } - if capability.GetBlock() == nil && capability.GetMount() == nil { - return errNoCapabilitiesSpecified - } - - accessMode := capability.GetAccessMode().GetMode() - if capability.GetBlock() != nil { - if _, ok := supportedBlockVolumeAccessMode[accessMode]; !ok { - return fmt.Errorf("%w: %s", errUnsupportedBlockAccessMode, accessMode) - } - } - if capability.GetMount() != nil { - _, mountOk := supportedMountVolumeAccessMode[accessMode] - _, blockOk := supportedBlockVolumeAccessMode[accessMode] - - // mount volumes can do everything block can too - if !blockOk && !mountOk { - return fmt.Errorf("%w: %s", errUnsupportedMountAccessMode, accessMode) - } - } - } - - return nil -} - -func getDiskTypeFromVolumeType(_ []*csi.VolumeCapability) string { - // TODO: support shared filesystem volumes - return blockVolumeDiskType -} - -func parseAndValidateBlockSize(strBlockSize string) (int64, error) { - parsedBlockSize, err := strconv.Atoi(strBlockSize) - if err != nil { - return 0, fmt.Errorf("invalid block size argument: %w", err) - } - if parsedBlockSize != 512 && parsedBlockSize != 4096 { - return 0, errInvalidBlockSize - } - - return int64(parsedBlockSize), nil -} - -func getCreateDiskRequest(name, capacity, location string, - capabilities []*csi.VolumeCapability, optionalParameters map[string]string, -) (*crusoeapi.DisksPostRequestV1Alpha5, error) { - params := &crusoeapi.DisksPostRequestV1Alpha5{ - Name: name, - Size: capacity, - Location: location, - } - if blockSize, ok := optionalParameters[BlockSizeParam]; ok { - parsedBlockSize, err := parseAndValidateBlockSize(blockSize) - if err != nil { - return nil, fmt.Errorf("failed to validate block size: %w", err) - } - params.BlockSize = parsedBlockSize - } - - params.Type_ = getDiskTypeFromVolumeType(capabilities) - - return params, nil -} - -func verifyExistingDisk(currentDisk *crusoeapi.DiskV1Alpha5, createReq *crusoeapi.DisksPostRequestV1Alpha5) error { - if currentDisk.Size != createReq.Size { - return errDiskDifferentSize - } - if currentDisk.Name != createReq.Name { - return errDiskDifferentName - } - if currentDisk.Location != createReq.Location { - return errDiskDifferentLocation - } - if currentDisk.BlockSize != createReq.BlockSize { - return errDiskDifferentBlockSize - } - if currentDisk.Type_ != createReq.Type_ { - return errDiskDifferentType - } - - return nil -} - -func parseCapacity(capacityRange *csi.CapacityRange) string { - // Note: both RequiredBytes and LimitBytes SHOULD be set to the same value, - // however, it is only guaranteed that one of them is set. - reqBytes := capacityRange.GetRequiredBytes() - if reqBytes == 0 { - reqBytes = capacityRange.GetLimitBytes() - } - reqCapacity := convertBytesToStorageUnit(reqBytes) - - return reqCapacity -} - -func getAttachmentTypeFromVolumeCapability(capability *csi.VolumeCapability) (string, error) { - accessMode := capability.GetAccessMode().GetMode() - switch accessMode { - case csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, - csi.VolumeCapability_AccessMode_MULTI_NODE_SINGLE_WRITER, - csi.VolumeCapability_AccessMode_SINGLE_NODE_SINGLE_WRITER, - csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, - csi.VolumeCapability_AccessMode_SINGLE_NODE_MULTI_WRITER: - return readWriteDiskMode, nil - case csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY, - csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY: - return readOnlyDiskMode, nil - case csi.VolumeCapability_AccessMode_UNKNOWN: - return "", errUnexpectedVolumeCapability - } - - return "", fmt.Errorf("%w: %s", errUnsupportedVolumeAccessMode, accessMode.String()) -} diff --git a/internal/driver/driver.go b/internal/driver/driver.go deleted file mode 100644 index 4bc2d25..0000000 --- a/internal/driver/driver.go +++ /dev/null @@ -1,57 +0,0 @@ -package driver - -type Config struct { - // These should be consistent regardless of which node the driver is running on. - VendorName string - VendorVersion string - // These are initialized on a per-node unique basis - NodeID string - NodeLocation string - NodeProject string -} - -type Service int - -const ( - NodeService Service = iota - IdentityService - ControllerService -) - -// Note: these are injected during build -// This name MUST correspond with the name provided to the storage class -// This is how Kubernetes knows to invoke our CSI. -// -//nolint:gochecknoglobals // we will use these global vars to identify the name and version of the CSI -var ( - name string - version string -) - -func GetVendorName() string { - return name -} - -func GetVendorVersion() string { - return version -} - -func (d *Config) GetName() string { - return d.VendorName -} - -func (d *Config) GetVendorVersion() string { - return d.VendorVersion -} - -func (d *Config) GetNodeID() string { - return d.NodeID -} - -func (d *Config) GetNodeProject() string { - return d.NodeProject -} - -func (d *Config) GetNodeLocation() string { - return d.NodeLocation -} diff --git a/internal/driver/identity.go b/internal/driver/identity.go deleted file mode 100644 index 780da43..0000000 --- a/internal/driver/identity.go +++ /dev/null @@ -1,81 +0,0 @@ -package driver - -import ( - "context" - - "github.com/container-storage-interface/spec/lib/go/csi" - "google.golang.org/grpc" - - crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" -) - -type IdentityServer struct { - apiClient *crusoeapi.APIClient - driver *Config - capabilities []*csi.PluginCapability -} - -func NewIdentityServer() *IdentityServer { - return &IdentityServer{} -} - -func (i *IdentityServer) Init(apiClient *crusoeapi.APIClient, driver *Config, services []Service) error { - i.driver = driver - i.apiClient = apiClient - i.capabilities = []*csi.PluginCapability{ - { - Type: &csi.PluginCapability_VolumeExpansion_{ - VolumeExpansion: &csi.PluginCapability_VolumeExpansion{ - Type: csi.PluginCapability_VolumeExpansion_OFFLINE, - }, - }, - }, - { - Type: &csi.PluginCapability_Service_{ - Service: &csi.PluginCapability_Service{ - Type: csi.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS, - }, - }, - }, - } - for _, service := range services { - if service == ControllerService { - i.capabilities = append(i.capabilities, &csi.PluginCapability{ - Type: &csi.PluginCapability_Service_{ - Service: &csi.PluginCapability_Service{ - Type: csi.PluginCapability_Service_CONTROLLER_SERVICE, - }, - }, - }) - } - } - - return nil -} - -func (i *IdentityServer) RegisterServer(srv *grpc.Server) error { - csi.RegisterIdentityServer(srv, i) - - return nil -} - -func (i *IdentityServer) GetPluginInfo(_ context.Context, - _ *csi.GetPluginInfoRequest, -) (*csi.GetPluginInfoResponse, error) { - return &csi.GetPluginInfoResponse{ - Name: i.driver.GetName(), - VendorVersion: i.driver.GetVendorVersion(), - }, nil -} - -func (i *IdentityServer) GetPluginCapabilities(_ context.Context, - _ *csi.GetPluginCapabilitiesRequest, -) (*csi.GetPluginCapabilitiesResponse, error) { - return &csi.GetPluginCapabilitiesResponse{ - Capabilities: i.capabilities, - }, nil -} - -func (i *IdentityServer) Probe(_ context.Context, _ *csi.ProbeRequest) (*csi.ProbeResponse, error) { - return &csi.ProbeResponse{}, nil -} diff --git a/internal/driver/node.go b/internal/driver/node.go deleted file mode 100644 index 1776140..0000000 --- a/internal/driver/node.go +++ /dev/null @@ -1,181 +0,0 @@ -package driver - -import ( - "context" - "errors" - "fmt" - - "github.com/container-storage-interface/spec/lib/go/csi" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "k8s.io/klog/v2" - "k8s.io/mount-utils" - "k8s.io/utils/exec" - - crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" -) - -const ( - // MaxVolumesPerNode refers to the maximum number of disks that can be attached to a VM - // ref: https://docs.crusoecloud.com/storage/disks/overview#persistent-disks - MaxVolumesPerNode = 16 - TopologyLocationKey = "topology.csi.crusoe.ai/location" - TopologyProjectKey = "topology.csi.crusoe.ai/project-id" - VolumeContextDiskSerialNumberKey = "serial-number" - VolumeContextDiskTypeKey = "disk-type" - ReadOnlyMountOption = "ro" - newDirPerms = 0o755 // this represents: rwxr-xr-x - newFilePerms = 0o644 // this represents: rw-r--r-- -) - -var errVolumeMissingSerialNumber = errors.New("volume missing serial number context key") - -//nolint:gochecknoglobals // we will use this slice to determine what the node service supports -var NodeServerCapabilities = []csi.NodeServiceCapability_RPC_Type{ - csi.NodeServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER, - csi.NodeServiceCapability_RPC_EXPAND_VOLUME, -} - -type NodeServer struct { - apiClient *crusoeapi.APIClient - driver *Config - mounter *mount.SafeFormatAndMount -} - -func NewNodeServer() *NodeServer { - return &NodeServer{} -} - -func (n *NodeServer) Init(apiClient *crusoeapi.APIClient, driver *Config, _ []Service) error { - n.driver = driver - n.apiClient = apiClient - n.mounter = mount.NewSafeFormatAndMount(mount.New(""), exec.New()) - - return nil -} - -func (n *NodeServer) RegisterServer(srv *grpc.Server) error { - csi.RegisterNodeServer(srv, n) - - return nil -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (n *NodeServer) NodeStageVolume(_ context.Context, - _ *csi.NodeStageVolumeRequest, -) (*csi.NodeStageVolumeResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (n *NodeServer) NodeUnstageVolume(_ context.Context, - _ *csi.NodeUnstageVolumeRequest, -) (*csi.NodeUnstageVolumeResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -func (n *NodeServer) NodePublishVolume(_ context.Context, - req *csi.NodePublishVolumeRequest, -) (*csi.NodePublishVolumeResponse, error) { - klog.Infof("Received request to publish volume: %+v", req) - targetPath := req.GetTargetPath() - readOnly := req.GetReadonly() - - volumeCapability := req.GetVolumeCapability() - - var mountOpts []string - - if readOnly { - mountOpts = append(mountOpts, ReadOnlyMountOption) - } - - // Check if volume is already mounted, if it is return success - mounted, err := n.mounter.IsMountPoint(targetPath) - if err == nil && mounted { - return &csi.NodePublishVolumeResponse{}, nil - } - - if volumeCapability.GetBlock() != nil { - mountErr := publishBlockVolume(req, targetPath, n.mounter, mountOpts) - if mountErr != nil { - return nil, status.Errorf(codes.Internal, "failed to mount block volume: %s", mountErr.Error()) - } - } else if volumeCapability.GetMount() != nil { - mountErr := publishFilesystemVolume(req, targetPath, n.mounter, mountOpts) - if mountErr != nil { - return nil, status.Errorf(codes.Internal, "failed to mount filesystem volume: %s", mountErr.Error()) - } - } - - klog.Infof("Successfully published volume: %s", req.GetVolumeId()) - - return &csi.NodePublishVolumeResponse{}, nil -} - -func (n *NodeServer) NodeUnpublishVolume(_ context.Context, - req *csi.NodeUnpublishVolumeRequest, -) (*csi.NodeUnpublishVolumeResponse, error) { - klog.Infof("Received request to unpublish volume: %+v", req) - - targetPath := req.GetTargetPath() - err := mount.CleanupMountPoint(targetPath, n.mounter, false) - if err != nil { - return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to cleanup mount point %s", err.Error())) - } - - klog.Infof("Successfully unpublished volume: %s", req.GetVolumeId()) - - return &csi.NodeUnpublishVolumeResponse{}, nil -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (n *NodeServer) NodeGetVolumeStats(_ context.Context, - _ *csi.NodeGetVolumeStatsRequest, -) (*csi.NodeGetVolumeStatsResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -//nolint:wrapcheck // we want to return gRPC Status errors -func (n *NodeServer) NodeExpandVolume(_ context.Context, - _ *csi.NodeExpandVolumeRequest, -) (*csi.NodeExpandVolumeResponse, error) { - return nil, status.Error(codes.Unimplemented, errRPCUnimplemented.Error()) -} - -func (n *NodeServer) NodeGetCapabilities(_ context.Context, - _ *csi.NodeGetCapabilitiesRequest, -) (*csi.NodeGetCapabilitiesResponse, error) { - nodeCapabilities := make([]*csi.NodeServiceCapability, 0, len(NodeServerCapabilities)) - - for _, capability := range NodeServerCapabilities { - nodeCapabilities = append(nodeCapabilities, &csi.NodeServiceCapability{ - Type: &csi.NodeServiceCapability_Rpc{ - Rpc: &csi.NodeServiceCapability_RPC{ - Type: capability, - }, - }, - }) - } - - return &csi.NodeGetCapabilitiesResponse{ - Capabilities: nodeCapabilities, - }, nil -} - -func (n *NodeServer) NodeGetInfo(_ context.Context, _ *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { - // We want to provide useful topological hints to the container orchestrator - // We can only stage/publish volumes in the same location as a node - accessibleTopology := &csi.Topology{ - Segments: map[string]string{ - TopologyLocationKey: n.driver.GetNodeLocation(), - TopologyProjectKey: n.driver.GetNodeProject(), - }, - } - - return &csi.NodeGetInfoResponse{ - NodeId: n.driver.GetNodeID(), - MaxVolumesPerNode: MaxVolumesPerNode, - AccessibleTopology: accessibleTopology, - }, nil -} diff --git a/internal/driver/node_util.go b/internal/driver/node_util.go deleted file mode 100644 index c0949ea..0000000 --- a/internal/driver/node_util.go +++ /dev/null @@ -1,88 +0,0 @@ -package driver - -import ( - "errors" - "fmt" - "os" - "path/filepath" - - "github.com/container-storage-interface/spec/lib/go/csi" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "k8s.io/mount-utils" -) - -func getPersistentSSDDevicePath(serialNumber string) string { - // symlink: /dev/disk/by-id/virtio- - return fmt.Sprintf("/dev/disk/by-id/virtio-%s", serialNumber) -} - -func publishBlockVolume(req *csi.NodePublishVolumeRequest, targetPath string, - mounter *mount.SafeFormatAndMount, mountOpts []string, -) error { - volumeContext := req.GetVolumeContext() - serialNumber, ok := volumeContext[VolumeContextDiskSerialNumberKey] - if !ok { - return errVolumeMissingSerialNumber - } - - devicePath := getPersistentSSDDevicePath(serialNumber) - dirPath := filepath.Dir(targetPath) - // Check if the directory exists - if _, err := os.Stat(dirPath); errors.Is(err, os.ErrNotExist) { - // Directory does not exist, create it - if err := os.MkdirAll(dirPath, newDirPerms); err != nil { - return fmt.Errorf("failed to make directory for target path: %w", err) - } - } - - // expose the block volume as a file - f, err := os.OpenFile(targetPath, os.O_CREATE|os.O_EXCL, os.FileMode(newFilePerms)) - if err != nil { - if !os.IsExist(err) { - return fmt.Errorf("failed to make file for target path: %w", err) - } - } - if err = f.Close(); err != nil { - return fmt.Errorf("failed to close file after making target path: %w", err) - } - - mountOpts = append(mountOpts, "bind") - err = mounter.Mount(devicePath, targetPath, "", mountOpts) - if err != nil { - return fmt.Errorf("failed to mount volume at target path: %w", err) - } - - return nil -} - -func publishFilesystemVolume(req *csi.NodePublishVolumeRequest, targetPath string, - mounter *mount.SafeFormatAndMount, mountOpts []string, -) error { - volumeContext := req.GetVolumeContext() - serialNumber, ok := volumeContext[VolumeContextDiskSerialNumberKey] - if !ok { - return errVolumeMissingSerialNumber - } - - devicePath := getPersistentSSDDevicePath(serialNumber) - - // Check if the directory exists - if _, err := os.Stat(targetPath); errors.Is(err, os.ErrNotExist) { - // Directory does not exist, create it - if mkdirErr := os.MkdirAll(targetPath, newDirPerms); mkdirErr != nil { - return fmt.Errorf("failed to make directory for target path: %w", mkdirErr) - } - } - - volumeCapability := req.GetVolumeCapability() - fsType := volumeCapability.GetMount().GetFsType() - mountOpts = append(mountOpts, volumeCapability.GetMount().GetMountFlags()...) - err := mounter.FormatAndMount(devicePath, targetPath, fsType, mountOpts) - if err != nil { - return status.Errorf(codes.Internal, - fmt.Sprintf("failed to mount volume at target path: %s", err.Error())) - } - - return nil -} diff --git a/internal/driver/secrets.go b/internal/driver/secrets.go deleted file mode 100644 index b2b2911..0000000 --- a/internal/driver/secrets.go +++ /dev/null @@ -1,45 +0,0 @@ -package driver - -import ( - "fmt" - "os" -) - -const ( - SecretPath = "/etc/secrets" - AccessKeyName = "CRUSOE_CSI_ACCESS_KEY" - //nolint:gosec // we are not hardcoding credentials, just the env var to get them - SecretKeyName = "CRUSOE_CSI_SECRET_KEY" -) - -// Kubernetes provides two main ways of injecting secrets into pods: -// 1) Injecting them into environment variables which can be retrieved by the application -// 2) Creating a file '/etc/secrets' which the application can then retrieve - -func ReadSecretFromFile(secretName string) (string, error) { - // Attempt to open the file corresponding to the secret key - file, err := os.Open(fmt.Sprintf("%s/%s", SecretPath, secretName)) - if err != nil { - return "", fmt.Errorf("error opening secret file: %w", err) - } - defer file.Close() - - // Read the entire file into a byte slice - data := make([]byte, 0) - _, err = file.Read(data) - if err != nil { - return "", fmt.Errorf("error reading secret file: %w", err) - } - - secretValue := string(data) - - return secretValue, nil -} - -func GetCrusoeAccessKey() string { - return ReadEnvVar(AccessKeyName) -} - -func GetCrusoeSecretKey() string { - return ReadEnvVar(SecretKeyName) -} diff --git a/internal/driver/util.go b/internal/driver/util.go deleted file mode 100644 index c8d5a00..0000000 --- a/internal/driver/util.go +++ /dev/null @@ -1,319 +0,0 @@ -package driver - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "os" - "strings" - "time" - - "github.com/antihax/optional" - "github.com/google/uuid" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/klog/v2" - - crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" -) - -const ( - pollInterval = 2 * time.Second - OpSucceeded opStatus = "SUCCEEDED" - OpInProgress opStatus = "IN_PROGRESS" - OpFailed opStatus = "FAILED" - nodeNameEnvKey = "NODE_NAME" - projectIDEnvKey = "CRUSOE_PROJECT_ID" - projectIDLabelKey = "crusoe.ai/project.id" -) - -// apiError models the error format returned by the Crusoe API go client. -type apiError struct { - Code string `json:"code"` - Message string `json:"message"` -} - -type opStatus string - -type opResultError struct { - Code string `json:"code"` - Message string `json:"message"` -} - -var ( - errUnableToGetOpRes = errors.New("failed to get result of operation") - // fallback error presented to the user in unexpected situations. - errUnexpected = errors.New("an unexpected error occurred, please try again, and if the problem persists, " + - "contact support@crusoecloud.com") - errInstanceNotFound = errors.New("instance not found") - errNodeNameEnvVarNotSet = fmt.Errorf("env var %s not set", nodeNameEnvKey) - errProjectIDNotFound = fmt.Errorf("project ID not found in %s env var or %s node label", - projectIDEnvKey, projectIDLabelKey) -) - -// UnpackAPIError takes a crusoeapi API error and safely attempts to extract any additional information -// present in the response. The original error is returned unchanged if it cannot be unpacked. -func UnpackAPIError(original error) error { - apiErr := &crusoeapi.GenericSwaggerError{} - if ok := errors.As(original, apiErr); !ok { - return original - } - - var model apiError - err := json.Unmarshal(apiErr.Body(), &model) - if err != nil { - return original - } - - // some error messages are of the format "rpc code = ... desc = ..." - // in those cases, we extract the description and return it - const two = 2 - components := strings.Split(model.Message, " desc = ") - if len(components) == two { - //nolint:goerr113 // error is dynamic - return fmt.Errorf("%s", components[1]) - } - - //nolint:goerr113 // error is dynamic - return fmt.Errorf("%s", model.Message) -} - -func opResultToError(res interface{}) (expectedErr, unexpectedErr error) { - b, err := json.Marshal(res) - if err != nil { - return nil, fmt.Errorf("unable to marshal operation error: %w", err) - } - resultError := opResultError{} - err = json.Unmarshal(b, &resultError) - if err != nil { - return nil, fmt.Errorf("op result type not error as expected: %w", err) - } - - //nolint:goerr113 //This function is designed to return dynamic errors - return fmt.Errorf("%s", resultError.Message), nil -} - -func parseOpResult[T any](opResult interface{}) (*T, error) { - b, err := json.Marshal(opResult) - if err != nil { - return nil, errUnableToGetOpRes - } - - var result T - err = json.Unmarshal(b, &result) - if err != nil { - return nil, errUnableToGetOpRes - } - - return &result, nil -} - -// awaitOperation polls an async API operation until it resolves into a success or failure state. -func awaitOperation(ctx context.Context, op *crusoeapi.Operation, projectID string, - getFunc func(context.Context, string, string) (crusoeapi.Operation, *http.Response, error)) ( - *crusoeapi.Operation, error, -) { - for op.State == string(OpInProgress) { - updatedOps, httpResp, err := getFunc(ctx, projectID, op.OperationId) - if err != nil { - return nil, fmt.Errorf("error getting operation with id %s: %w", op.OperationId, err) - } - httpResp.Body.Close() - - op = &updatedOps - - time.Sleep(pollInterval) - } - - switch op.State { - case string(OpSucceeded): - return op, nil - case string(OpFailed): - opError, err := opResultToError(op.Result) - if err != nil { - return op, err - } - - return op, opError - default: - - return op, errUnexpected - } -} - -// AwaitOperationAndResolve awaits an async API operation and attempts to parse the response as an instance of T, -// if the operation was successful. -func awaitOperationAndResolve[T any](ctx context.Context, op *crusoeapi.Operation, projectID string, - getFunc func(context.Context, string, string) (crusoeapi.Operation, *http.Response, error), -) (*T, *crusoeapi.Operation, error) { - op, err := awaitOperation(ctx, op, projectID, getFunc) - if err != nil { - return nil, op, err - } - - result, err := parseOpResult[T](op.Result) - if err != nil { - return nil, op, err - } - - return result, op, nil -} - -func getProjectID(ctx context.Context, kubeClient *kubernetes.Clientset, nodeName string) (string, error) { - projectIDFromEnv := os.Getenv(projectIDEnvKey) - - if projectIDFromEnv != "" { - _, err := uuid.Parse(projectIDFromEnv) - if err != nil { - return "", fmt.Errorf("failed to parse project ID from env var: %w", err) - } - - return projectIDFromEnv, nil - } - - node, err := kubeClient.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) - if err != nil { - return "", fmt.Errorf("could not fetch current node with kube client: %w", err) - } - - projectIDFromNodeLabels, ok := node.Labels[projectIDLabelKey] - if !ok { - return "", errProjectIDNotFound - } - - return projectIDFromNodeLabels, nil -} - -func getInstanceInfoWithProjectID(ctx context.Context, - client *crusoeapi.APIClient, - kubeClient *kubernetes.Clientset, - nodeName string) ( - instanceID string, - projectID string, - location string, - err error, -) { - projectID, err = getProjectID(ctx, kubeClient, nodeName) - if err != nil { - return "", "", "", err - } - - instance, err := findInstanceInProject(ctx, client, projectID, nodeName) - if err != nil { - return "", "", "", err - } - - return instance.Id, instance.ProjectId, instance.Location, nil -} - -func getInstanceInfoFallback(ctx context.Context, client *crusoeapi.APIClient, nodeName string) ( - instanceID string, - projectID string, - location string, - err error, -) { - instance, err := findInstance(ctx, client, nodeName) - if err != nil { - return "", "", "", fmt.Errorf("could not find instance with name '%s': %w", nodeName, err) - } - - return instance.Id, instance.ProjectId, instance.Location, nil -} - -func GetInstanceInfo(ctx context.Context, client *crusoeapi.APIClient, kubeClient *kubernetes.Clientset) ( - instanceID string, - projectID string, - location string, - err error, -) { - nodeName := os.Getenv(nodeNameEnvKey) - if nodeName == "" { - return "", "", "", errNodeNameEnvVarNotSet - } - - instanceID, projectID, location, err = getInstanceInfoWithProjectID(ctx, client, kubeClient, nodeName) - if err != nil { - klog.Warningf("failed to get instance id of node with project ID: %s", err) - klog.Info("Attempting fallback method") - } else { - // No error, return - return instanceID, projectID, location, nil - } - - // Fall back to getInstanceInfoFallback - instanceID, projectID, location, err = getInstanceInfoFallback(ctx, client, nodeName) - if err != nil { - return "", "", "", err - } - - return instanceID, projectID, location, nil -} - -func findInstanceInProject(ctx context.Context, - client *crusoeapi.APIClient, - projectID string, - instanceName string, -) (*crusoeapi.InstanceV1Alpha5, error) { - listVMOpts := &crusoeapi.VMsApiListInstancesOpts{ - Names: optional.NewString(instanceName), - } - instances, instancesHTTPResp, instancesErr := client.VMsApi.ListInstances(ctx, projectID, listVMOpts) - if instancesErr != nil { - return nil, fmt.Errorf("failed to list instances: %w", instancesErr) - } - instancesHTTPResp.Body.Close() - - if len(instances.Items) == 0 { - return nil, errInstanceNotFound - } - - return &instances.Items[0], nil -} - -func findInstance(ctx context.Context, - client *crusoeapi.APIClient, instanceName string, -) (*crusoeapi.InstanceV1Alpha5, error) { - opts := &crusoeapi.ProjectsApiListProjectsOpts{ - OrgId: optional.EmptyString(), - } - - projectsResp, projectHTTPResp, err := client.ProjectsApi.ListProjects(ctx, opts) - if err != nil { - return nil, fmt.Errorf("failed to query for projects: %w", err) - } - - defer projectHTTPResp.Body.Close() - - for _, project := range projectsResp.Items { - listVMOpts := &crusoeapi.VMsApiListInstancesOpts{ - Names: optional.NewString(instanceName), - } - instances, instancesHTTPResp, instancesErr := client.VMsApi.ListInstances(ctx, project.Id, listVMOpts) - if instancesErr != nil { - return nil, fmt.Errorf("failed to list instances: %w", instancesErr) - } - instancesHTTPResp.Body.Close() - - if len(instances.Items) == 0 { - continue - } - - for i := range instances.Items { - if instances.Items[i].Name == instanceName { - return &instances.Items[i], nil - } - } - } - - return nil, errInstanceNotFound -} - -func ReadEnvVar(secretName string) string { - return os.Getenv(secretName) -} - -func GetNodeFQDN() string { - return ReadEnvVar("NODE_NAME") -} diff --git a/internal/identity/identity.go b/internal/identity/identity.go new file mode 100644 index 0000000..2ca231a --- /dev/null +++ b/internal/identity/identity.go @@ -0,0 +1,37 @@ +package identity + +import ( + "context" + + "github.com/container-storage-interface/spec/lib/go/csi" +) + +type Service struct { + csi.UnimplementedIdentityServer + PluginName string + PluginVersion string + Capabilities []*csi.PluginCapability +} + +func (s *Service) GetPluginInfo(_ context.Context, + _ *csi.GetPluginInfoRequest, +) (*csi.GetPluginInfoResponse, error) { + return &csi.GetPluginInfoResponse{ + Name: s.PluginName, + VendorVersion: s.PluginVersion, + }, nil +} + +func (s *Service) GetPluginCapabilities(_ context.Context, + _ *csi.GetPluginCapabilitiesRequest, +) (*csi.GetPluginCapabilitiesResponse, error) { + return &csi.GetPluginCapabilitiesResponse{ + Capabilities: s.Capabilities, + }, nil +} + +func (s *Service) Probe(_ context.Context, + _ *csi.ProbeRequest, +) (*csi.ProbeResponse, error) { + return &csi.ProbeResponse{}, nil +} diff --git a/internal/locker/locker.go b/internal/locker/locker.go deleted file mode 100644 index 8eb66f8..0000000 --- a/internal/locker/locker.go +++ /dev/null @@ -1,56 +0,0 @@ -package locker - -import ( - "sync" -) - -type Locker struct { - locks map[string]*sync.RWMutex // Maps ID to its mutex for locking. - mx *sync.Mutex -} - -func NewLocker() *Locker { - return &Locker{ - locks: make(map[string]*sync.RWMutex), - mx: &sync.Mutex{}, - } -} - -func (l *Locker) TryAcquireReadLock(id string) bool { - l.mx.Lock() - rwLock, ok := l.locks[id] - if !ok { - rwLock = &sync.RWMutex{} - l.locks[id] = rwLock - } - l.mx.Unlock() - - return rwLock.TryRLock() -} - -func (l *Locker) ReleaseReadLock(id string) { - rwLock, ok := l.locks[id] - if ok { - rwLock.RUnlock() - } -} - -func (l *Locker) TryAcquireWriteLock(id string) bool { - l.mx.Lock() - rwLock, ok := l.locks[id] - if !ok { - rwLock = &sync.RWMutex{} - l.locks[id] = rwLock - } - l.mx.Unlock() - - return rwLock.TryLock() -} - -// ReleaseWriteLock releases a previously acquired write lock for the given ID. -func (l *Locker) ReleaseWriteLock(id string) { - rwLock, ok := l.locks[id] - if ok { - rwLock.Unlock() - } -} diff --git a/internal/node/node.go b/internal/node/node.go new file mode 100644 index 0000000..ad3b248 --- /dev/null +++ b/internal/node/node.go @@ -0,0 +1,156 @@ +package node + +import ( + "context" + "errors" + "fmt" + "strconv" + + "github.com/crusoecloud/crusoe-csi-driver/internal/crusoe" + + "github.com/container-storage-interface/spec/lib/go/csi" + crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" + "github.com/crusoecloud/crusoe-csi-driver/internal/common" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "k8s.io/klog/v2" + "k8s.io/mount-utils" +) + +var ErrFailedResize = errors.New("failed to resize disk") + +type DefaultNode struct { + csi.UnimplementedNodeServer + CrusoeClient *crusoeapi.APIClient + HostInstance *crusoeapi.InstanceV1Alpha5 + Mounter *mount.SafeFormatAndMount + Resizer *mount.ResizeFs + DiskType common.DiskType + PluginName string + PluginVersion string + Capabilities []*csi.NodeServiceCapability +} + +func (d *DefaultNode) NodeStageVolume(_ context.Context, _ *csi.NodeStageVolumeRequest) ( + *csi.NodeStageVolumeResponse, + error, +) { + return nil, status.Errorf(codes.Unimplemented, "%s: NodeStageVolume", common.ErrNotImplemented) +} + +func (d *DefaultNode) NodeUnstageVolume(_ context.Context, _ *csi.NodeUnstageVolumeRequest) ( + *csi.NodeUnstageVolumeResponse, + error, +) { + return nil, status.Errorf(codes.Unimplemented, "%s: NodeUnstageVolume", common.ErrNotImplemented) +} + +func (d *DefaultNode) NodePublishVolume(_ context.Context, request *csi.NodePublishVolumeRequest) ( + *csi.NodePublishVolumeResponse, + error, +) { + klog.Infof("Received request to publish volume: %+v", request) + + var mountOpts []string + + if request.GetReadonly() { + mountOpts = append(mountOpts, readOnlyMountOption) + } + + err := nodePublishVolume(d.Mounter, d.Resizer, mountOpts, d.DiskType, request) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to publish volume: %s", err.Error()) + } + + klog.Infof("Successfully published volume: %s", request.GetVolumeId()) + + return &csi.NodePublishVolumeResponse{}, nil +} + +func (d *DefaultNode) NodeUnpublishVolume(_ context.Context, request *csi.NodeUnpublishVolumeRequest) ( + *csi.NodeUnpublishVolumeResponse, + error, +) { + klog.Infof("Received request to unpublish volume: %+v", request) + + targetPath := request.GetTargetPath() + err := mount.CleanupMountPoint(targetPath, d.Mounter, false) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to cleanup mount point %s", err.Error()) + } + + klog.Infof("Successfully unpublished volume: %s", request.GetVolumeId()) + + return &csi.NodeUnpublishVolumeResponse{}, nil +} + +func (d *DefaultNode) NodeGetVolumeStats(_ context.Context, _ *csi.NodeGetVolumeStatsRequest) ( + *csi.NodeGetVolumeStatsResponse, + error, +) { + return nil, status.Errorf(codes.Unimplemented, "%s: NodeGetVolumeStats", common.ErrNotImplemented) +} + +// NodeExpandVolume This function is currently unused. +// common.DiskTypeFS disks do not require expansion on the node. +// common.DiskTypeSSD disks would require expansion on the node if they supported online expansion. +func (d *DefaultNode) NodeExpandVolume(ctx context.Context, request *csi.NodeExpandVolumeRequest) ( + *csi.NodeExpandVolumeResponse, + error, +) { + // Note that this function will only be called for diskType == common.DiskTypeSSD + // because FS disks do not require expansion on the node + + // Block devices do not require expansion on the node + if request.GetVolumeCapability().GetBlock() != nil { + return &csi.NodeExpandVolumeResponse{}, nil + } + + // Fetch disk's serial number because NodeExpandVolumeRequest does not include the volume context :( + disk, err := crusoe.FindDiskByIDFallible(ctx, d.CrusoeClient, d.HostInstance.ProjectId, request.GetVolumeId()) + if err != nil { + return nil, status.Errorf(codes.NotFound, "failed to find disk: %s", err) + } + devicePath := getSSDDevicePath(disk.SerialNumber) + + ok, err := d.Resizer.Resize(devicePath, request.GetVolumePath()) + if err != nil { + return nil, fmt.Errorf("failed to resize %s: %w", request.GetVolumePath(), err) + } + + if !ok { + return nil, fmt.Errorf("%w: %s", ErrFailedResize, request.GetVolumePath()) + } + + return &csi.NodeExpandVolumeResponse{}, nil +} + +func (d *DefaultNode) NodeGetCapabilities(_ context.Context, _ *csi.NodeGetCapabilitiesRequest) ( + *csi.NodeGetCapabilitiesResponse, + error, +) { + return &csi.NodeGetCapabilitiesResponse{ + Capabilities: d.Capabilities, + }, nil +} + +func (d *DefaultNode) NodeGetInfo(_ context.Context, _ *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { + topologySegments := map[string]string{ + common.GetTopologyKey(d.PluginName, common.TopologyLocationKey): d.HostInstance.Location, + } + + //nolint:lll // long names + if d.DiskType == common.DiskTypeFS { + topologySegments[common.GetTopologyKey(d.PluginName, common.TopologySupportsSharedDisksKey)] = strconv.FormatBool(supportsFS(d.HostInstance)) + } + + return &csi.NodeGetInfoResponse{ + NodeId: d.HostInstance.Id, + // Hard limit upstream, change if needed + // Subtract 1 to allow for the boot disk + MaxVolumesPerNode: common.MaxVolumesPerNode - 1, + AccessibleTopology: &csi.Topology{ + Segments: topologySegments, + }, + }, nil +} diff --git a/internal/node/util.go b/internal/node/util.go new file mode 100644 index 0000000..3f4b9de --- /dev/null +++ b/internal/node/util.go @@ -0,0 +1,176 @@ +package node + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/crusoecloud/crusoe-csi-driver/internal/common" + + "github.com/container-storage-interface/spec/lib/go/csi" + crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" + "k8s.io/klog/v2" + "k8s.io/mount-utils" +) + +const ( + newDirPerms = 0o755 // this represents: rwxr-xr-x + newFilePerms = 0o644 // this represents: rw-r--r-- + expectedTypeSegments = 2 + fsDiskFilesystem = "virtiofs" + readOnlyMountOption = "ro" +) + +var ( + errUnexpectedVolumeCapability = errors.New("unexpected volume capability") + errVolumeMissingSerialNumber = fmt.Errorf( + "volume missing serial number context key %s", + common.VolumeContextDiskSerialNumberKey) + errVolumeMissingName = fmt.Errorf("volume missing name context key %s", common.VolumeContextDiskNameKey) + errFailedMount = errors.New("failed to mount volume") +) + +func getSSDDevicePath(serialNumber string) string { + // symlink: /dev/disk/by-id/virtio- + return fmt.Sprintf("/dev/disk/by-id/virtio-%s", serialNumber) +} + +func nodePublishBlockVolume(devicePath string, + mounter *mount.SafeFormatAndMount, + mountOpts []string, + request *csi.NodePublishVolumeRequest, +) error { + dirPath := filepath.Dir(request.GetTargetPath()) + // Check if the directory exists + if _, err := os.Stat(dirPath); errors.Is(err, os.ErrNotExist) { + // Directory does not exist, create it + if err := os.MkdirAll(dirPath, newDirPerms); err != nil { + return fmt.Errorf("failed to make directory for target path: %w", err) + } + } + + // expose the block volume as a file + f, err := os.OpenFile(request.GetTargetPath(), os.O_CREATE|os.O_EXCL, os.FileMode(newFilePerms)) + if err != nil { + if !os.IsExist(err) { + return fmt.Errorf("failed to make file for target path: %w", err) + } + } + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close file after making target path: %w", err) + } + + mountOpts = append(mountOpts, "bind") + mountOpts = append(mountOpts, request.GetVolumeCapability().GetMount().GetMountFlags()...) + err = mounter.Mount(devicePath, request.GetTargetPath(), "", mountOpts) + if err != nil { + return fmt.Errorf("%w at target path %s: %s", errFailedMount, request.GetTargetPath(), err.Error()) + } + + return nil +} + +func nodePublishFilesystemVolume(devicePath string, + mounter *mount.SafeFormatAndMount, + resizer *mount.ResizeFs, + mountOpts []string, + diskType common.DiskType, + request *csi.NodePublishVolumeRequest, +) error { + // Check if the directory exists + if _, err := os.Stat(request.GetTargetPath()); errors.Is(err, os.ErrNotExist) { + // Directory does not exist, create it + if mkdirErr := os.MkdirAll(request.GetTargetPath(), newDirPerms); mkdirErr != nil { + return fmt.Errorf("failed to make directory for target path: %w", mkdirErr) + } + } + + mountOpts = append(mountOpts, request.GetVolumeCapability().GetMount().GetMountFlags()...) + + //nolint:nestif // error handling + if diskType == common.DiskTypeFS { + volumeContext := request.GetVolumeContext() + diskName, ok := volumeContext[common.VolumeContextDiskNameKey] + if !ok { + return errVolumeMissingName + } + + err := mounter.Mount(diskName, request.GetTargetPath(), fsDiskFilesystem, mountOpts) + if err != nil { + return fmt.Errorf("%w at target path %s: %s", errFailedMount, request.GetTargetPath(), err.Error()) + } + } else { + err := mounter.FormatAndMount(devicePath, + request.GetTargetPath(), + request.GetVolumeCapability().GetMount().GetFsType(), + mountOpts) + if err != nil { + return fmt.Errorf("%w at target path %s: %s", errFailedMount, request.GetTargetPath(), err.Error()) + } + + // Resize the filesystem to span the entire disk + // The size of the underlying disk may have changed due to volume expansion (offline) + ok, err := resizer.Resize(devicePath, request.GetTargetPath()) + if err != nil { + return fmt.Errorf("%w at target path %s: %w", ErrFailedResize, request.GetTargetPath(), err) + } + + if !ok { + return fmt.Errorf("%w: %s", ErrFailedResize, request.GetTargetPath()) + } + } + + return nil +} + +func nodePublishVolume(mounter *mount.SafeFormatAndMount, + resizer *mount.ResizeFs, + mountOpts []string, + diskType common.DiskType, + request *csi.NodePublishVolumeRequest, +) error { + volumeContext := request.GetVolumeContext() + serialNumber, ok := volumeContext[common.VolumeContextDiskSerialNumberKey] + if !ok { + return errVolumeMissingSerialNumber + } + + devicePath := getSSDDevicePath(serialNumber) + + switch { + case request.GetVolumeCapability().GetBlock() != nil: + return nodePublishBlockVolume(devicePath, mounter, mountOpts, request) + case request.GetVolumeCapability().GetMount() != nil: + return nodePublishFilesystemVolume(devicePath, mounter, resizer, mountOpts, diskType, request) + default: + return fmt.Errorf("%w: %s", errUnexpectedVolumeCapability, request.GetVolumeCapability()) + } +} + +func supportsFS(node *crusoeapi.InstanceV1Alpha5) bool { + typeSegments := strings.Split(node.Type_, ".") + if len(typeSegments) != expectedTypeSegments { + klog.Infof("Unexpected node type: %s", node.Type_) + + return false + } + + // All CPU instances support shared filesystems + if typeSegments[0] == "c1a" || typeSegments[0] == "s1a" { + return true + } + + // There are 10 slices in an L40s node + if typeSegments[0] == "l40s-48gb" && typeSegments[1] == "10x" { + return true + } + + // Otherwise, there are 8 slices in every other GPU node + if typeSegments[1] == "8x" { + return true + } + + return false +} diff --git a/internal/server.go b/internal/server.go new file mode 100644 index 0000000..d267fb3 --- /dev/null +++ b/internal/server.go @@ -0,0 +1,280 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "io/fs" + "net" + "net/url" + "os" + "os/signal" + "sync" + "syscall" + + "github.com/crusoecloud/crusoe-csi-driver/internal/controller" + + "github.com/crusoecloud/crusoe-csi-driver/internal/common" + + "github.com/crusoecloud/crusoe-csi-driver/internal/node" + + "k8s.io/mount-utils" + "k8s.io/utils/exec" + + "github.com/crusoecloud/crusoe-csi-driver/internal/crusoe" + "github.com/crusoecloud/crusoe-csi-driver/internal/identity" + + "github.com/antihax/optional" + "github.com/container-storage-interface/spec/lib/go/csi" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "google.golang.org/grpc" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" + + crusoeapi "github.com/crusoecloud/client-go/swagger/v1alpha5" +) + +const ( + projectIDEnvKey = "CRUSOE_PROJECT_ID" + projectIDLabelKey = "crusoe.ai/project.id" +) + +var ( + errInstanceNotFound = errors.New("instance not found") + errMultipleInstances = errors.New("multiple instances found") + errProjectIDNotFound = fmt.Errorf("project ID not found in %s env var or %s node label", + projectIDEnvKey, projectIDLabelKey) +) + +func interruptHandler() (*sync.WaitGroup, context.Context) { + // Handle interrupts + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + signal.Notify(interrupt, syscall.SIGTERM) + var wg sync.WaitGroup + wg.Add(1) + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + select { + case <-ctx.Done(): + return + + case <-interrupt: + wg.Done() + cancel() + } + }() + + return &wg, ctx +} + +func getHostInstance(ctx context.Context) (*crusoeapi.InstanceV1Alpha5, error) { + crusoeClient := crusoe.NewCrusoeClient( + viper.GetString(CrusoeAPIEndpointFlag), + viper.GetString(CrusoeAccessKeyFlag), + viper.GetString(CrusoeSecretKeyFlag), + "crusoe-csi-driver/0.0.1", + ) + + nodeName := viper.GetString(NodeNameFlag) + + var projectID string + + projectID = viper.GetString(CrusoeProjectIDFlag) + if projectID == "" { + var ok bool + kubeClientConfig, err := rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("could not get kube client config: %w", err) + } + + kubeClient, err := kubernetes.NewForConfig(kubeClientConfig) + if err != nil { + return nil, fmt.Errorf("could not get kube client: %w", err) + } + hostNode, nodeFetchErr := kubeClient.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) + if nodeFetchErr != nil { + return nil, fmt.Errorf("could not fetch current node with kube client: %w", err) + } + + projectID, ok = hostNode.Labels[projectIDLabelKey] + if !ok { + return nil, errProjectIDNotFound + } + } + + instances, _, err := crusoeClient.VMsApi.ListInstances(ctx, projectID, + &crusoeapi.VMsApiListInstancesOpts{ + Names: optional.NewString(nodeName), + }) + if err != nil { + return nil, fmt.Errorf("failed to list instances: %w", err) + } + + if len(instances.Items) == 0 { + return nil, fmt.Errorf("%w: %s", errInstanceNotFound, nodeName) + } else if len(instances.Items) > 1 { + return nil, fmt.Errorf("%w: %s", errMultipleInstances, nodeName) + } + + return &instances.Items[0], nil +} + +func listen() (net.Listener, error) { + ep, err := url.Parse(viper.GetString(SocketAddressFlag)) + if err != nil { + return nil, fmt.Errorf("failed to parse socket url: %w", err) + } + + if ep.Scheme == "unix" { + removeErr := os.Remove(ep.Path) + if removeErr != nil { + if !errors.Is(removeErr, fs.ErrNotExist) { + return nil, fmt.Errorf("failed to remove socket file %s: %w", ep.Path, removeErr) + } + } + } + listener, listenErr := net.Listen(ep.Scheme, ep.Path) + if listenErr != nil { + return nil, fmt.Errorf("failed to start listener on provided socket url: %w", listenErr) + } + + return listener, nil +} + +//nolint:gocritic // don't combine parameter types +func newCrusoeClientWithViperConfig(pluginName string, pluginVersion string) *crusoeapi.APIClient { + return crusoe.NewCrusoeClient( + viper.GetString(CrusoeAPIEndpointFlag), + viper.GetString(CrusoeAccessKeyFlag), + viper.GetString(CrusoeSecretKeyFlag), + fmt.Sprintf("%s/%s", pluginName, pluginVersion), + ) +} + +//nolint:funlen,cyclop // server instantiation is long +func registerServices(grpcServer *grpc.Server, hostInstance *crusoeapi.InstanceV1Alpha5) ( + diskType common.DiskType, + pluginName string, + pluginVersion string, +) { + serveIdentity := false + serveController := false + serveNode := false + + switch SelectedCSIDriverType { + case CSIDriverTypeSSD: + diskType = common.DiskTypeSSD + pluginName = common.SSDPluginName + pluginVersion = common.SSDPluginVersion + case CSIDriverTypeFS: + diskType = common.DiskTypeFS + pluginName = common.FSPluginName + pluginVersion = common.FSPluginVersion + default: + panic(fmt.Sprintf("%s is not a valid driver type", viper.GetString(CSIDriverTypeFlag))) + } + + for _, s := range Services { + switch s { + case ServiceTypeIdentity: + serveIdentity = true + case ServiceTypeController: + serveController = true + case ServiceTypeNode: + serveNode = true + } + } + + if serveIdentity { + capabilities := common.BaseIdentityCapabilities + + if serveController { + capabilities = append(capabilities, &common.PluginCapabilityControllerService) + } + if diskType == common.DiskTypeFS { + capabilities = append(capabilities, &common.PluginCapabilityVolumeExpansionOnline) + } + if diskType == common.DiskTypeSSD { + capabilities = append(capabilities, &common.PluginCapabilityVolumeExpansionOffline) + } + + csi.RegisterIdentityServer(grpcServer, &identity.Service{ + Capabilities: capabilities, + PluginName: pluginName, + PluginVersion: pluginVersion, + }) + } + + if serveController { + capabilities := common.BaseControllerCapabilities + + csi.RegisterControllerServer(grpcServer, &controller.DefaultController{ + CrusoeClient: newCrusoeClientWithViperConfig(pluginName, pluginVersion), + HostInstance: hostInstance, + Capabilities: capabilities, + DiskType: diskType, + PluginName: pluginName, + PluginVersion: pluginVersion, + }) + } + + if serveNode { + capabilities := common.BaseNodeCapabilities + + // TODO: Add NodeExpandVolume capability once SSD online expansion is supported upstream + + csi.RegisterNodeServer(grpcServer, &node.DefaultNode{ + CrusoeClient: newCrusoeClientWithViperConfig(pluginName, pluginVersion), + HostInstance: hostInstance, + Capabilities: capabilities, + Mounter: mount.NewSafeFormatAndMount(mount.New(""), exec.New()), + Resizer: mount.NewResizeFs(exec.New()), + DiskType: diskType, + PluginName: pluginName, + PluginVersion: pluginVersion, + }) + } + + return diskType, pluginName, pluginVersion +} + +func RunMain(_ *cobra.Command, _ []string) error { + wg, ctx := interruptHandler() + + srv := grpc.NewServer() + + hostInstance, err := getHostInstance(ctx) + if err != nil { + return fmt.Errorf("failed to get host instance: %w", err) + } + klog.Infof("Crusoe host instance ID: %+v", hostInstance.Id) + + diskType, pluginName, pluginVersion := registerServices(srv, hostInstance) + _ = diskType + + listener, err := listen() + if err != nil { + return err + } + + klog.Infof("Listening on: %s", listener.Addr()) + + go func() { + klog.Infof("Starting driver name: %s version: %s", pluginName, pluginVersion) + err = srv.Serve(listener) + if !errors.Is(err, grpc.ErrServerStopped) { + klog.Errorf("gRPC server stopped: %s", err) + } + }() + + wg.Wait() + + srv.GracefulStop() + + return nil +} diff --git a/scripts/tag_semver.sh b/scripts/tag_semver.sh old mode 100644 new mode 100755 diff --git a/versions.env b/versions.env index 94a9141..4a4f29a 100644 --- a/versions.env +++ b/versions.env @@ -1,2 +1,2 @@ export MAJOR_VERSION=0 -export MINOR_VERSION=0 +export MINOR_VERSION=1