diff --git a/Gopkg.lock b/Gopkg.lock index 87f8a311e6..5c8dfd904e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -190,23 +190,26 @@ version = "v1.0.1" [[projects]] - digest = "1:610f50d23ee3e0fa37d77fc23bf2c3b6897d2dc1c934739f13cdf551e9fc57d9" + digest = "1:b09c9ac14d93f481c768e73a12d4e8527ac25232d593f2ad918ffe462901b654" name = "github.com/kubernetes-csi/csi-lib-utils" - packages = ["protosanitizer"] + packages = [ + "connection", + "protosanitizer", + ] pruneopts = "NUT" - revision = "1628ab5351eafa4fc89a96862a08a891e601e03a" - version = "v0.1.0" + revision = "8053f37bf1d11d769c20f9514538c4b3b906e1f7" + version = "v0.4.0-rc1" [[projects]] - digest = "1:cab5d1fe86e273b35887f707dbec779d77d87613d9f2f14ea23002912197ce81" + digest = "1:0f47ba38b647bb8e7cddb71c3341134b2c2eaa8cef2af82291b5c9870ee7f572" name = "github.com/kubernetes-csi/csi-test" packages = [ "driver", "utils", ] pruneopts = "NUT" - revision = "619da6853e10bef67ddcc8f1c2b68b73154bf11d" - version = "v1.0.0-rc2" + revision = "722eead38c269060656e0fc91f280610ea56f19b" + version = "v1.0.3" [[projects]] digest = "1:70e121796a8b5b538b08bf54c8d34245069a82300d8f6e8476874e4dfa450bf6" @@ -851,6 +854,7 @@ input-imports = [ "github.com/container-storage-interface/spec/lib/go/csi", "github.com/golang/mock/gomock", + "github.com/kubernetes-csi/csi-lib-utils/connection", "github.com/kubernetes-csi/csi-lib-utils/protosanitizer", "github.com/kubernetes-csi/csi-test/driver", "github.com/kubernetes-csi/external-snapshotter/pkg/apis/volumesnapshot/v1alpha1", @@ -860,9 +864,6 @@ "github.com/kubernetes-sigs/sig-storage-lib-external-provisioner/util", "github.com/spf13/pflag", "google.golang.org/grpc", - "google.golang.org/grpc/codes", - "google.golang.org/grpc/connectivity", - "google.golang.org/grpc/status", "k8s.io/api/core/v1", "k8s.io/api/storage/v1beta1", "k8s.io/apimachinery/pkg/api/resource", @@ -881,6 +882,7 @@ "k8s.io/client-go/rest", "k8s.io/client-go/testing", "k8s.io/client-go/tools/clientcmd", + "k8s.io/client-go/util/workqueue", "k8s.io/csi-api/pkg/apis/csi/v1alpha1", "k8s.io/csi-api/pkg/client/clientset/versioned", "k8s.io/csi-api/pkg/client/clientset/versioned/fake", diff --git a/Gopkg.toml b/Gopkg.toml index d211955c62..0be166930f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -26,7 +26,7 @@ [[constraint]] name = "github.com/kubernetes-csi/csi-test" - version = "v1.0.0-1" + version = "~v1.0.3" [[constraint]] name = "github.com/kubernetes-csi/external-snapshotter" @@ -62,7 +62,8 @@ [[constraint]] name = "github.com/kubernetes-csi/csi-lib-utils" - version = "0.1.0" + version = ">=0.4.0-rc1" + [prune] non-go = true go-tests = true diff --git a/cmd/csi-provisioner/csi-provisioner.go b/cmd/csi-provisioner/csi-provisioner.go index 77542bdd31..e0b9d04c9c 100644 --- a/cmd/csi-provisioner/csi-provisioner.go +++ b/cmd/csi-provisioner/csi-provisioner.go @@ -28,7 +28,6 @@ import ( "k8s.io/klog" flag "github.com/spf13/pflag" - "google.golang.org/grpc" ctrl "github.com/kubernetes-csi/external-provisioner/pkg/controller" snapclientset "github.com/kubernetes-csi/external-snapshotter/pkg/client/clientset/versioned" @@ -49,7 +48,7 @@ var ( master = flag.String("master", "", "Master URL to build a client config from. Either this or kubeconfig needs to be set if the provisioner is being run out of cluster.") kubeconfig = flag.String("kubeconfig", "", "Absolute path to the kubeconfig file. Either this or master needs to be set if the provisioner is being run out of cluster.") csiEndpoint = flag.String("csi-address", "/run/csi/socket", "The gRPC endpoint for Target CSI Volume.") - connectionTimeout = flag.Duration("connection-timeout", 10*time.Second, "Timeout for waiting for CSI driver socket.") + connectionTimeout = flag.Duration("connection-timeout", 0, "This option is deprecated.") volumeNamePrefix = flag.String("volume-name-prefix", "pvc", "Prefix to apply to the name of a created volume.") volumeNameUUIDLength = flag.Int("volume-name-uuid-length", -1, "Truncates generated UUID of a created volume to this length. Defaults behavior is to NOT truncate.") showVersion = flag.Bool("version", false, "Show version.") @@ -78,6 +77,10 @@ func init() { flag.Set("logtostderr", "true") flag.Parse() + if *connectionTimeout != 0 { + klog.Warningf("Warning: option -connection-timeout is deprecated and has no effect") + } + if err := utilfeature.DefaultFeatureGate.SetFromMap(featureGates); err != nil { klog.Fatal(err) } @@ -127,17 +130,16 @@ func init() { klog.Fatalf("Error getting server version: %v", err) } - // Provisioner will stay in Init until driver opens csi socket, once it's done - // controller will exit this loop and proceed normally. - socketDown := true - grpcClient := &grpc.ClientConn{} - for socketDown { - grpcClient, err = ctrl.Connect(*csiEndpoint, *connectionTimeout) - if err == nil { - socketDown = false - continue - } - time.Sleep(10 * time.Second) + grpcClient, err := ctrl.Connect(*csiEndpoint) + if err != nil { + klog.Error(err.Error()) + os.Exit(1) + } + + err = ctrl.Probe(grpcClient, *operationTimeout) + if err != nil { + klog.Error(err.Error()) + os.Exit(1) } // Autodetect provisioner name diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 6157f37e44..170ccf03f8 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -20,12 +20,12 @@ import ( "context" "fmt" "math" - "net" "os" "strings" "time" "github.com/container-storage-interface/spec/lib/go/csi" + "github.com/kubernetes-csi/csi-lib-utils/connection" "github.com/kubernetes-csi/csi-lib-utils/protosanitizer" "github.com/kubernetes-csi/external-provisioner/pkg/features" snapapi "github.com/kubernetes-csi/external-snapshotter/pkg/apis/volumesnapshot/v1alpha1" @@ -46,7 +46,6 @@ import ( "k8s.io/klog" "google.golang.org/grpc" - "google.golang.org/grpc/connectivity" ) type deprecatedSecretParamsMap struct { @@ -185,55 +184,18 @@ func logGRPC(ctx context.Context, method string, req, reply interface{}, cc *grp return err } -func Connect(address string, timeout time.Duration) (*grpc.ClientConn, error) { - klog.V(2).Infof("Connecting to %s", address) - dialOptions := []grpc.DialOption{ - grpc.WithInsecure(), - grpc.WithBackoffMaxDelay(time.Second), - grpc.WithUnaryInterceptor(logGRPC), - } - if strings.HasPrefix(address, "/") { - dialOptions = append(dialOptions, grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { - return net.DialTimeout("unix", addr, timeout) - })) - } - conn, err := grpc.Dial(address, dialOptions...) +func Connect(address string) (*grpc.ClientConn, error) { + return connection.Connect(address, connection.OnConnectionLoss(connection.ExitOnConnectionLoss())) +} - if err != nil { - return nil, err - } - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - for { - if !conn.WaitForStateChange(ctx, conn.GetState()) { - klog.V(4).Infof("Connection timed out") - return conn, fmt.Errorf("Connection timed out") - } - if conn.GetState() == connectivity.Ready { - klog.V(3).Infof("Connected") - return conn, nil - } - klog.V(4).Infof("Still trying, connection is %s", conn.GetState()) - } +func Probe(conn *grpc.ClientConn, singleCallTimeout time.Duration) error { + return connection.ProbeForever(conn, singleCallTimeout) } func GetDriverName(conn *grpc.ClientConn, timeout time.Duration) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - - client := csi.NewIdentityClient(conn) - - req := csi.GetPluginInfoRequest{} - - rsp, err := client.GetPluginInfo(ctx, &req) - if err != nil { - return "", err - } - name := rsp.GetName() - if name == "" { - return "", fmt.Errorf("name is empty") - } - return name, nil + return connection.GetDriverName(ctx, conn) } func getDriverCapabilities(conn *grpc.ClientConn, timeout time.Duration) (sets.Int, error) { @@ -248,30 +210,16 @@ func getDriverCapabilities(conn *grpc.ClientConn, timeout time.Duration) (sets.I } capabilities := make(sets.Int) - for _, cap := range pluginCaps { - if cap == nil { - continue - } - service := cap.GetService() - if service == nil { - continue - } - switch service.GetType() { + for cap := range pluginCaps { + switch cap { case csi.PluginCapability_Service_CONTROLLER_SERVICE: capabilities.Insert(PluginCapability_CONTROLLER_SERVICE) case csi.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS: capabilities.Insert(PluginCapability_ACCESSIBILITY_CONSTRAINTS) } } - for _, cap := range controllerCaps { - if cap == nil { - continue - } - rpc := cap.GetRpc() - if rpc == nil { - continue - } - switch rpc.GetType() { + for cap := range controllerCaps { + switch cap { case csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME: capabilities.Insert(ControllerCapability_CREATE_DELETE_VOLUME) case csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT: @@ -281,32 +229,16 @@ func getDriverCapabilities(conn *grpc.ClientConn, timeout time.Duration) (sets.I return capabilities, nil } -func getPluginCapabilities(conn *grpc.ClientConn, timeout time.Duration) ([]*csi.PluginCapability, error) { +func getPluginCapabilities(conn *grpc.ClientConn, timeout time.Duration) (connection.PluginCapabilitySet, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - - client := csi.NewIdentityClient(conn) - req := csi.GetPluginCapabilitiesRequest{} - - rsp, err := client.GetPluginCapabilities(ctx, &req) - if err != nil { - return nil, err - } - return rsp.GetCapabilities(), nil + return connection.GetPluginCapabilities(ctx, conn) } -func getControllerCapabilities(conn *grpc.ClientConn, timeout time.Duration) ([]*csi.ControllerServiceCapability, error) { +func getControllerCapabilities(conn *grpc.ClientConn, timeout time.Duration) (connection.ControllerCapabilitySet, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - - client := csi.NewControllerClient(conn) - req := csi.ControllerGetCapabilitiesRequest{} - - rsp, err := client.ControllerGetCapabilities(ctx, &req) - if err != nil { - return nil, err - } - return rsp.GetCapabilities(), nil + return connection.GetControllerCapabilities(ctx, conn) } // NewCSIProvisioner creates new CSI provisioner diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index 7211ab0606..a066ccb2dc 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -20,6 +20,9 @@ import ( "context" "errors" "fmt" + "io/ioutil" + "os" + "path/filepath" "reflect" "strconv" "testing" @@ -27,6 +30,7 @@ import ( "github.com/container-storage-interface/spec/lib/go/csi" "github.com/golang/mock/gomock" + "github.com/kubernetes-csi/csi-lib-utils/connection" "github.com/kubernetes-csi/csi-test/driver" "github.com/kubernetes-csi/external-provisioner/pkg/features" crdv1 "github.com/kubernetes-csi/external-snapshotter/pkg/apis/volumesnapshot/v1alpha1" @@ -62,8 +66,8 @@ type csiConnection struct { conn *grpc.ClientConn } -func New(address string, timeout time.Duration) (csiConnection, error) { - conn, err := Connect(address, timeout) +func New(address string) (csiConnection, error) { + conn, err := connection.Connect(address) if err != nil { return csiConnection{}, err } @@ -72,7 +76,7 @@ func New(address string, timeout time.Duration) (csiConnection, error) { }, nil } -func createMockServer(t *testing.T) (*gomock.Controller, +func createMockServer(t *testing.T, tmpdir string) (*gomock.Controller, *driver.MockCSIDriver, *driver.MockIdentityServer, *driver.MockControllerServer, @@ -85,11 +89,11 @@ func createMockServer(t *testing.T) (*gomock.Controller, Identity: identityServer, Controller: controllerServer, }) - drv.Start() + drv.StartOnAddress("unix", filepath.Join(tmpdir, "csi.sock")) // Create a client connection to it addr := drv.Address() - csiConn, err := New(addr, timeout) + csiConn, err := New(addr) if err != nil { return nil, nil, nil, nil, csiConnection{}, err } @@ -97,6 +101,14 @@ func createMockServer(t *testing.T) (*gomock.Controller, return mockController, drv, identityServer, controllerServer, csiConn, nil } +func tempDir(t *testing.T) string { + dir, err := ioutil.TempDir("", "external-attacher-test-") + if err != nil { + t.Fatalf("Cannot create temporary directory: %s", err) + } + return dir +} + func TestGetPluginName(t *testing.T) { test := struct { name string @@ -121,7 +133,9 @@ func TestGetPluginName(t *testing.T) { }, } - mockController, driver, identityServer, _, csiConn, err := createMockServer(t) + tmpdir := tempDir(t) + defer os.RemoveAll(tmpdir) + mockController, driver, identityServer, _, csiConn, err := createMockServer(t, tmpdir) if err != nil { t.Fatal(err) } @@ -326,7 +340,9 @@ func TestGetDriverCapabilities(t *testing.T) { }, }...) - mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t) + tmpdir := tempDir(t) + defer os.RemoveAll(tmpdir) + mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t, tmpdir) if err != nil { t.Fatal(err) } @@ -448,7 +464,9 @@ func TestGetDriverName(t *testing.T) { }, } - mockController, driver, identityServer, _, csiConn, err := createMockServer(t) + tmpdir := tempDir(t) + defer os.RemoveAll(tmpdir) + mockController, driver, identityServer, _, csiConn, err := createMockServer(t, tmpdir) if err != nil { t.Fatal(err) } @@ -527,7 +545,10 @@ func TestBytesToQuantity(t *testing.T) { func TestCreateDriverReturnsInvalidCapacityDuringProvision(t *testing.T) { // Set up mocks var requestedBytes int64 = 100 - mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t) + + tmpdir := tempDir(t) + defer os.RemoveAll(tmpdir) + mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t, tmpdir) if err != nil { t.Fatal(err) } @@ -1349,7 +1370,9 @@ func newSnapshot(name, className, boundToContent, snapshotUID, claimName string, func runProvisionTest(t *testing.T, k string, tc provisioningTestcase, requestedBytes int64) { t.Logf("Running test: %v", k) - mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t) + tmpdir := tempDir(t) + defer os.RemoveAll(tmpdir) + mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t, tmpdir) if err != nil { t.Fatal(err) } @@ -1712,7 +1735,9 @@ func TestProvisionFromSnapshot(t *testing.T) { }, } - mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t) + tmpdir := tempDir(t) + defer os.RemoveAll(tmpdir) + mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t, tmpdir) if err != nil { t.Fatal(err) } @@ -1820,7 +1845,10 @@ func TestProvisionWithTopology(t *testing.T) { } const requestBytes = 100 - mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t) + + tmpdir := tempDir(t) + defer os.RemoveAll(tmpdir) + mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t, tmpdir) if err != nil { t.Fatal(err) } @@ -1858,7 +1886,10 @@ func TestProvisionWithTopology(t *testing.T) { func TestProvisionWithMountOptions(t *testing.T) { expectedOptions := []string{"foo=bar", "baz=qux"} const requestBytes = 100 - mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t) + + tmpdir := tempDir(t) + defer os.RemoveAll(tmpdir) + mockController, driver, identityServer, controllerServer, csiConn, err := createMockServer(t, tmpdir) if err != nil { t.Fatal(err) } diff --git a/vendor/github.com/kubernetes-csi/csi-lib-utils/connection/connection.go b/vendor/github.com/kubernetes-csi/csi-lib-utils/connection/connection.go new file mode 100644 index 0000000000..588826ee38 --- /dev/null +++ b/vendor/github.com/kubernetes-csi/csi-lib-utils/connection/connection.go @@ -0,0 +1,310 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package connection + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "net" + "strings" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/container-storage-interface/spec/lib/go/csi" + "github.com/kubernetes-csi/csi-lib-utils/protosanitizer" + "google.golang.org/grpc" + "k8s.io/klog" +) + +const ( + // Interval of logging connection errors + connectionLoggingInterval = 10 * time.Second + + // Interval of trying to call Probe() until it succeeds + probeInterval = 1 * time.Second +) + +const terminationLogPath = "/dev/termination-log" + +// Connect opens insecure gRPC connection to a CSI driver. Address must be either absolute path to UNIX domain socket +// file or have format '://', following gRPC name resolution mechanism at +// https://github.com/grpc/grpc/blob/master/doc/naming.md. +// +// The function tries to connect indefinitely every second until it connects. The function automatically disables TLS +// and adds interceptor for logging of all gRPC messages at level 5. +// +// For a connection to a Unix Domain socket, the behavior after +// loosing the connection is configurable. The default is to +// log the connection loss and reestablish a connection. Applications +// which need to know about a connection loss can be notified by +// passing a callback with OnConnectionLoss and in that callback +// can decide what to do: +// - exit the application with os.Exit +// - invalidate cached information +// - disable the reconnect, which will cause all gRPC method calls to fail with status.Unavailable +// +// For other connections, the default behavior from gRPC is used and +// loss of connection is not detected reliably. +func Connect(address string, options ...Option) (*grpc.ClientConn, error) { + return connect(address, []grpc.DialOption{}, options) +} + +// Option is the type of all optional parameters for Connect. +type Option func(o *options) + +// OnConnectionLoss registers a callback that will be invoked when the +// connection got lost. If that callback returns true, the connection +// is reestablished. Otherwise the connection is left as it is and +// all future gRPC calls using it will fail with status.Unavailable. +func OnConnectionLoss(reconnect func() bool) Option { + return func(o *options) { + o.reconnect = reconnect + } +} + +// ExitOnConnectionLoss returns callback for OnConnectionLoss() that writes +// an error to /dev/termination-log and exits. +func ExitOnConnectionLoss() func() bool { + return func() bool { + terminationMsg := "Lost connection to CSI driver, exiting" + if err := ioutil.WriteFile(terminationLogPath, []byte(terminationMsg), 0644); err != nil { + klog.Errorf("%s: %s", terminationLogPath, err) + } + klog.Fatalf(terminationMsg) + return false + } +} + +type options struct { + reconnect func() bool +} + +// connect is the internal implementation of Connect. It has more options to enable testing. +func connect(address string, dialOptions []grpc.DialOption, connectOptions []Option) (*grpc.ClientConn, error) { + var o options + for _, option := range connectOptions { + option(&o) + } + + dialOptions = append(dialOptions, + grpc.WithInsecure(), // Don't use TLS, it's usually local Unix domain socket in a container. + grpc.WithBackoffMaxDelay(time.Second), // Retry every second after failure. + grpc.WithBlock(), // Block until connection succeeds. + grpc.WithUnaryInterceptor(LogGRPC), // Log all messages. + ) + unixPrefix := "unix://" + if strings.HasPrefix(address, "/") { + // It looks like filesystem path. + address = unixPrefix + address + } + + if strings.HasPrefix(address, unixPrefix) { + // state variables for the custom dialer + haveConnected := false + lostConnection := false + reconnect := true + + dialOptions = append(dialOptions, grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { + if haveConnected && !lostConnection { + // We have detected a loss of connection for the first time. Decide what to do... + // Record this once. TODO (?): log at regular time intervals. + klog.Errorf("Lost connection to %s.", address) + // Inform caller and let it decide? Default is to reconnect. + if o.reconnect != nil { + reconnect = o.reconnect() + } + lostConnection = true + } + if !reconnect { + return nil, errors.New("connection lost, reconnecting disabled") + } + conn, err := net.DialTimeout("unix", address[len(unixPrefix):], timeout) + if err == nil { + // Connection restablished. + haveConnected = true + lostConnection = false + } + return conn, err + })) + } else if o.reconnect != nil { + return nil, errors.New("OnConnectionLoss callback only supported for unix:// addresses") + } + + klog.Infof("Connecting to %s", address) + + // Connect in background. + var conn *grpc.ClientConn + var err error + ready := make(chan bool) + go func() { + conn, err = grpc.Dial(address, dialOptions...) + close(ready) + }() + + // Log error every connectionLoggingInterval + ticker := time.NewTicker(connectionLoggingInterval) + defer ticker.Stop() + + // Wait until Dial() succeeds. + for { + select { + case <-ticker.C: + klog.Warningf("Still connecting to %s", address) + + case <-ready: + return conn, err + } + } +} + +// LogGRPC is gPRC unary interceptor for logging of CSI messages at level 5. It removes any secrets from the message. +func LogGRPC(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + klog.V(5).Infof("GRPC call: %s", method) + klog.V(5).Infof("GRPC request: %s", protosanitizer.StripSecrets(req)) + err := invoker(ctx, method, req, reply, cc, opts...) + klog.V(5).Infof("GRPC response: %s", protosanitizer.StripSecrets(reply)) + klog.V(5).Infof("GRPC error: %v", err) + return err +} + +// GetDriverName returns name of CSI driver. +func GetDriverName(ctx context.Context, conn *grpc.ClientConn) (string, error) { + client := csi.NewIdentityClient(conn) + + req := csi.GetPluginInfoRequest{} + rsp, err := client.GetPluginInfo(ctx, &req) + if err != nil { + return "", err + } + name := rsp.GetName() + if name == "" { + return "", fmt.Errorf("driver name is empty") + } + return name, nil +} + +// PluginCapabilitySet is set of CSI plugin capabilities. Only supported capabilities are in the map. +type PluginCapabilitySet map[csi.PluginCapability_Service_Type]bool + +// GetPluginCapabilities returns set of supported capabilities of CSI driver. +func GetPluginCapabilities(ctx context.Context, conn *grpc.ClientConn) (PluginCapabilitySet, error) { + client := csi.NewIdentityClient(conn) + req := csi.GetPluginCapabilitiesRequest{} + rsp, err := client.GetPluginCapabilities(ctx, &req) + if err != nil { + return nil, err + } + caps := PluginCapabilitySet{} + for _, cap := range rsp.GetCapabilities() { + if cap == nil { + continue + } + srv := cap.GetService() + if srv == nil { + continue + } + t := srv.GetType() + caps[t] = true + } + return caps, nil +} + +// ControllerCapabilitySet is set of CSI controller capabilities. Only supported capabilities are in the map. +type ControllerCapabilitySet map[csi.ControllerServiceCapability_RPC_Type]bool + +// GetControllerCapabilities returns set of supported controller capabilities of CSI driver. +func GetControllerCapabilities(ctx context.Context, conn *grpc.ClientConn) (ControllerCapabilitySet, error) { + client := csi.NewControllerClient(conn) + req := csi.ControllerGetCapabilitiesRequest{} + rsp, err := client.ControllerGetCapabilities(ctx, &req) + if err != nil { + return nil, err + } + + caps := ControllerCapabilitySet{} + for _, cap := range rsp.GetCapabilities() { + if cap == nil { + continue + } + rpc := cap.GetRpc() + if rpc == nil { + continue + } + t := rpc.GetType() + caps[t] = true + } + return caps, nil +} + +// ProbeForever calls Probe() of a CSI driver and waits until the driver becomes ready. +// Any error other than timeout is returned. +func ProbeForever(conn *grpc.ClientConn, singleProbeTimeout time.Duration) error { + for { + klog.Info("Probing CSI driver for readiness") + ready, err := probeOnce(conn, singleProbeTimeout) + if err != nil { + st, ok := status.FromError(err) + if !ok { + // This is not gRPC error. The probe must have failed before gRPC + // method was called, otherwise we would get gRPC error. + return fmt.Errorf("CSI driver probe failed: %s", err) + } + if st.Code() != codes.DeadlineExceeded { + return fmt.Errorf("CSI driver probe failed: %s", err) + } + // Timeout -> driver is not ready. Fall through to sleep() below. + klog.Warning("CSI driver probe timed out") + } else { + if ready { + return nil + } + klog.Warning("CSI driver is not ready") + } + // Timeout was returned or driver is not ready. + time.Sleep(probeInterval) + } +} + +// probeOnce is a helper to simplify defer cancel() +func probeOnce(conn *grpc.ClientConn, timeout time.Duration) (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return Probe(ctx, conn) +} + +// Probe calls driver Probe() just once and returns its result without any processing. +func Probe(ctx context.Context, conn *grpc.ClientConn) (ready bool, err error) { + client := csi.NewIdentityClient(conn) + + req := csi.ProbeRequest{} + rsp, err := client.Probe(ctx, &req) + + if err != nil { + return false, err + } + + r := rsp.GetReady() + if r == nil { + // "If not present, the caller SHALL assume that the plugin is in a ready state" + return true, nil + } + return r.GetValue(), nil +} diff --git a/vendor/github.com/kubernetes-csi/csi-lib-utils/protosanitizer/protosanitizer.go b/vendor/github.com/kubernetes-csi/csi-lib-utils/protosanitizer/protosanitizer.go index d75e0e2123..af64a7b27d 100644 --- a/vendor/github.com/kubernetes-csi/csi-lib-utils/protosanitizer/protosanitizer.go +++ b/vendor/github.com/kubernetes-csi/csi-lib-utils/protosanitizer/protosanitizer.go @@ -24,10 +24,10 @@ import ( "reflect" "strings" - "github.com/container-storage-interface/spec/lib/go/csi" "github.com/golang/protobuf/descriptor" "github.com/golang/protobuf/proto" protobuf "github.com/golang/protobuf/protoc-gen-go/descriptor" + protobufdescriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" ) // StripSecrets returns a wrapper around the original CSI gRPC message @@ -36,15 +36,27 @@ import ( // Instead of the secret value(s), the string "***stripped***" is // included in the result. // +// StripSecrets relies on an extension in CSI 1.0 and thus can only +// be used for messages based on that or a more recent spec! +// // StripSecrets itself is fast and therefore it is cheap to pass the // result to logging functions which may or may not end up serializing // the parameter depending on the current log level. func StripSecrets(msg interface{}) fmt.Stringer { - return &stripSecrets{msg} + return &stripSecrets{msg, isCSI1Secret} +} + +// StripSecretsCSI03 is like StripSecrets, except that it works +// for messages based on CSI 0.3 and older. It does not work +// for CSI 1.0, use StripSecrets for that. +func StripSecretsCSI03(msg interface{}) fmt.Stringer { + return &stripSecrets{msg, isCSI03Secret} } type stripSecrets struct { msg interface{} + + isSecretField func(field *protobuf.FieldDescriptorProto) bool } func (s *stripSecrets) String() string { @@ -60,7 +72,7 @@ func (s *stripSecrets) String() string { } // Now remove secrets from the generic representation of the message. - strip(parsed, s.msg) + s.strip(parsed, s.msg) // Re-encoded the stripped representation and return that. b, err = json.Marshal(parsed) @@ -70,7 +82,7 @@ func (s *stripSecrets) String() string { return string(b) } -func strip(parsed interface{}, msg interface{}) { +func (s *stripSecrets) strip(parsed interface{}, msg interface{}) { protobufMsg, ok := msg.(descriptor.Message) if !ok { // Not a protobuf message, so we are done. @@ -93,8 +105,7 @@ func strip(parsed interface{}, msg interface{}) { fields := md.GetField() if fields != nil { for _, field := range fields { - ex, err := proto.GetExtension(field.Options, csi.E_CsiSecret) - if err == nil && ex != nil && *ex.(*bool) { + if s.isSecretField(field) { // Overwrite only if already set. if _, ok := parsedFields[field.GetName()]; ok { parsedFields[field.GetName()] = "***stripped***" @@ -126,13 +137,41 @@ func strip(parsed interface{}, msg interface{}) { if slice, ok := entry.([]interface{}); ok { // Array of values, like VolumeCapabilities in CreateVolumeRequest. for _, entry := range slice { - strip(entry, i) + s.strip(entry, i) } } else { // Single value. - strip(entry, i) + s.strip(entry, i) } } } } } + +// isCSI1Secret uses the csi.E_CsiSecret extension from CSI 1.0 to +// determine whether a field contains secrets. +func isCSI1Secret(field *protobuf.FieldDescriptorProto) bool { + ex, err := proto.GetExtension(field.Options, e_CsiSecret) + return err == nil && ex != nil && *ex.(*bool) +} + +// Copied from the CSI 1.0 spec (https://github.com/container-storage-interface/spec/blob/37e74064635d27c8e33537c863b37ccb1182d4f8/lib/go/csi/csi.pb.go#L4520-L4527) +// to avoid a package dependency that would prevent usage of this package +// in repos using an older version of the spec. +// +// Future revision of the CSI spec must not change this extensions, otherwise +// they will break filtering in binaries based on the 1.0 version of the spec. +var e_CsiSecret = &proto.ExtensionDesc{ + ExtendedType: (*protobufdescriptor.FieldOptions)(nil), + ExtensionType: (*bool)(nil), + Field: 1059, + Name: "csi.v1.csi_secret", + Tag: "varint,1059,opt,name=csi_secret,json=csiSecret", + Filename: "github.com/container-storage-interface/spec/csi.proto", +} + +// isCSI03Secret relies on the naming convention in CSI <= 0.3 +// to determine whether a field contains secrets. +func isCSI03Secret(field *protobuf.FieldDescriptorProto) bool { + return strings.HasSuffix(field.GetName(), "_secrets") +} diff --git a/vendor/github.com/kubernetes-csi/csi-lib-utils/release-tools/LICENSE b/vendor/github.com/kubernetes-csi/csi-lib-utils/release-tools/LICENSE new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/vendor/github.com/kubernetes-csi/csi-lib-utils/release-tools/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/kubernetes-csi/csi-test/driver/mock.go b/vendor/github.com/kubernetes-csi/csi-test/driver/mock.go index 9b051eee18..0eed51d170 100644 --- a/vendor/github.com/kubernetes-csi/csi-test/driver/mock.go +++ b/vendor/github.com/kubernetes-csi/csi-test/driver/mock.go @@ -46,9 +46,9 @@ func NewMockCSIDriver(servers *MockCSIDriverServers) *MockCSIDriver { } } -func (m *MockCSIDriver) Start() error { - // Listen on a port assigned by the net package - l, err := net.Listen("tcp", "127.0.0.1:0") +// StartOnAddress starts a new gRPC server listening on given address. +func (m *MockCSIDriver) StartOnAddress(network, address string) error { + l, err := net.Listen(network, address) if err != nil { return err } @@ -61,6 +61,12 @@ func (m *MockCSIDriver) Start() error { return nil } +// Start starts a new gRPC server listening on a random TCP loopback port. +func (m *MockCSIDriver) Start() error { + // Listen on a port assigned by the net package + return m.StartOnAddress("tcp", "127.0.0.1:0") +} + func (m *MockCSIDriver) Nexus() (*grpc.ClientConn, error) { // Start server err := m.Start()