<commit_before>package config

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/go-kit/kit/endpoint"
	"github.com/go-kit/kit/log"
	kitprom "github.com/go-kit/kit/metrics/prometheus"
	kithttp "github.com/go-kit/kit/transport/http"
	"github.com/gorilla/mux"
	"github.com/prometheus/client_golang/prometheus"

	"github.com/lifesum/configsum/pkg/errors"
)

type contextKey string

const (
	ctxKeyTimeBegin contextKey = "begin"
	ctxKeyRoute     contextKey = "route"
)

// Field names for metric labels.
const (
	labelComponent = "component"
	labelRoute     = "route"
	labelStatus    = "status"
)

var (
	namespace        = "configsum"
	subsystemRequest = "request"
	requestLabels    = []string{
		labelComponent,
		labelRoute,
		labelStatus,
	}
)

// Headers.
const (
	headerContentType = "Content-Type"
	headerBaseID      = "X-Configsum-Base-Id"
	headerBaseName    = "X-Configsum-Base-Name"
	headerClientID    = "X-Configsum-Client-Id"
	headerID          = "X-Configsum-Id"
	headerCreatedAt   = "X-Configsum-Created"
)

// MakeHandler returns an http.Handler for the config service.
func MakeHandler(
	logger log.Logger,
	svc ServiceUser,
	auth endpoint.Middleware,
	opts ...kithttp.ServerOption,
) http.Handler {
	r := mux.NewRouter()
	r.StrictSlash(true)

	opts = append(
		opts,
		kithttp.ServerBefore(kithttp.PopulateRequestContext),
		kithttp.ServerBefore(addBeginTime),
		kithttp.ServerBefore(addRoute),
		kithttp.ServerErrorEncoder(encodeError),
		kithttp.ServerErrorLogger(log.With(logger, "route", "configUser")),
		kithttp.ServerFinalizer(serverFinalizer(log.With(logger, "route", "configUser"))),
	)

	r.Methods("PUT").Path(`/{baseConfig:[a-z0-9\-]+}`).Name("configUser").Handler(
		kithttp.NewServer(
			auth(userEndpoint(svc)),
			decodeUserRequest,
			encodeUserResponse,
			opts...,
		),
	)

	return r
}

func decodeUserRequest(ctx context.Context, r *http.Request) (interface{}, error) {
	c, ok := mux.Vars(r)["baseConfig"]
	if !ok {
		return nil, fmt.Errorf("Baseconfig missing")
	}

	return userRequest{baseConfig: c}, nil
}

func encodeUserResponse(
	_ context.Context,
	w http.ResponseWriter,
	response interface{},
) error {
	r := response.(userResponse)

	w.Header().Set(headerContentType, "application/json; charset=utf-8")
	w.Header().Set(headerBaseID, r.baseID)
	w.Header().Set(headerBaseName, r.baseName)
	w.Header().Set(headerClientID, r.clientID)
	w.Header().Set(headerID, r.id)
	w.Header().Set(headerCreatedAt, r.createdAt.Format(time.RFC3339Nano))

	return json.NewEncoder(w).Encode(r.rendered)
}

func encodeError(_ context.Context, err error, w http.ResponseWriter) {
	switch errors.Cause(err) {
	case errors.ErrNotFound:
		w.WriteHeader(http.StatusNotFound)
	case errors.ErrClientNotFound, errors.ErrSecretMissing:
		w.WriteHeader(http.StatusUnauthorized)
	case errors.ErrSignatureMissing, errors.ErrSignatureMissmatch, errors.ErrUserIDMissing:
		w.WriteHeader(http.StatusUnauthorized)
	default:
		w.WriteHeader(http.StatusInternalServerError)
	}

	w.Header().Set(headerContentType, "application/json; charset=utf-8")

	_ = json.NewEncoder(w).Encode(struct {
		Reason string `json:"reason"`
	}{
		Reason: err.Error(),
	})
}

func addBeginTime(ctx context.Context, r *http.Request) context.Context {
	return context.WithValue(ctx, ctxKeyTimeBegin, time.Now())
}

func addRoute(ctx context.Context, r *http.Request) context.Context {
	route := "unknown"

	if current := mux.CurrentRoute(r); current != nil {
		route = current.GetName()
	}

	return context.WithValue(ctx, ctxKeyRoute, route)
}

func serverFinalizer(logger log.Logger) kithttp.ServerFinalizerFunc {
	return func(ctx context.Context, code int, r *http.Request) {
		var (
			timeBegin = time.Since(ctx.Value(ctxKeyTimeBegin).(time.Time)).Seconds()
		)

		_ = logger.Log(
			"duration", timeBegin,
			"request", map[string]interface{}{
				"authorization":    ctx.Value(kithttp.ContextKeyRequestAuthorization),
				"header":           r.Header,
				"host":             ctx.Value(kithttp.ContextKeyRequestHost),
				"method":           ctx.Value(kithttp.ContextKeyRequestMethod),
				"path":             ctx.Value(kithttp.ContextKeyRequestPath),
				"proto":            ctx.Value(kithttp.ContextKeyRequestProto),
				"referer":          ctx.Value(kithttp.ContextKeyRequestReferer),
				"remoteAddr":       ctx.Value(kithttp.ContextKeyRequestRemoteAddr),
				"requestId":        ctx.Value(kithttp.ContextKeyRequestXRequestID),
				"requestUri":       ctx.Value(kithttp.ContextKeyRequestURI),
				"transferEncoding": r.TransferEncoding,
			},
			"response", map[string]interface{}{
				"header":     ctx.Value(kithttp.ContextKeyResponseHeaders).(http.Header),
				"size":       ctx.Value(kithttp.ContextKeyResponseSize).(int64),
				"statusCode": code,
			},
		)

		requestLatency := kitprom.NewHistogramFrom(
			prometheus.HistogramOpts{
				Namespace: namespace,
				Subsystem: subsystemRequest,
				Name:      "req_latency_seconds",
				Help:      "Total duration of requests in seconds",
			},
			requestLabels,
		)

		requestLatency.With(
			labelComponent, "request",
			labelRoute, ctx.Value(ctxKeyRoute).(string),
			labelStatus, strconv.Itoa(code),
		).Observe(timeBegin)
	}
}
<commit_msg>Extend request metrics to include request counter<commit_after>package config

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/go-kit/kit/endpoint"
	"github.com/go-kit/kit/log"
	kitprom "github.com/go-kit/kit/metrics/prometheus"
	kithttp "github.com/go-kit/kit/transport/http"
	"github.com/gorilla/mux"
	"github.com/prometheus/client_golang/prometheus"

	"github.com/lifesum/configsum/pkg/errors"
)

type contextKey string

const (
	ctxKeyTimeBegin contextKey = "begin"
	ctxKeyRoute     contextKey = "route"
)

// Field names for metric labels.
const (
	labelComponent = "component"
	labelRoute     = "route"
	labelStatus    = "status"
)

var (
	namespace        = "configsum"
	subsystemRequest = "request"
	requestLabels    = []string{
		labelComponent,
		labelRoute,
		labelStatus,
	}
)

// Headers.
const (
	headerContentType = "Content-Type"
	headerBaseID      = "X-Configsum-Base-Id"
	headerBaseName    = "X-Configsum-Base-Name"
	headerClientID    = "X-Configsum-Client-Id"
	headerID          = "X-Configsum-Id"
	headerCreatedAt   = "X-Configsum-Created"
)

// MakeHandler returns an http.Handler for the config service.
func MakeHandler(
	logger log.Logger,
	svc ServiceUser,
	auth endpoint.Middleware,
	opts ...kithttp.ServerOption,
) http.Handler {
	r := mux.NewRouter()
	r.StrictSlash(true)

	opts = append(
		opts,
		kithttp.ServerBefore(kithttp.PopulateRequestContext),
		kithttp.ServerBefore(addBeginTime),
		kithttp.ServerBefore(addRoute),
		kithttp.ServerErrorEncoder(encodeError),
		kithttp.ServerErrorLogger(log.With(logger, "route", "configUser")),
		kithttp.ServerFinalizer(serverFinalizer(log.With(logger, "route", "configUser"))),
	)

	r.Methods("PUT").Path(`/{baseConfig:[a-z0-9\-]+}`).Name("configUser").Handler(
		kithttp.NewServer(
			auth(userEndpoint(svc)),
			decodeUserRequest,
			encodeUserResponse,
			opts...,
		),
	)

	return r
}

func decodeUserRequest(ctx context.Context, r *http.Request) (interface{}, error) {
	c, ok := mux.Vars(r)["baseConfig"]
	if !ok {
		return nil, fmt.Errorf("Baseconfig missing")
	}

	return userRequest{baseConfig: c}, nil
}

func encodeUserResponse(
	_ context.Context,
	w http.ResponseWriter,
	response interface{},
) error {
	r := response.(userResponse)

	w.Header().Set(headerContentType, "application/json; charset=utf-8")
	w.Header().Set(headerBaseID, r.baseID)
	w.Header().Set(headerBaseName, r.baseName)
	w.Header().Set(headerClientID, r.clientID)
	w.Header().Set(headerID, r.id)
	w.Header().Set(headerCreatedAt, r.createdAt.Format(time.RFC3339Nano))

	return json.NewEncoder(w).Encode(r.rendered)
}

func encodeError(_ context.Context, err error, w http.ResponseWriter) {
	switch errors.Cause(err) {
	case errors.ErrNotFound:
		w.WriteHeader(http.StatusNotFound)
	case errors.ErrClientNotFound, errors.ErrSecretMissing:
		w.WriteHeader(http.StatusUnauthorized)
	case errors.ErrSignatureMissing, errors.ErrSignatureMissmatch, errors.ErrUserIDMissing:
		w.WriteHeader(http.StatusUnauthorized)
	default:
		w.WriteHeader(http.StatusInternalServerError)
	}

	w.Header().Set(headerContentType, "application/json; charset=utf-8")

	_ = json.NewEncoder(w).Encode(struct {
		Reason string `json:"reason"`
	}{
		Reason: err.Error(),
	})
}

func addBeginTime(ctx context.Context, r *http.Request) context.Context {
	return context.WithValue(ctx, ctxKeyTimeBegin, time.Now())
}

func addRoute(ctx context.Context, r *http.Request) context.Context {
	route := "unknown"

	if current := mux.CurrentRoute(r); current != nil {
		route = current.GetName()
	}

	return context.WithValue(ctx, ctxKeyRoute, route)
}

func serverFinalizer(logger log.Logger) kithttp.ServerFinalizerFunc {
	return func(ctx context.Context, code int, r *http.Request) {
		var (
			timeBegin  = time.Since(ctx.Value(ctxKeyTimeBegin).(time.Time)).Seconds()
			route      = ctx.Value(ctxKeyRoute).(string)
			statusCode = strconv.Itoa(code)
		)

		_ = logger.Log(
			"duration", timeBegin,
			"request", map[string]interface{}{
				"authorization":    ctx.Value(kithttp.ContextKeyRequestAuthorization),
				"header":           r.Header,
				"host":             ctx.Value(kithttp.ContextKeyRequestHost),
				"method":           ctx.Value(kithttp.ContextKeyRequestMethod),
				"path":             ctx.Value(kithttp.ContextKeyRequestPath),
				"proto":            ctx.Value(kithttp.ContextKeyRequestProto),
				"referer":          ctx.Value(kithttp.ContextKeyRequestReferer),
				"remoteAddr":       ctx.Value(kithttp.ContextKeyRequestRemoteAddr),
				"requestId":        ctx.Value(kithttp.ContextKeyRequestXRequestID),
				"requestUri":       ctx.Value(kithttp.ContextKeyRequestURI),
				"transferEncoding": r.TransferEncoding,
			},
			"response", map[string]interface{}{
				"header":     ctx.Value(kithttp.ContextKeyResponseHeaders).(http.Header),
				"size":       ctx.Value(kithttp.ContextKeyResponseSize).(int64),
				"statusCode": code,
			},
		)

		requestLatency := kitprom.NewHistogramFrom(
			prometheus.HistogramOpts{
				Namespace: namespace,
				Subsystem: subsystemRequest,
				Name:      "req_latency_seconds",
				Help:      "Total duration of requests in seconds",
			},
			requestLabels,
		)

		requestLatency.With(
			labelComponent, "request",
			labelRoute, route,
			labelStatus, statusCode,
		).Observe(timeBegin)

		requestCount := kitprom.NewCounterFrom(
			prometheus.CounterOpts{
				Namespace: namespace,
				Subsystem: subsystemRequest,
				Name:      "req_count",
				Help:      "Number of requests received",
			}, requestLabels)

		requestCount.With(
			labelComponent, "request",
			labelRoute, route,
			labelStatus, statusCode,
		).Add(1)
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2018 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 service

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"

	"github.com/akutz/gofsutil"
	"github.com/container-storage-interface/spec/lib/go/csi"
	"golang.org/x/net/context"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const (
	devDiskID   = "/dev/disk/by-id"
	blockPrefix = "wwn-0x"
)

func (s *service) NodeStageVolume(
	ctx context.Context,
	req *csi.NodeStageVolumeRequest) (
	*csi.NodeStageVolumeResponse, error) {

	volID := req.GetVolumeId()
	if volID == "" {
		return nil, status.Error(codes.InvalidArgument,
			"Volume ID required")
	}

	// Check that volume exists and is accessible
	volPath, err := getDiskPath(volID, nil)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"Error trying to read attached disks: %v", err)
	}
	if volPath == "" {
		return nil, status.Errorf(codes.NotFound,
			"Volume ID: %s not attached to node", volID)
	}

	// Check that block device looks good
	dev, err := getDevice(volPath)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"error getting block device for volume: %s, err: %s",
			volID, err.Error())
	}

	// Check that target_path is created by CO and is a directory
	target := req.GetStagingTargetPath()
	if target == "" {
		return nil, status.Error(codes.InvalidArgument,
			"target path required")
	}

	tgtStat, err := os.Stat(target)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, status.Errorf(codes.FailedPrecondition,
				"stage volume, target: %s not pre-created", target)
		}
		return nil, status.Errorf(codes.Internal,
			"failed to stat target, err: %s", err.Error())
	}

	// This check is mandated by the spec, but this would/should faile if the
	// volume has a block accessType. Mayvbe staging isn't intended to be used
	// with block? That would make sense you can share the volume for block.
	if !tgtStat.IsDir() {
		return nil, status.Errorf(codes.FailedPrecondition,
			"existing path: %s is not a directory", target)
	}

	//Mount if the device if needed, and if already mounted, verify compatibility
	volCap := req.GetVolumeCapability()
	mountVol := volCap.GetMount()
	if mountVol == nil {
		return nil, status.Error(codes.InvalidArgument,
			"Only Mount access type supported")
	}
	fs := mountVol.GetFsType()
	mntFlags := mountVol.GetMountFlags()

	accMode := volCap.GetAccessMode().GetMode()
	ro := false
	if accMode == csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY ||
		accMode == csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY {
		ro = true
	}

	// Get mounts to check if already staged
	mnts, err := gofsutil.GetDevMounts(context.Background(), dev.RealDev)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"could not reliably determine existing mount status: %s",
			err.Error())
	}

	if len(mnts) == 0 {
		// Device isn't mounted anywhere, stage the volume
		if fs == "" {
			fs = "ext4"
		}

		// If read-only access mode, we don't allow formatting
		if ro {
			mntFlags = append(mntFlags, "ro")
			if err := gofsutil.Mount(ctx, dev.FullPath, target, fs, mntFlags...); err != nil {
				return nil, status.Errorf(codes.Internal,
					"error with mount during staging: %s",
					err.Error())
			}
			return &csi.NodeStageVolumeResponse{}, nil
		}
		if err := gofsutil.FormatAndMount(ctx, dev.FullPath, target, fs, mntFlags...); err != nil {
			return nil, status.Errorf(codes.Internal,
				"error with format and mount during staging: %s",
				err.Error())
		}
		return &csi.NodeStageVolumeResponse{}, nil

	}
	// Device is already mounted. Need to ensure that it is already
	// mounted to the expected staging target, with correct rw/ro perms
	mounted := false
	for _, m := range mnts {
		if m.Path == target {
			mounted = true
			rwo := "rw"
			if ro {
				rwo = "ro"
			}
			if contains(m.Opts, rwo) {
				//TODO make sure that mount options match
				//log.WithFields(f).Debug(
				//	"private mount already in place")
				return &csi.NodeStageVolumeResponse{}, nil
			}
			return nil, status.Error(codes.AlreadyExists,
				"access mode conflicts with existing mount")
		}
	}
	if !mounted {
		return nil, status.Error(codes.Internal,
			"device already in use and mounted elsewhere")
	}

	return nil, nil
}

func (s *service) NodeUnstageVolume(
	ctx context.Context,
	req *csi.NodeUnstageVolumeRequest) (
	*csi.NodeUnstageVolumeResponse, error) {

	volID := req.GetVolumeId()
	if volID == "" {
		return nil, status.Error(codes.InvalidArgument,
			"Volume ID required")
	}

	// Check that volume is attached
	volPath, err := getDiskPath(volID, nil)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"Error trying to read attached disks: %v", err)
	}
	if volPath == "" {
		return nil, status.Errorf(codes.NotFound,
			"Volume ID: %s not attached to node", volID)
	}

	target := req.GetStagingTargetPath()
	if target == "" {
		return nil, status.Error(codes.InvalidArgument,
			"target path required")
	}

	// Check that block device looks good
	dev, err := getDevice(volPath)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"error getting block device for volume: %s, err: %s",
			volID, err.Error())
	}

	// Get mounts for device
	mnts, err := gofsutil.GetDevMounts(context.Background(), dev.RealDev)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"could not reliably determine existing mount status: %s",
			err.Error())
	}

	if len(mnts) == 0 {
		// device isn't mounted, so this has been unstaged already
		return &csi.NodeUnstageVolumeResponse{}, nil
	}

	// device is mounted. Should only be mounted to target
	if len(mnts) > 1 {
		return nil, status.Errorf(codes.Internal,
			"volume: %s appears mounted in multiple places", volID)
	}

	if mnts[0].Source == dev.RealDev && mnts[0].Path == target {
		// perfect, unstage this
		if err := gofsutil.Unmount(context.Background(), target); err != nil {
			return nil, status.Errorf(codes.Internal,
				"Error unmounting target: %s", err.Error())
		}
	} else {
		return nil, status.Errorf(codes.Internal,
			"volume %s is mounted someplace other than target: %s, mounted to: %s",
			volID, target, mnts[0].Path)
	}

	return &csi.NodeUnstageVolumeResponse{}, nil
}

func (s *service) NodePublishVolume(
	ctx context.Context,
	req *csi.NodePublishVolumeRequest) (
	*csi.NodePublishVolumeResponse, error) {

	return nil, nil
}

func (s *service) NodeUnpublishVolume(
	ctx context.Context,
	req *csi.NodeUnpublishVolumeRequest) (
	*csi.NodeUnpublishVolumeResponse, error) {

	return nil, nil
}

func (s *service) NodeGetVolumeStats(
	ctx context.Context,
	req *csi.NodeGetVolumeStatsRequest) (
	*csi.NodeGetVolumeStatsResponse, error) {

	return nil, nil
}

func (s *service) NodeGetCapabilities(
	ctx context.Context,
	req *csi.NodeGetCapabilitiesRequest) (
	*csi.NodeGetCapabilitiesResponse, error) {

	return &csi.NodeGetCapabilitiesResponse{
		Capabilities: []*csi.NodeServiceCapability{
			&csi.NodeServiceCapability{
				Type: &csi.NodeServiceCapability_Rpc{
					Rpc: &csi.NodeServiceCapability_RPC{
						Type: csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
					},
				},
			},
		},
	}, nil
}

func (s *service) NodeGetInfo(
	ctx context.Context,
	req *csi.NodeGetInfoRequest) (
	*csi.NodeGetInfoResponse, error) {

	return nil, nil
}

// Device is a struct for holding details about a block device
type Device struct {
	FullPath string
	Name     string
	RealDev  string
}

// getDevice returns a Device struct with info about the given device, or
// an error if it doesn't exist or is not a block device
func getDevice(path string) (*Device, error) {

	fi, err := os.Lstat(path)
	if err != nil {
		return nil, err
	}

	// eval any symlinks and make sure it points to a device
	d, err := filepath.EvalSymlinks(path)
	if err != nil {
		return nil, err
	}

	ds, err := os.Stat(d)
	if err != nil {
		return nil, err
	}
	dm := ds.Mode()
	if dm&os.ModeDevice == 0 {
		return nil, fmt.Errorf(
			"%s is not a block device", path)
	}

	return &Device{
		Name:     fi.Name(),
		FullPath: path,
		RealDev:  d,
	}, nil
}

// The files parameter is optional for testing purposes
func getDiskPath(id string, files []os.FileInfo) (string, error) {
	var (
		devs []os.FileInfo
		err  error
	)

	if files == nil {
		devs, err = ioutil.ReadDir(devDiskID)
		if err != nil {
			return "", err
		}
	} else {
		devs = files
	}

	targetDisk := blockPrefix + id

	for _, f := range devs {
		if f.Name() == targetDisk {
			return filepath.Join(devDiskID, f.Name()), nil
		}
	}

	return "", nil
}

func contains(list []string, item string) bool {
	for _, x := range list {
		if x == item {
			return true
		}
	}
	return false
}
<commit_msg>Implement Node publish/unpublish<commit_after>/*
Copyright 2018 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 service

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"

	"github.com/akutz/gofsutil"
	"github.com/container-storage-interface/spec/lib/go/csi"
	log "github.com/sirupsen/logrus"
	"golang.org/x/net/context"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const (
	devDiskID   = "/dev/disk/by-id"
	blockPrefix = "wwn-0x"
)

func (s *service) NodeStageVolume(
	ctx context.Context,
	req *csi.NodeStageVolumeRequest) (
	*csi.NodeStageVolumeResponse, error) {

	volID := req.GetVolumeId()
	volPath, err := verifyVolumeAttached(volID)
	if err != nil {
		return nil, err
	}

	// Check that block device looks good
	dev, err := getDevice(volPath)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"error getting block device for volume: %s, err: %s",
			volID, err.Error())
	}

	// Check that target_path is created by CO and is a directory
	target := req.GetStagingTargetPath()
	if err = verifyTargetDir(target); err != nil {
		return nil, err
	}

	//Mount if the device if needed, and if already mounted, verify compatibility
	volCap := req.GetVolumeCapability()
	fs, mntFlags, err := ensureMountVol(volCap)
	if err != nil {
		return nil, err
	}

	accMode := volCap.GetAccessMode().GetMode()
	ro := false
	if accMode == csi.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY ||
		accMode == csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY {
		ro = true
	}

	// Get mounts to check if already staged
	mnts, err := gofsutil.GetDevMounts(context.Background(), dev.RealDev)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"could not reliably determine existing mount status: %s",
			err.Error())
	}

	if len(mnts) == 0 {
		// Device isn't mounted anywhere, stage the volume
		if fs == "" {
			fs = "ext4"
		}

		// If read-only access mode, we don't allow formatting
		if ro {
			mntFlags = append(mntFlags, "ro")
			if err := gofsutil.Mount(ctx, dev.FullPath, target, fs, mntFlags...); err != nil {
				return nil, status.Errorf(codes.Internal,
					"error with mount during staging: %s",
					err.Error())
			}
			return &csi.NodeStageVolumeResponse{}, nil
		}
		if err := gofsutil.FormatAndMount(ctx, dev.FullPath, target, fs, mntFlags...); err != nil {
			return nil, status.Errorf(codes.Internal,
				"error with format and mount during staging: %s",
				err.Error())
		}
		return &csi.NodeStageVolumeResponse{}, nil

	}
	// Device is already mounted. Need to ensure that it is already
	// mounted to the expected staging target, with correct rw/ro perms
	mounted := false
	for _, m := range mnts {
		if m.Path == target {
			mounted = true
			rwo := "rw"
			if ro {
				rwo = "ro"
			}
			if contains(m.Opts, rwo) {
				//TODO make sure that mount options match
				//log.WithFields(f).Debug(
				//	"private mount already in place")
				return &csi.NodeStageVolumeResponse{}, nil
			}
			return nil, status.Error(codes.AlreadyExists,
				"access mode conflicts with existing mount")
		}
	}
	if !mounted {
		return nil, status.Error(codes.Internal,
			"device already in use and mounted elsewhere")
	}

	return nil, nil
}

func (s *service) NodeUnstageVolume(
	ctx context.Context,
	req *csi.NodeUnstageVolumeRequest) (
	*csi.NodeUnstageVolumeResponse, error) {

	volID := req.GetVolumeId()
	volPath, err := verifyVolumeAttached(volID)
	if err != nil {
		return nil, err
	}

	target := req.GetStagingTargetPath()
	if err = verifyTargetDir(target); err != nil {
		return nil, err
	}

	// Check that block device looks good
	dev, err := getDevice(volPath)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"error getting block device for volume: %s, err: %s",
			volID, err.Error())
	}

	// Get mounts for device
	mnts, err := gofsutil.GetDevMounts(context.Background(), dev.RealDev)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"could not reliably determine existing mount status: %s",
			err.Error())
	}

	if len(mnts) == 0 {
		// device isn't mounted, so this has been unstaged already
		return &csi.NodeUnstageVolumeResponse{}, nil
	}

	// device is mounted. Should only be mounted to target
	if len(mnts) > 1 {
		return nil, status.Errorf(codes.Internal,
			"volume: %s appears mounted in multiple places", volID)
	}

	if mnts[0].Source == dev.RealDev && mnts[0].Path == target {
		// perfect, unstage this
		if err := gofsutil.Unmount(context.Background(), target); err != nil {
			return nil, status.Errorf(codes.Internal,
				"Error unmounting target: %s", err.Error())
		}
	} else {
		return nil, status.Errorf(codes.Internal,
			"volume %s is mounted someplace other than target: %s, mounted to: %s",
			volID, target, mnts[0].Path)
	}

	return &csi.NodeUnstageVolumeResponse{}, nil
}

func (s *service) NodePublishVolume(
	ctx context.Context,
	req *csi.NodePublishVolumeRequest) (
	*csi.NodePublishVolumeResponse, error) {

	volID := req.GetVolumeId()
	volPath, err := verifyVolumeAttached(volID)
	if err != nil {
		return nil, err
	}

	target := req.GetTargetPath()
	// We are responsible for creating target dir, per spec
	_, err = mkdir(target)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"Unable to create target dir: %s, err: %v", target, err)
	}

	stagingTarget := req.GetStagingTargetPath()
	if err := verifyTargetDir(stagingTarget); err != nil {
		return nil, err
	}

	ro := req.GetReadonly()
	volCap := req.GetVolumeCapability()
	_, mntFlags, err := ensureMountVol(volCap)
	if err != nil {
		return nil, err
	}

	// Get underlying block device
	dev, err := getDevice(volPath)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"error getting block device for volume: %s, err: %s",
			volID, err.Error())
	}

	f := log.Fields{
		"volID":         volID,
		"volumePath":    dev.FullPath,
		"device":        dev.RealDev,
		"target":        target,
		"stagingTarget": stagingTarget,
	}

	// get block device mounts
	// Check if device is already mounted
	devMnts, err := getDevMounts(dev)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"could not reliably determine existing mount status: %s",
			err.Error())
	}

	// We expect that block device already staged, so there should be at least 1
	// mount already. if it's > 1, it may already be published
	if len(devMnts) > 1 {
		// check if publish is already there
		for _, m := range devMnts {
			if m.Path == target {
				// volume already published to target
				// if mount options look good, do nothing
				rwo := "rw"
				if ro {
					rwo = "ro"
				}
				if !contains(m.Opts, rwo) {
					return nil, status.Error(codes.AlreadyExists,
						"volume previously published with different options")
				}

				// Existing mount satisfies request
				log.WithFields(f).Debug("volume already published to target")
				return &csi.NodePublishVolumeResponse{}, nil
			}
		}
	} else if len(devMnts) == 0 {
		return nil, status.Errorf(codes.FailedPrecondition,
			"Volume ID: %s does not appear staged to %s", volID, stagingTarget)
	}

	// Do the bind mount to publish the volume
	if ro {
		mntFlags = append(mntFlags, "ro")
	}

	if err := gofsutil.BindMount(ctx, stagingTarget, target, mntFlags...); err != nil {
		return nil, status.Errorf(codes.Internal,
			"error publish volume to target path: %s",
			err.Error())
	}

	return &csi.NodePublishVolumeResponse{}, nil
}

func (s *service) NodeUnpublishVolume(
	ctx context.Context,
	req *csi.NodeUnpublishVolumeRequest) (
	*csi.NodeUnpublishVolumeResponse, error) {

	volID := req.GetVolumeId()
	volPath, err := verifyVolumeAttached(volID)
	if err != nil {
		return nil, err
	}

	target := req.GetTargetPath()
	if err := verifyTargetDir(target); err != nil {
		return nil, err
	}

	// Get underlying block device
	dev, err := getDevice(volPath)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"error getting block device for volume: %s, err: %s",
			volID, err.Error())
	}

	// get mounts
	// Check if device is already unmounted
	mnts, err := gofsutil.GetMounts(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal,
			"could not reliably determine existing mount status: %s",
			err.Error())
	}

	for _, m := range mnts {
		if m.Source == dev.RealDev || m.Device == dev.RealDev {
			if m.Path == target {
				err = gofsutil.Unmount(ctx, target)
				if err != nil {
					return nil, status.Errorf(codes.Internal,
						"Error unmounting target: %s", err.Error())
				} else {
					// directory should be empty
					log.WithField("path", target).Debug("removing directory")
					if err := os.Remove(target); err != nil {
						return nil, status.Errorf(codes.Internal,
							"Unable to remove target dir: %s, err: %v", target, err)
					}
				}
			}
		}
	}

	return &csi.NodeUnpublishVolumeResponse{}, nil
}

func (s *service) NodeGetVolumeStats(
	ctx context.Context,
	req *csi.NodeGetVolumeStatsRequest) (
	*csi.NodeGetVolumeStatsResponse, error) {

	return nil, nil
}

func (s *service) NodeGetCapabilities(
	ctx context.Context,
	req *csi.NodeGetCapabilitiesRequest) (
	*csi.NodeGetCapabilitiesResponse, error) {

	return &csi.NodeGetCapabilitiesResponse{
		Capabilities: []*csi.NodeServiceCapability{
			&csi.NodeServiceCapability{
				Type: &csi.NodeServiceCapability_Rpc{
					Rpc: &csi.NodeServiceCapability_RPC{
						Type: csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
					},
				},
			},
		},
	}, nil
}

func (s *service) NodeGetInfo(
	ctx context.Context,
	req *csi.NodeGetInfoRequest) (
	*csi.NodeGetInfoResponse, error) {

	return nil, nil
}

// Device is a struct for holding details about a block device
type Device struct {
	FullPath string
	Name     string
	RealDev  string
}

// getDevice returns a Device struct with info about the given device, or
// an error if it doesn't exist or is not a block device
func getDevice(path string) (*Device, error) {

	fi, err := os.Lstat(path)
	if err != nil {
		return nil, err
	}

	// eval any symlinks and make sure it points to a device
	d, err := filepath.EvalSymlinks(path)
	if err != nil {
		return nil, err
	}

	ds, err := os.Stat(d)
	if err != nil {
		return nil, err
	}
	dm := ds.Mode()
	if dm&os.ModeDevice == 0 {
		return nil, fmt.Errorf(
			"%s is not a block device", path)
	}

	return &Device{
		Name:     fi.Name(),
		FullPath: path,
		RealDev:  d,
	}, nil
}

// The files parameter is optional for testing purposes
func getDiskPath(id string, files []os.FileInfo) (string, error) {
	var (
		devs []os.FileInfo
		err  error
	)

	if files == nil {
		devs, err = ioutil.ReadDir(devDiskID)
		if err != nil {
			return "", err
		}
	} else {
		devs = files
	}

	targetDisk := blockPrefix + id

	for _, f := range devs {
		if f.Name() == targetDisk {
			return filepath.Join(devDiskID, f.Name()), nil
		}
	}

	return "", nil
}

func contains(list []string, item string) bool {
	for _, x := range list {
		if x == item {
			return true
		}
	}
	return false
}

func verifyVolumeAttached(volID string) (string, error) {
	if volID == "" {
		return "", status.Error(codes.InvalidArgument,
			"Volume ID required")
	}

	// Check that volume is attached
	volPath, err := getDiskPath(volID, nil)
	if err != nil {
		return "", status.Errorf(codes.Internal,
			"Error trying to read attached disks: %v", err)
	}
	if volPath == "" {
		return "", status.Errorf(codes.NotFound,
			"Volume ID: %s not attached to node", volID)
	}

	return volPath, nil
}

func verifyTargetDir(target string) error {
	if target == "" {
		return status.Error(codes.InvalidArgument,
			"target path required")
	}

	tgtStat, err := os.Stat(target)
	if err != nil {
		if os.IsNotExist(err) {
			return status.Errorf(codes.FailedPrecondition,
				"target: %s not pre-created", target)
		}
		return status.Errorf(codes.Internal,
			"failed to stat target, err: %s", err.Error())
	}

	// This check is mandated by the spec, but this would/should fail if the
	// volume has a block accessType. Maybe staging isn't intended to be used
	// with block? That would make sense you cannot share the volume for block.
	if !tgtStat.IsDir() {
		return status.Errorf(codes.FailedPrecondition,
			"existing path: %s is not a directory", target)
	}

	return nil
}

// mkdir creates the directory specified by path if needed.
// return pair is a bool flag of whether dir was created, and an error
func mkdir(path string) (bool, error) {
	st, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			if err := os.Mkdir(path, 0750); err != nil {
				log.WithField("dir", path).WithError(
					err).Error("Unable to create dir")
				return false, err
			}
			log.WithField("path", path).Debug("created directory")
			return true, nil
		}
		return false, err
	}
	if !st.IsDir() {
		return false, fmt.Errorf("existing path is not a directory")
	}
	return false, nil
}

func ensureMountVol(volCap *csi.VolumeCapability) (string, []string, error) {
	mountVol := volCap.GetMount()
	if mountVol == nil {
		return "", nil, status.Error(codes.InvalidArgument,
			"Only Mount access type supported")
	}
	fs := mountVol.GetFsType()
	mntFlags := mountVol.GetMountFlags()

	return fs, mntFlags, nil
}

// a wrapper around gofsutil.GetMounts that handles bind mounts
func getDevMounts(
	sysDevice *Device) ([]gofsutil.Info, error) {

	ctx := context.Background()
	devMnts := make([]gofsutil.Info, 0)

	mnts, err := gofsutil.GetMounts(ctx)
	if err != nil {
		return devMnts, err
	}
	for _, m := range mnts {
		if m.Device == sysDevice.RealDev || (m.Device == "devtmpfs" && m.Source == sysDevice.RealDev) {
			devMnts = append(devMnts, m)
		}
	}
	return devMnts, nil
}
<|endoftext|>


<commit_before>package filesystem

import (
	"context"
	"os"
	"path/filepath"
	"time"

	"github.com/pkg/errors"
)

const (
	// DefaultPollingInterval is the default watch polling interval, in seconds.
	DefaultPollingInterval = 10
)

// UnmarshalText implements the text unmarshalling interface used when loading
// from TOML files.
func (m *WatchMode) UnmarshalText(textBytes []byte) error {
	// Convert the bytes to a string.
	text := string(textBytes)

	// Convert to a VCS mode.
	switch text {
	case "portable":
		*m = WatchMode_WatchPortable
	case "force-poll":
		*m = WatchMode_WatchForcePoll
	default:
		return errors.Errorf("unknown watch mode specification: %s", text)
	}

	// Success.
	return nil
}

// Supported indicates whether or not a particular watch mode is a valid,
// non-default value.
func (m WatchMode) Supported() bool {
	switch m {
	case WatchMode_WatchPortable:
		return true
	case WatchMode_WatchForcePoll:
		return true
	default:
		return false
	}
}

// Description returns a human-readable description of a watch mode.
func (m WatchMode) Description() string {
	switch m {
	case WatchMode_WatchDefault:
		return "Default"
	case WatchMode_WatchPortable:
		return "Portable"
	case WatchMode_WatchForcePoll:
		return "Force Poll"
	default:
		return "Unknown"
	}
}

func fileInfoEqual(first, second os.FileInfo) bool {
	return first.Size() == second.Size() &&
		first.Mode() == second.Mode() &&
		first.ModTime().Equal(second.ModTime())
}

func poll(root string, existing map[string]os.FileInfo) (map[string]os.FileInfo, bool, error) {
	// Create our result map.
	result := make(map[string]os.FileInfo, len(existing))

	// Create a walk visitor.
	changed := false
	rootDoesNotExist := false
	visitor := func(path string, info os.FileInfo, err error) error {
		// If there's an error, then halt walking by returning it. Before doing
		// that though, determine if the error is due to the root not existing.
		// If that's the case, then we can create a valid result (an empty map)
		// as well as determine whether or not there's been a change.
		if err != nil {
			if path == root && os.IsNotExist(err) {
				changed = len(existing) > 0
				rootDoesNotExist = true
			}
			return err
		}

		// If this is an executability preservation or Unicode decomposition
		// test path, ignore it.
		if isExecutabilityTestPath(path) || isDecompositionTestPath(path) {
			return nil
		}

		// Insert the entry for this path.
		result[path] = info

		// Compare the entry for this path.
		if previous, ok := existing[path]; !ok {
			changed = true
		} else if !fileInfoEqual(info, previous) {
			changed = true
		}

		// Success.
		return nil
	}

	// Perform the walk. If it fails, and it's not due to the root not existing,
	// then we can't return a valid result and need to abort.
	if err := filepath.Walk(root, visitor); err != nil && !rootDoesNotExist {
		return nil, false, errors.Wrap(err, "unable to perform filesystem walk")
	}

	// Done.
	return result, changed, nil
}

// TODO: Document that this function closes the events channel when the watch
// is cancelled.
// TODO: Document that this function will always succeed in one way or another
// (it doesn't have any total failure modes) and won't exit until the associated
// context is cancelled.
// TODO: Document that the events channel must be buffered.
func Watch(context context.Context, root string, events chan struct{}, mode WatchMode, pollInterval uint32) {
	// Ensure that the events channel is buffered.
	if cap(events) < 1 {
		panic("watch channel should be buffered")
	}

	// Ensure that the events channel is closed when we're cancelled.
	defer close(events)

	// If we're in a recurisve home watch mode, attempt to watch in that manner.
	// This will be fail if we're on a system without native recursive watching,
	// the root is not a subpath of the home directory, or the watch is
	// cancelled.
	if mode == WatchMode_WatchPortable {
		watchRecursiveHome(context, root, events)
	}

	// If native watching failed, check (in a non-blocking fashion) if it was
	// due to cancellation. If so, then we don't want to fall back to polling
	// and can save some setup. If native watching failed for some other reason,
	// then we can fall back to polling until cancellation.
	select {
	case <-context.Done():
		return
	default:
	}

	// Create a timer to regular polling.
	if pollInterval == 0 {
		pollInterval = DefaultPollingInterval
	}
	pollIntervalDuration := time.Duration(pollInterval) * time.Second
	timer := time.NewTimer(pollIntervalDuration)

	// Loop and poll for changes, but watch for cancellation.
	var contents map[string]os.FileInfo
	for {
		select {
		case <-timer.C:
			// Perform a scan. If there's an error or no change, then reset the
			// timer and try again. We have to assume that errors here are due
			// to concurrent modifications, so there's not much we can do to
			// handle them.
			// TODO: If we see a certain number of failed polls, we could just
			// fall back to a timer.
			newContents, changed, err := poll(root, contents)
			if err != nil || !changed {
				timer.Reset(pollIntervalDuration)
				continue
			}

			// Store the new contents.
			contents = newContents

			// Forward the event in a non-blocking fashion.
			select {
			case events <- struct{}{}:
			default:
			}

			// Reset the timer and continue polling.
			timer.Reset(pollIntervalDuration)
		case <-context.Done():
			return
		}
	}
}
<commit_msg>Fixed poll-based filesystem watching to work with probe files.<commit_after>package filesystem

import (
	"context"
	"os"
	"path/filepath"
	"time"

	"github.com/pkg/errors"
)

const (
	// DefaultPollingInterval is the default watch polling interval, in seconds.
	DefaultPollingInterval = 10
)

// UnmarshalText implements the text unmarshalling interface used when loading
// from TOML files.
func (m *WatchMode) UnmarshalText(textBytes []byte) error {
	// Convert the bytes to a string.
	text := string(textBytes)

	// Convert to a VCS mode.
	switch text {
	case "portable":
		*m = WatchMode_WatchPortable
	case "force-poll":
		*m = WatchMode_WatchForcePoll
	default:
		return errors.Errorf("unknown watch mode specification: %s", text)
	}

	// Success.
	return nil
}

// Supported indicates whether or not a particular watch mode is a valid,
// non-default value.
func (m WatchMode) Supported() bool {
	switch m {
	case WatchMode_WatchPortable:
		return true
	case WatchMode_WatchForcePoll:
		return true
	default:
		return false
	}
}

// Description returns a human-readable description of a watch mode.
func (m WatchMode) Description() string {
	switch m {
	case WatchMode_WatchDefault:
		return "Default"
	case WatchMode_WatchPortable:
		return "Portable"
	case WatchMode_WatchForcePoll:
		return "Force Poll"
	default:
		return "Unknown"
	}
}

func fileInfoEqual(first, second os.FileInfo) bool {
	// Compare modes.
	if first.Mode() != second.Mode() {
		return false
	}

	// If we're dealing with directories, don't check size or time. Size doesn't
	// really make sense and modification time will be affected by our
	// executability preservation or Unicode decomposition probe file creation.
	if first.IsDir() {
		return true
	}

	// Compare size and time.
	return first.Size() == second.Size() &&
		first.ModTime().Equal(second.ModTime())
}

func poll(root string, existing map[string]os.FileInfo) (map[string]os.FileInfo, bool, error) {
	// Create our result map.
	result := make(map[string]os.FileInfo, len(existing))

	// Create a walk visitor.
	changed := false
	rootDoesNotExist := false
	visitor := func(path string, info os.FileInfo, err error) error {
		// If there's an error, then halt walking by returning it. Before doing
		// that though, determine if the error is due to the root not existing.
		// If that's the case, then we can create a valid result (an empty map)
		// as well as determine whether or not there's been a change.
		if err != nil {
			if path == root && os.IsNotExist(err) {
				changed = len(existing) > 0
				rootDoesNotExist = true
			}
			return err
		}

		// If this is an executability preservation or Unicode decomposition
		// test path, ignore it.
		if isExecutabilityTestPath(path) || isDecompositionTestPath(path) {
			return nil
		}

		// Insert the entry for this path.
		result[path] = info

		// Compare the entry for this path.
		if previous, ok := existing[path]; !ok {
			changed = true
		} else if !fileInfoEqual(info, previous) {
			changed = true
		}

		// Success.
		return nil
	}

	// Perform the walk. If it fails, and it's not due to the root not existing,
	// then we can't return a valid result and need to abort.
	if err := filepath.Walk(root, visitor); err != nil && !rootDoesNotExist {
		return nil, false, errors.Wrap(err, "unable to perform filesystem walk")
	}

	// Done.
	return result, changed, nil
}

// TODO: Document that this function closes the events channel when the watch
// is cancelled.
// TODO: Document that this function will always succeed in one way or another
// (it doesn't have any total failure modes) and won't exit until the associated
// context is cancelled.
// TODO: Document that the events channel must be buffered.
func Watch(context context.Context, root string, events chan struct{}, mode WatchMode, pollInterval uint32) {
	// Ensure that the events channel is buffered.
	if cap(events) < 1 {
		panic("watch channel should be buffered")
	}

	// Ensure that the events channel is closed when we're cancelled.
	defer close(events)

	// If we're in a recurisve home watch mode, attempt to watch in that manner.
	// This will be fail if we're on a system without native recursive watching,
	// the root is not a subpath of the home directory, or the watch is
	// cancelled.
	if mode == WatchMode_WatchPortable {
		watchRecursiveHome(context, root, events)
	}

	// If native watching failed, check (in a non-blocking fashion) if it was
	// due to cancellation. If so, then we don't want to fall back to polling
	// and can save some setup. If native watching failed for some other reason,
	// then we can fall back to polling until cancellation.
	select {
	case <-context.Done():
		return
	default:
	}

	// Create a timer to regular polling.
	if pollInterval == 0 {
		pollInterval = DefaultPollingInterval
	}
	pollIntervalDuration := time.Duration(pollInterval) * time.Second
	timer := time.NewTimer(pollIntervalDuration)

	// Loop and poll for changes, but watch for cancellation.
	var contents map[string]os.FileInfo
	for {
		select {
		case <-timer.C:
			// Perform a scan. If there's an error or no change, then reset the
			// timer and try again. We have to assume that errors here are due
			// to concurrent modifications, so there's not much we can do to
			// handle them.
			// TODO: If we see a certain number of failed polls, we could just
			// fall back to a timer.
			newContents, changed, err := poll(root, contents)
			if err != nil || !changed {
				timer.Reset(pollIntervalDuration)
				continue
			}

			// Store the new contents.
			contents = newContents

			// Forward the event in a non-blocking fashion.
			select {
			case events <- struct{}{}:
			default:
			}

			// Reset the timer and continue polling.
			timer.Reset(pollIntervalDuration)
		case <-context.Done():
			return
		}
	}
}
<|endoftext|>


<commit_before>package msg_test

import (
	"testing"

	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/book"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/candle"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/event"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/ticker"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/trade"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/mux/msg"
	"github.com/stretchr/testify/assert"
)

func TestIsEvent(t *testing.T) {
	cases := map[string]struct {
		pld      []byte
		expected bool
	}{
		"event type": {
			pld:      []byte(`{}`),
			expected: true,
		},
		"not event type": {
			pld:      []byte(`[]`),
			expected: false,
		},
	}

	for k, v := range cases {
		t.Run(k, func(t *testing.T) {
			m := msg.Msg{
				Data: v.pld,
			}

			got := m.IsEvent()
			assert.Equal(t, v.expected, got)
		})
	}
}

func TestIsRaw(t *testing.T) {
	cases := map[string]struct {
		pld      []byte
		expected bool
	}{
		"raw msg type": {
			pld:      []byte(`[]`),
			expected: true,
		},
		"raw info type": {
			pld:      []byte(`{}`),
			expected: false,
		},
	}

	for k, v := range cases {
		t.Run(k, func(t *testing.T) {
			m := msg.Msg{
				Data: v.pld,
			}

			got := m.IsRaw()
			assert.Equal(t, v.expected, got)
		})
	}
}

func TestProcessEvent(t *testing.T) {
	m := msg.Msg{
		Data: []byte(`{
			"event": "info",
			"version": 2,
			"serverId": "dbea77ee-4740-4a82-84f3-c6bc1b5abb9a",
			"platform": {
				"status":1
			}
		}`),
	}

	expected := event.Info{
		Subscribe: event.Subscribe{
			Event: "info",
		},
		Version:  2,
		ServerID: "dbea77ee-4740-4a82-84f3-c6bc1b5abb9a",
		Platform: event.Platform{Status: 1},
	}

	got, err := m.ProcessEvent()
	assert.NoError(t, err)
	assert.Equal(t, expected, got)
}

func TestProcessRaw(t *testing.T) {
	cases := map[string]struct {
		pld      []byte
		expected interface{}
		inf      map[int64]event.Info
	}{
		"info event": {
			pld: []byte(`[123, "hb"]`),
			inf: map[int64]event.Info{123: {}},
			expected: event.Info{
				ChanID:    123,
				Subscribe: event.Subscribe{Event: "hb"},
			},
		},
		"trades snapshot": {
			pld: []byte(`[111,[[559273857,1609665708633,-0.0048,34113]]]`),
			inf: map[int64]event.Info{
				111: {
					Subscribe: event.Subscribe{
						Channel: "trades",
						Symbol:  "tBTCUST",
					},
				},
			},
			expected: &trade.Snapshot{
				Snapshot: []*trade.Trade{
					{
						Pair:   "tBTCUST",
						ID:     559273857,
						MTS:    1609665708633,
						Amount: -0.0048,
						Price:  34113,
					},
				},
			},
		},
		"trade": {
			pld: []byte(`[111,[559273857,1609665708633,-0.0048,34113]]`),
			inf: map[int64]event.Info{
				111: {
					Subscribe: event.Subscribe{
						Channel: "trades",
						Symbol:  "tBTCUST",
					},
				},
			},
			expected: &trade.Trade{
				Pair:   "tBTCUST",
				ID:     559273857,
				MTS:    1609665708633,
				Amount: -0.0048,
				Price:  34113,
			},
		},
		"ticker": {
			pld: []byte(`[
				111,
				[
					34072,0.019999999999999997,34080,6.69793272,4350,
					0.1464,34062,4047.85335915,34758,29490
				]
			]`),
			inf: map[int64]event.Info{
				111: {
					Subscribe: event.Subscribe{
						Channel: "ticker",
						Symbol:  "tBTCUST",
					},
				},
			},
			expected: &ticker.Ticker{
				Symbol:          "tBTCUST",
				Bid:             34072,
				BidSize:         0.019999999999999997,
				Ask:             34080,
				AskSize:         6.69793272,
				DailyChange:     4350,
				DailyChangePerc: 0.1464,
				LastPrice:       34062,
				Volume:          4047.85335915,
				High:            34758,
				Low:             29490,
			},
		},
		"candles snapshot": {
			pld: []byte(`[111,[[1609668540000,828.01,827.67,828.42,827.67,2.32080241]]]`),
			inf: map[int64]event.Info{
				111: {
					Subscribe: event.Subscribe{
						Channel: "candles",
						Key:     "trade:1m:tETHUST",
					},
				},
			},
			expected: &candle.Snapshot{
				Snapshot: []*candle.Candle{
					{
						Symbol:     "tETHUST",
						Resolution: "1m",
						MTS:        1609668540000,
						Open:       828.01,
						Close:      827.67,
						High:       828.42,
						Low:        827.67,
						Volume:     2.32080241,
					},
				},
			},
		},
		"candle": {
			pld: []byte(`[111,[1609668540000,828.01,827.67,828.42,827.67,2.32080241]]`),
			inf: map[int64]event.Info{
				111: {
					Subscribe: event.Subscribe{
						Channel: "candles",
						Key:     "trade:1m:tETHUST",
					},
				},
			},
			expected: &candle.Candle{
				Symbol:     "tETHUST",
				Resolution: "1m",
				MTS:        1609668540000,
				Open:       828.01,
				Close:      827.67,
				High:       828.42,
				Low:        827.67,
				Volume:     2.32080241,
			},
		},
		"raw book snapshot": {
			pld: []byte(`[869944,[[55804480297,33766,2]]]`),
			inf: map[int64]event.Info{
				869944: {
					Subscribe: event.Subscribe{
						Channel:   "book",
						Symbol:    "tBTCUSD",
						Precision: "R0",
					},
				},
			},
			expected: &book.Snapshot{
				Snapshot: []*book.Book{
					{
						Symbol:      "tBTCUSD",
						ID:          55804480297,
						Price:       33766,
						Amount:      2,
						PriceJsNum:  "33766",
						AmountJsNum: "2",
						Side:        1,
						Action:      0,
					},
				},
			},
		},
		"raw book": {
			pld: []byte(`[869944,[55804480297,33766,2]]`),
			inf: map[int64]event.Info{
				869944: {
					Subscribe: event.Subscribe{
						Channel:   "book",
						Symbol:    "tBTCUSD",
						Precision: "R0",
					},
				},
			},
			expected: &book.Book{
				Symbol:      "tBTCUSD",
				ID:          55804480297,
				Price:       33766,
				Amount:      2,
				PriceJsNum:  "33766",
				AmountJsNum: "2",
				Side:        1,
				Action:      0,
			},
		},
		"book snapshot": {
			pld: []byte(`[793767,[[676.3,1,5]]]`),
			inf: map[int64]event.Info{
				793767: {
					Subscribe: event.Subscribe{
						Channel:   "book",
						Symbol:    "tETHEUR",
						Precision: "P0",
						Frequency: "F0",
					},
				},
			},
			expected: &book.Snapshot{
				Snapshot: []*book.Book{
					{
						Symbol:      "tETHEUR",
						Count:       1,
						Period:      0,
						Price:       676.3,
						Amount:      5,
						Rate:        0,
						PriceJsNum:  "676.3",
						AmountJsNum: "5",
						Side:        1,
						Action:      0,
					},
				},
			},
		},
		"book": {
			pld: []byte(`[793767,[676.3,1,5]]`),
			inf: map[int64]event.Info{
				793767: {
					Subscribe: event.Subscribe{
						Channel:   "book",
						Symbol:    "tETHEUR",
						Precision: "P0",
						Frequency: "F0",
					},
				},
			},
			expected: &book.Book{
				Symbol:      "tETHEUR",
				Count:       1,
				Period:      0,
				Price:       676.3,
				Amount:      5,
				Rate:        0,
				PriceJsNum:  "676.3",
				AmountJsNum: "5",
				Side:        1,
				Action:      0,
			},
		},
	}

	for k, v := range cases {
		t.Run(k, func(t *testing.T) {
			m := msg.Msg{Data: v.pld}
			got, err := m.ProcessRaw(v.inf)
			assert.NoError(t, err)
			assert.Equal(t, v.expected, got)
		})
	}
}
<commit_msg>derivatives and liquidation status transform from raw test coverage<commit_after>package msg_test

import (
	"testing"

	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/book"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/candle"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/event"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/status"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/ticker"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/trade"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/mux/msg"
	"github.com/stretchr/testify/assert"
)

func TestIsEvent(t *testing.T) {
	cases := map[string]struct {
		pld      []byte
		expected bool
	}{
		"event type": {
			pld:      []byte(`{}`),
			expected: true,
		},
		"not event type": {
			pld:      []byte(`[]`),
			expected: false,
		},
	}

	for k, v := range cases {
		t.Run(k, func(t *testing.T) {
			m := msg.Msg{
				Data: v.pld,
			}

			got := m.IsEvent()
			assert.Equal(t, v.expected, got)
		})
	}
}

func TestIsRaw(t *testing.T) {
	cases := map[string]struct {
		pld      []byte
		expected bool
	}{
		"raw msg type": {
			pld:      []byte(`[]`),
			expected: true,
		},
		"raw info type": {
			pld:      []byte(`{}`),
			expected: false,
		},
	}

	for k, v := range cases {
		t.Run(k, func(t *testing.T) {
			m := msg.Msg{
				Data: v.pld,
			}

			got := m.IsRaw()
			assert.Equal(t, v.expected, got)
		})
	}
}

func TestProcessEvent(t *testing.T) {
	m := msg.Msg{
		Data: []byte(`{
			"event": "info",
			"version": 2,
			"serverId": "dbea77ee-4740-4a82-84f3-c6bc1b5abb9a",
			"platform": {
				"status":1
			}
		}`),
	}

	expected := event.Info{
		Subscribe: event.Subscribe{
			Event: "info",
		},
		Version:  2,
		ServerID: "dbea77ee-4740-4a82-84f3-c6bc1b5abb9a",
		Platform: event.Platform{Status: 1},
	}

	got, err := m.ProcessEvent()
	assert.NoError(t, err)
	assert.Equal(t, expected, got)
}

func TestProcessRaw(t *testing.T) {
	cases := map[string]struct {
		pld      []byte
		expected interface{}
		inf      map[int64]event.Info
	}{
		"info event": {
			pld: []byte(`[123, "hb"]`),
			inf: map[int64]event.Info{123: {}},
			expected: event.Info{
				ChanID:    123,
				Subscribe: event.Subscribe{Event: "hb"},
			},
		},
		"trades snapshot": {
			pld: []byte(`[111,[[559273857,1609665708633,-0.0048,34113]]]`),
			inf: map[int64]event.Info{
				111: {
					Subscribe: event.Subscribe{
						Channel: "trades",
						Symbol:  "tBTCUST",
					},
				},
			},
			expected: &trade.Snapshot{
				Snapshot: []*trade.Trade{
					{
						Pair:   "tBTCUST",
						ID:     559273857,
						MTS:    1609665708633,
						Amount: -0.0048,
						Price:  34113,
					},
				},
			},
		},
		"trade": {
			pld: []byte(`[111,[559273857,1609665708633,-0.0048,34113]]`),
			inf: map[int64]event.Info{
				111: {
					Subscribe: event.Subscribe{
						Channel: "trades",
						Symbol:  "tBTCUST",
					},
				},
			},
			expected: &trade.Trade{
				Pair:   "tBTCUST",
				ID:     559273857,
				MTS:    1609665708633,
				Amount: -0.0048,
				Price:  34113,
			},
		},
		"ticker": {
			pld: []byte(`[
				111,
				[
					34072,0.019999999999999997,34080,6.69793272,4350,
					0.1464,34062,4047.85335915,34758,29490
				]
			]`),
			inf: map[int64]event.Info{
				111: {
					Subscribe: event.Subscribe{
						Channel: "ticker",
						Symbol:  "tBTCUST",
					},
				},
			},
			expected: &ticker.Ticker{
				Symbol:          "tBTCUST",
				Bid:             34072,
				BidSize:         0.019999999999999997,
				Ask:             34080,
				AskSize:         6.69793272,
				DailyChange:     4350,
				DailyChangePerc: 0.1464,
				LastPrice:       34062,
				Volume:          4047.85335915,
				High:            34758,
				Low:             29490,
			},
		},
		"candles snapshot": {
			pld: []byte(`[111,[[1609668540000,828.01,827.67,828.42,827.67,2.32080241]]]`),
			inf: map[int64]event.Info{
				111: {
					Subscribe: event.Subscribe{
						Channel: "candles",
						Key:     "trade:1m:tETHUST",
					},
				},
			},
			expected: &candle.Snapshot{
				Snapshot: []*candle.Candle{
					{
						Symbol:     "tETHUST",
						Resolution: "1m",
						MTS:        1609668540000,
						Open:       828.01,
						Close:      827.67,
						High:       828.42,
						Low:        827.67,
						Volume:     2.32080241,
					},
				},
			},
		},
		"candle": {
			pld: []byte(`[111,[1609668540000,828.01,827.67,828.42,827.67,2.32080241]]`),
			inf: map[int64]event.Info{
				111: {
					Subscribe: event.Subscribe{
						Channel: "candles",
						Key:     "trade:1m:tETHUST",
					},
				},
			},
			expected: &candle.Candle{
				Symbol:     "tETHUST",
				Resolution: "1m",
				MTS:        1609668540000,
				Open:       828.01,
				Close:      827.67,
				High:       828.42,
				Low:        827.67,
				Volume:     2.32080241,
			},
		},
		"raw book snapshot": {
			pld: []byte(`[869944,[[55804480297,33766,2]]]`),
			inf: map[int64]event.Info{
				869944: {
					Subscribe: event.Subscribe{
						Channel:   "book",
						Symbol:    "tBTCUSD",
						Precision: "R0",
					},
				},
			},
			expected: &book.Snapshot{
				Snapshot: []*book.Book{
					{
						Symbol:      "tBTCUSD",
						ID:          55804480297,
						Price:       33766,
						Amount:      2,
						PriceJsNum:  "33766",
						AmountJsNum: "2",
						Side:        1,
						Action:      0,
					},
				},
			},
		},
		"raw book": {
			pld: []byte(`[869944,[55804480297,33766,2]]`),
			inf: map[int64]event.Info{
				869944: {
					Subscribe: event.Subscribe{
						Channel:   "book",
						Symbol:    "tBTCUSD",
						Precision: "R0",
					},
				},
			},
			expected: &book.Book{
				Symbol:      "tBTCUSD",
				ID:          55804480297,
				Price:       33766,
				Amount:      2,
				PriceJsNum:  "33766",
				AmountJsNum: "2",
				Side:        1,
				Action:      0,
			},
		},
		"book snapshot": {
			pld: []byte(`[793767,[[676.3,1,5]]]`),
			inf: map[int64]event.Info{
				793767: {
					Subscribe: event.Subscribe{
						Channel:   "book",
						Symbol:    "tETHEUR",
						Precision: "P0",
						Frequency: "F0",
					},
				},
			},
			expected: &book.Snapshot{
				Snapshot: []*book.Book{
					{
						Symbol:      "tETHEUR",
						Count:       1,
						Period:      0,
						Price:       676.3,
						Amount:      5,
						Rate:        0,
						PriceJsNum:  "676.3",
						AmountJsNum: "5",
						Side:        1,
						Action:      0,
					},
				},
			},
		},
		"book": {
			pld: []byte(`[793767,[676.3,1,5]]`),
			inf: map[int64]event.Info{
				793767: {
					Subscribe: event.Subscribe{
						Channel:   "book",
						Symbol:    "tETHEUR",
						Precision: "P0",
						Frequency: "F0",
					},
				},
			},
			expected: &book.Book{
				Symbol:      "tETHEUR",
				Count:       1,
				Period:      0,
				Price:       676.3,
				Amount:      5,
				Rate:        0,
				PriceJsNum:  "676.3",
				AmountJsNum: "5",
				Side:        1,
				Action:      0,
			},
		},
		"derivatives status snapshot": {
			pld: []byte(`[
				799830,
				[[
					1609921474000,null,34568.786626655,34575.5,null,1856521.42387705,
					null,1609948800000,-0.00004348,481,null,0,null,null,34593.64333333333,
					null,null,11153.74635347,null,null,null,null,null
				]]
			]`),
			inf: map[int64]event.Info{
				799830: {
					Subscribe: event.Subscribe{
						Channel: "status",
						Key:     "deriv:tBTCF0:USTF0",
					},
				},
			},
			expected: &status.DerivativesSnapshot{
				Snapshot: []*status.Derivative{
					{
						Symbol:               "tBTCF0:USTF0",
						MTS:                  1609921474000,
						Price:                34568.786626655,
						SpotPrice:            34575.5,
						InsuranceFundBalance: 1.85652142387705e+06,
						FundingEventMTS:      1609948800000,
						FundingAccrued:       -4.348e-05,
						FundingStep:          481,
						MarkPrice:            34593.64333333333,
						OpenInterest:         11153.74635347,
					},
				},
			},
		},
		"derivatives status": {
			pld: []byte(`[
				799830,
				[
					1609921474000,null,34568.786626655,34575.5,null,1856521.42387705,
					null,1609948800000,-0.00004348,481,null,0,null,null,34593.64333333333,
					null,null,11153.74635347,null,null,null,null,null
				]
			]`),
			inf: map[int64]event.Info{
				799830: {
					Subscribe: event.Subscribe{
						Channel: "status",
						Key:     "deriv:tBTCF0:USTF0",
					},
				},
			},
			expected: &status.Derivative{
				Symbol:               "tBTCF0:USTF0",
				MTS:                  1609921474000,
				Price:                34568.786626655,
				SpotPrice:            34575.5,
				InsuranceFundBalance: 1.85652142387705e+06,
				FundingEventMTS:      1609948800000,
				FundingAccrued:       -4.348e-05,
				FundingStep:          481,
				MarkPrice:            34593.64333333333,
				OpenInterest:         11153.74635347,
			},
		},
		"liquidation status snapshot": {
			pld: []byte(`[
				521209,
				[[
					"pos",145511476,1609921778489,null,"tBTCF0:USTF0",
					0.12173,34618.82986269,null,1,1,null,34281
				]]
			]`),
			inf: map[int64]event.Info{
				521209: {
					Subscribe: event.Subscribe{
						Channel: "status",
						Key:     "liq:global",
					},
				},
			},
			expected: &status.LiquidationsSnapshot{
				Snapshot: []*status.Liquidation{
					{
						Symbol:        "tBTCF0:USTF0",
						PositionID:    145511476,
						MTS:           1609921778489,
						Amount:        0.12173,
						BasePrice:     34618.82986269,
						IsMatch:       1,
						IsMarketSold:  1,
						PriceAcquired: 34281,
					},
				},
			},
		},
		"liquidation status": {
			pld: []byte(`[
				521209,
				[
					"pos",145511476,1609921778489,null,"tBTCF0:USTF0",
					0.12173,34618.82986269,null,1,1,null,34281
				]
			]`),
			inf: map[int64]event.Info{
				521209: {
					Subscribe: event.Subscribe{
						Channel: "status",
						Key:     "liq:global",
					},
				},
			},
			expected: &status.Liquidation{
				Symbol:        "tBTCF0:USTF0",
				PositionID:    145511476,
				MTS:           1609921778489,
				Amount:        0.12173,
				BasePrice:     34618.82986269,
				IsMatch:       1,
				IsMarketSold:  1,
				PriceAcquired: 34281,
			},
		},
	}

	for k, v := range cases {
		t.Run(k, func(t *testing.T) {
			m := msg.Msg{Data: v.pld}
			got, err := m.ProcessRaw(v.inf)
			assert.NoError(t, err)
			assert.Equal(t, v.expected, got)
		})
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2016 The Rook Authors. All rights reserved.

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.

Some of the code below came from https://github.com/coreos/etcd-operator
which also has the apache 2.0 license.
*/
package operator

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"

	"sync"

	"github.com/rook/rook/pkg/operator/cluster"
	"github.com/rook/rook/pkg/operator/k8sutil"
	rookclient "github.com/rook/rook/pkg/rook/client"
	kwatch "k8s.io/apimachinery/pkg/watch"
)

type clusterManager struct {
	context      *context
	name         string
	watchVersion string
	devicesInUse bool
	clusters     map[string]*cluster.Cluster
	tracker      *tprTracker
	sync.RWMutex
	// The initiators that create TPRs specific to specific Rook clusters.
	// For example, pools, object services, and file services, only make sense in the context of a Rook cluster
	inclusterInitiators []inclusterInitiator
	inclusterMgrs       []tprManager
}

func newClusterManager(context *context, inclusterInitiators []inclusterInitiator) *clusterManager {
	return &clusterManager{
		context:             context,
		clusters:            make(map[string]*cluster.Cluster),
		tracker:             newTPRTracker(),
		inclusterInitiators: inclusterInitiators,
	}
}

// Gets the name of the TPR
func (m *clusterManager) Name() string {
	return "rookcluster"
}

// Gets the description of the TPR
func (m *clusterManager) Description() string {
	return "Managed Rook clusters"
}

func (m *clusterManager) Manage() {
	for {
		logger.Infof("Managing clusters")
		err := m.Load()
		if err != nil {
			logger.Errorf("failed to load cluster. %+v", err)
		} else {
			if err := m.Watch(); err != nil {
				logger.Errorf("failed to watch clusters. %+v", err)
			}
		}

		<-time.After(time.Second * time.Duration(m.context.retryDelay))
	}
}

func (m *clusterManager) Load() error {

	// Check if there is an existing cluster to recover
	logger.Info("finding existing clusters...")
	clusterList, err := m.getClusterList()
	if err != nil {
		return err
	}
	logger.Infof("found %d clusters", len(clusterList.Items))
	for i := range clusterList.Items {
		c := clusterList.Items[i]
		logger.Infof("checking if cluster %s is running in namespace %s", c.Name, c.Namespace)
		m.startCluster(&c)
	}

	m.watchVersion = clusterList.Metadata.ResourceVersion
	return nil
}

func (m *clusterManager) startTrack(c *cluster.Cluster) error {
	m.Lock()
	defer m.Unlock()

	existing, ok := m.clusters[c.Namespace]
	if ok {
		if c.Name != existing.Name {
			return fmt.Errorf("cluster %s is already running in namespace %s. Multiple clusters per namespace not supported.", existing.Name, existing.Namespace)
		}
	} else {
		// only start the cluster if we're not already tracking it from a previous iteration
		m.clusters[c.Namespace] = c
	}

	// refresh the version of the cluster we're tracking
	m.tracker.add(c.Namespace, c.ResourceVersion)

	return nil
}

func (m *clusterManager) stopTrack(c *cluster.Cluster) {
	m.Lock()
	defer m.Unlock()

	m.tracker.remove(c.Namespace)
	delete(m.clusters, c.Namespace)
}

func (m *clusterManager) startCluster(c *cluster.Cluster) {
	c.Init(m.context.factory, m.context.clientset)
	if err := m.startTrack(c); err != nil {
		logger.Errorf("failed to start cluster %s in namespace %s. %+v", c.Name, c.Namespace, err)
		return
	}

	if m.devicesInUse && c.Spec.Storage.AnyUseAllDevices() {
		logger.Warningf("using all devices in more than one namespace not supported. ignoring devices in namespace %s", c.Namespace)
		c.Spec.Storage.ClearUseAllDevices()
	}

	if c.Spec.Storage.AnyUseAllDevices() {
		m.devicesInUse = true
	}

	go func() {
		defer m.stopTrack(c)
		logger.Infof("starting cluster %s in namespace %s", c.Name, c.Namespace)

		// Start the Rook cluster components
		err := c.CreateInstance()
		if err != nil {
			logger.Errorf("failed to create cluster %s in namespace %s. %+v", c.Name, c.Namespace, err)
			return
		}

		// Start all the TPRs for this cluster
		for _, tpr := range m.inclusterInitiators {
			k8sutil.Retry(time.Duration(m.context.retryDelay)*time.Second, m.context.maxRetries, func() (bool, error) {
				tprMgr, err := tpr.Create(m, c.Name, c.Namespace)
				if err != nil {
					logger.Warningf("cannot create in-cluster tpr %s. %+v. retrying...", m.Name(), err)
					return false, nil
				}

				// Start the tpr-manager asynchronously
				go tprMgr.Manage()

				m.Lock()
				defer m.Unlock()
				m.inclusterMgrs = append(m.inclusterMgrs, tprMgr)

				return true, nil
			})
		}
		c.Monitor(m.tracker.stopChMap[c.Namespace])
	}()
}

func (m *clusterManager) isClustersCacheStale(currentClusters []cluster.Cluster) bool {
	if len(m.tracker.clusterRVs) != len(currentClusters) {
		return true
	}

	for _, cc := range currentClusters {
		rv, ok := m.tracker.clusterRVs[cc.Name]
		if !ok || rv != cc.ResourceVersion {
			return true
		}
	}

	return false
}

func (m *clusterManager) getClusterList() (*cluster.ClusterList, error) {
	b, err := getRawList(m.context.clientset, m.Name())
	if err != nil {
		return nil, err
	}

	clusters := &cluster.ClusterList{}
	if err := json.Unmarshal(b, clusters); err != nil {
		return nil, err
	}
	return clusters, nil
}

func (m *clusterManager) Watch() error {
	logger.Infof("start watching cluster tpr: %s", m.watchVersion)
	defer m.tracker.stop()

	eventCh, errCh := m.watch()

	go func() {
		timer := k8sutil.NewPanicTimer(
			time.Minute,
			"unexpected long blocking (> 1 Minute) when handling cluster event")

		for event := range eventCh {
			timer.Start()

			c := event.Object

			switch event.Type {
			case kwatch.Added:
				logger.Infof("starting new cluster %s in namespace %s", c.Name, c.Namespace)
				m.startCluster(c)

			case kwatch.Modified:
				logger.Infof("modifying a cluster not implemented")

			case kwatch.Deleted:
				logger.Infof("deleting a cluster not implemented")
			}

			timer.Stop()
		}
	}()
	return <-errCh
}

// watch creates a go routine, and watches the cluster.rook kind resources from
// the given watch version. It emits events on the resources through the returned
// event chan. Errors will be reported through the returned error chan. The go routine
// exits on any error.
func (m *clusterManager) watch() (<-chan *clusterEvent, <-chan error) {
	eventCh := make(chan *clusterEvent)
	// On unexpected error case, the operator should exit
	errCh := make(chan error, 1)

	go func() {
		defer close(eventCh)

		for {
			err := m.watchOuterTPR(eventCh, errCh)
			if err != nil {
				logger.Warningf("cancelling cluster tpr watch. %+v", err)
				return
			}
		}
	}()

	return eventCh, errCh
}

func (m *clusterManager) watchOuterTPR(eventCh chan *clusterEvent, errCh chan error) error {
	resp, err := watchTPR(m.context, m.Name(), m.watchVersion)
	if err != nil {
		errCh <- err
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		err := errors.New("invalid status code: " + resp.Status)
		errCh <- err
		return err
	}

	decoder := json.NewDecoder(resp.Body)
	for {
		ev, st, err := pollClusterEvent(decoder)
		done, err := handlePollEventResult(st, err, m.checkStaleCache, errCh)
		if err != nil {
			return err
		}
		if done {
			return nil
		}
		logger.Debugf("rook cluster event: %+v", ev)

		m.watchVersion = ev.Object.ResourceVersion
		eventCh <- ev
	}
}

func (m *clusterManager) checkStaleCache() (bool, error) {
	clusterList, err := m.getClusterList()
	if err == nil && !m.isClustersCacheStale(clusterList.Items) {
		m.watchVersion = clusterList.Metadata.ResourceVersion
		return false, nil
	}

	return true, err
}

func (m *clusterManager) getRookClient(namespace string) (rookclient.RookRestClient, error) {
	m.Lock()
	defer m.Unlock()
	if c, ok := m.clusters[namespace]; ok {
		return c.GetRookClient()
	}

	return nil, fmt.Errorf("namespace %s not found", namespace)
}
<commit_msg>retry initializing a cluster instead of giving up<commit_after>/*
Copyright 2016 The Rook Authors. All rights reserved.

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.

Some of the code below came from https://github.com/coreos/etcd-operator
which also has the apache 2.0 license.
*/
package operator

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"

	"sync"

	"github.com/rook/rook/pkg/operator/cluster"
	"github.com/rook/rook/pkg/operator/k8sutil"
	rookclient "github.com/rook/rook/pkg/rook/client"
	kwatch "k8s.io/apimachinery/pkg/watch"
)

type clusterManager struct {
	context      *context
	name         string
	watchVersion string
	devicesInUse bool
	clusters     map[string]*cluster.Cluster
	tracker      *tprTracker
	sync.RWMutex
	// The initiators that create TPRs specific to specific Rook clusters.
	// For example, pools, object services, and file services, only make sense in the context of a Rook cluster
	inclusterInitiators []inclusterInitiator
	inclusterMgrs       []tprManager
}

func newClusterManager(context *context, inclusterInitiators []inclusterInitiator) *clusterManager {
	return &clusterManager{
		context:             context,
		clusters:            make(map[string]*cluster.Cluster),
		tracker:             newTPRTracker(),
		inclusterInitiators: inclusterInitiators,
	}
}

// Gets the name of the TPR
func (m *clusterManager) Name() string {
	return "rookcluster"
}

// Gets the description of the TPR
func (m *clusterManager) Description() string {
	return "Managed Rook clusters"
}

func (m *clusterManager) Manage() {
	for {
		logger.Infof("Managing clusters")
		err := m.Load()
		if err != nil {
			logger.Errorf("failed to load cluster. %+v", err)
		} else {
			if err := m.Watch(); err != nil {
				logger.Errorf("failed to watch clusters. %+v", err)
			}
		}

		<-time.After(time.Second * time.Duration(m.context.retryDelay))
	}
}

func (m *clusterManager) Load() error {

	// Check if there is an existing cluster to recover
	logger.Info("finding existing clusters...")
	clusterList, err := m.getClusterList()
	if err != nil {
		return err
	}
	logger.Infof("found %d clusters", len(clusterList.Items))
	for i := range clusterList.Items {
		c := clusterList.Items[i]
		logger.Infof("checking if cluster %s is running in namespace %s", c.Name, c.Namespace)
		m.startCluster(&c)
	}

	m.watchVersion = clusterList.Metadata.ResourceVersion
	return nil
}

func (m *clusterManager) startTrack(c *cluster.Cluster) error {
	m.Lock()
	defer m.Unlock()

	existing, ok := m.clusters[c.Namespace]
	if ok {
		if c.Name != existing.Name {
			return fmt.Errorf("cluster %s is already running in namespace %s. Multiple clusters per namespace not supported.", existing.Name, existing.Namespace)
		}
	} else {
		// only start the cluster if we're not already tracking it from a previous iteration
		m.clusters[c.Namespace] = c
	}

	// refresh the version of the cluster we're tracking
	m.tracker.add(c.Namespace, c.ResourceVersion)

	return nil
}

func (m *clusterManager) stopTrack(c *cluster.Cluster) {
	m.Lock()
	defer m.Unlock()

	m.tracker.remove(c.Namespace)
	delete(m.clusters, c.Namespace)
}

func (m *clusterManager) startCluster(c *cluster.Cluster) {
	c.Init(m.context.factory, m.context.clientset)
	if err := m.startTrack(c); err != nil {
		logger.Errorf("failed to start cluster %s in namespace %s. %+v", c.Name, c.Namespace, err)
		return
	}

	if m.devicesInUse && c.Spec.Storage.AnyUseAllDevices() {
		logger.Warningf("using all devices in more than one namespace not supported. ignoring devices in namespace %s", c.Namespace)
		c.Spec.Storage.ClearUseAllDevices()
	}

	if c.Spec.Storage.AnyUseAllDevices() {
		m.devicesInUse = true
	}

	go func() {
		defer m.stopTrack(c)
		logger.Infof("starting cluster %s in namespace %s", c.Name, c.Namespace)

		// Start the Rook cluster components. Retry several times in case of failure.
		err := k8sutil.Retry(time.Duration(m.context.retryDelay)*time.Second, m.context.maxRetries, func() (bool, error) {
			err := c.CreateInstance()
			if err != nil {
				logger.Errorf("failed to create cluster %s in namespace %s. %+v", c.Name, c.Namespace, err)
				return false, nil
			}
			return true, nil
		})
		if err != nil {
			logger.Errorf("giving up to create cluster %s in namespace %s", c.Name, c.Namespace)
			return
		}

		// Start all the TPRs for this cluster
		for _, tpr := range m.inclusterInitiators {
			k8sutil.Retry(time.Duration(m.context.retryDelay)*time.Second, m.context.maxRetries, func() (bool, error) {
				tprMgr, err := tpr.Create(m, c.Name, c.Namespace)
				if err != nil {
					logger.Warningf("cannot create in-cluster tpr %s. %+v. retrying...", m.Name(), err)
					return false, nil
				}

				// Start the tpr-manager asynchronously
				go tprMgr.Manage()

				m.Lock()
				defer m.Unlock()
				m.inclusterMgrs = append(m.inclusterMgrs, tprMgr)

				return true, nil
			})
		}
		c.Monitor(m.tracker.stopChMap[c.Namespace])
	}()
}

func (m *clusterManager) isClustersCacheStale(currentClusters []cluster.Cluster) bool {
	if len(m.tracker.clusterRVs) != len(currentClusters) {
		return true
	}

	for _, cc := range currentClusters {
		rv, ok := m.tracker.clusterRVs[cc.Name]
		if !ok || rv != cc.ResourceVersion {
			return true
		}
	}

	return false
}

func (m *clusterManager) getClusterList() (*cluster.ClusterList, error) {
	b, err := getRawList(m.context.clientset, m.Name())
	if err != nil {
		return nil, err
	}

	clusters := &cluster.ClusterList{}
	if err := json.Unmarshal(b, clusters); err != nil {
		return nil, err
	}
	return clusters, nil
}

func (m *clusterManager) Watch() error {
	logger.Infof("start watching cluster tpr: %s", m.watchVersion)
	defer m.tracker.stop()

	eventCh, errCh := m.watch()

	go func() {
		timer := k8sutil.NewPanicTimer(
			time.Minute,
			"unexpected long blocking (> 1 Minute) when handling cluster event")

		for event := range eventCh {
			timer.Start()

			c := event.Object

			switch event.Type {
			case kwatch.Added:
				logger.Infof("starting new cluster %s in namespace %s", c.Name, c.Namespace)
				m.startCluster(c)

			case kwatch.Modified:
				logger.Infof("modifying a cluster not implemented")

			case kwatch.Deleted:
				logger.Infof("deleting a cluster not implemented")
			}

			timer.Stop()
		}
	}()
	return <-errCh
}

// watch creates a go routine, and watches the cluster.rook kind resources from
// the given watch version. It emits events on the resources through the returned
// event chan. Errors will be reported through the returned error chan. The go routine
// exits on any error.
func (m *clusterManager) watch() (<-chan *clusterEvent, <-chan error) {
	eventCh := make(chan *clusterEvent)
	// On unexpected error case, the operator should exit
	errCh := make(chan error, 1)

	go func() {
		defer close(eventCh)

		for {
			err := m.watchOuterTPR(eventCh, errCh)
			if err != nil {
				logger.Warningf("cancelling cluster tpr watch. %+v", err)
				return
			}
		}
	}()

	return eventCh, errCh
}

func (m *clusterManager) watchOuterTPR(eventCh chan *clusterEvent, errCh chan error) error {
	resp, err := watchTPR(m.context, m.Name(), m.watchVersion)
	if err != nil {
		errCh <- err
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		err := errors.New("invalid status code: " + resp.Status)
		errCh <- err
		return err
	}

	decoder := json.NewDecoder(resp.Body)
	for {
		ev, st, err := pollClusterEvent(decoder)
		done, err := handlePollEventResult(st, err, m.checkStaleCache, errCh)
		if err != nil {
			return err
		}
		if done {
			return nil
		}
		logger.Debugf("rook cluster event: %+v", ev)

		m.watchVersion = ev.Object.ResourceVersion
		eventCh <- ev
	}
}

func (m *clusterManager) checkStaleCache() (bool, error) {
	clusterList, err := m.getClusterList()
	if err == nil && !m.isClustersCacheStale(clusterList.Items) {
		m.watchVersion = clusterList.Metadata.ResourceVersion
		return false, nil
	}

	return true, err
}

func (m *clusterManager) getRookClient(namespace string) (rookclient.RookRestClient, error) {
	m.Lock()
	defer m.Unlock()
	if c, ok := m.clusters[namespace]; ok {
		return c.GetRookClient()
	}

	return nil, fmt.Errorf("namespace %s not found", namespace)
}
<|endoftext|>


<commit_before>package pubsub // import "github.com/docker/docker/pkg/pubsub"

import (
	"sync"
	"time"
)

var wgPool = sync.Pool{New: func() interface{} { return new(sync.WaitGroup) }}

// NewPublisher creates a new pub/sub publisher to broadcast messages.
// The duration is used as the send timeout as to not block the publisher publishing
// messages to other clients if one client is slow or unresponsive.
// The buffer is used when creating new channels for subscribers.
func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher {
	return &Publisher{
		buffer:      buffer,
		timeout:     publishTimeout,
		subscribers: make(map[subscriber]topicFunc),
	}
}

type subscriber chan interface{}
type topicFunc func(v interface{}) bool

// Publisher is basic pub/sub structure. Allows to send events and subscribe
// to them. Can be safely used from multiple goroutines.
type Publisher struct {
	m           sync.RWMutex
	buffer      int
	timeout     time.Duration
	subscribers map[subscriber]topicFunc
}

// Len returns the number of subscribers for the publisher
func (p *Publisher) Len() int {
	p.m.RLock()
	i := len(p.subscribers)
	p.m.RUnlock()
	return i
}

// Subscribe adds a new subscriber to the publisher returning the channel.
func (p *Publisher) Subscribe() chan interface{} {
	return p.SubscribeTopic(nil)
}

// SubscribeTopic adds a new subscriber that filters messages sent by a topic.
func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} {
	ch := make(chan interface{}, p.buffer)
	p.m.Lock()
	p.subscribers[ch] = topic
	p.m.Unlock()
	return ch
}

// SubscribeTopicWithBuffer adds a new subscriber that filters messages sent by a topic.
// The returned channel has a buffer of the specified size.
func (p *Publisher) SubscribeTopicWithBuffer(topic topicFunc, buffer int) chan interface{} {
	ch := make(chan interface{}, buffer)
	p.m.Lock()
	p.subscribers[ch] = topic
	p.m.Unlock()
	return ch
}

// Evict removes the specified subscriber from receiving any more messages.
func (p *Publisher) Evict(sub chan interface{}) {
	p.m.Lock()
	delete(p.subscribers, sub)
	close(sub)
	p.m.Unlock()
}

// Publish sends the data in v to all subscribers currently registered with the publisher.
func (p *Publisher) Publish(v interface{}) {
	p.m.RLock()
	if len(p.subscribers) == 0 {
		p.m.RUnlock()
		return
	}

	wg := wgPool.Get().(*sync.WaitGroup)
	for sub, topic := range p.subscribers {
		wg.Add(1)
		go p.sendTopic(sub, topic, v, wg)
	}
	wg.Wait()
	wgPool.Put(wg)
	p.m.RUnlock()
}

// Close closes the channels to all subscribers registered with the publisher.
func (p *Publisher) Close() {
	p.m.Lock()
	for sub := range p.subscribers {
		delete(p.subscribers, sub)
		close(sub)
	}
	p.m.Unlock()
}

func (p *Publisher) sendTopic(sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup) {
	defer wg.Done()
	if topic != nil && !topic(v) {
		return
	}

	// send under a select as to not block if the receiver is unavailable
	if p.timeout > 0 {
		timeout := time.NewTimer(p.timeout)
		defer timeout.Stop()

		select {
		case sub <- v:
		case <-timeout.C:
		}
		return
	}

	select {
	case sub <- v:
	default:
	}
}
<commit_msg>docker stats: fix 'panic: close of closed channel'<commit_after>package pubsub // import "github.com/docker/docker/pkg/pubsub"

import (
	"sync"
	"time"
)

var wgPool = sync.Pool{New: func() interface{} { return new(sync.WaitGroup) }}

// NewPublisher creates a new pub/sub publisher to broadcast messages.
// The duration is used as the send timeout as to not block the publisher publishing
// messages to other clients if one client is slow or unresponsive.
// The buffer is used when creating new channels for subscribers.
func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher {
	return &Publisher{
		buffer:      buffer,
		timeout:     publishTimeout,
		subscribers: make(map[subscriber]topicFunc),
	}
}

type subscriber chan interface{}
type topicFunc func(v interface{}) bool

// Publisher is basic pub/sub structure. Allows to send events and subscribe
// to them. Can be safely used from multiple goroutines.
type Publisher struct {
	m           sync.RWMutex
	buffer      int
	timeout     time.Duration
	subscribers map[subscriber]topicFunc
}

// Len returns the number of subscribers for the publisher
func (p *Publisher) Len() int {
	p.m.RLock()
	i := len(p.subscribers)
	p.m.RUnlock()
	return i
}

// Subscribe adds a new subscriber to the publisher returning the channel.
func (p *Publisher) Subscribe() chan interface{} {
	return p.SubscribeTopic(nil)
}

// SubscribeTopic adds a new subscriber that filters messages sent by a topic.
func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} {
	ch := make(chan interface{}, p.buffer)
	p.m.Lock()
	p.subscribers[ch] = topic
	p.m.Unlock()
	return ch
}

// SubscribeTopicWithBuffer adds a new subscriber that filters messages sent by a topic.
// The returned channel has a buffer of the specified size.
func (p *Publisher) SubscribeTopicWithBuffer(topic topicFunc, buffer int) chan interface{} {
	ch := make(chan interface{}, buffer)
	p.m.Lock()
	p.subscribers[ch] = topic
	p.m.Unlock()
	return ch
}

// Evict removes the specified subscriber from receiving any more messages.
func (p *Publisher) Evict(sub chan interface{}) {
	p.m.Lock()
	_, exists := p.subscribers[sub]
	if exists {
		delete(p.subscribers, sub)
		close(sub)
	}
	p.m.Unlock()
}

// Publish sends the data in v to all subscribers currently registered with the publisher.
func (p *Publisher) Publish(v interface{}) {
	p.m.RLock()
	if len(p.subscribers) == 0 {
		p.m.RUnlock()
		return
	}

	wg := wgPool.Get().(*sync.WaitGroup)
	for sub, topic := range p.subscribers {
		wg.Add(1)
		go p.sendTopic(sub, topic, v, wg)
	}
	wg.Wait()
	wgPool.Put(wg)
	p.m.RUnlock()
}

// Close closes the channels to all subscribers registered with the publisher.
func (p *Publisher) Close() {
	p.m.Lock()
	for sub := range p.subscribers {
		delete(p.subscribers, sub)
		close(sub)
	}
	p.m.Unlock()
}

func (p *Publisher) sendTopic(sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup) {
	defer wg.Done()
	if topic != nil && !topic(v) {
		return
	}

	// send under a select as to not block if the receiver is unavailable
	if p.timeout > 0 {
		timeout := time.NewTimer(p.timeout)
		defer timeout.Stop()

		select {
		case sub <- v:
		case <-timeout.C:
		}
		return
	}

	select {
	case sub <- v:
	default:
	}
}
<|endoftext|>


<commit_before>package flapjack

import "testing"

func TestDialFails(t *testing.T) {
	address := "localhost:55555" // non-existent Redis server
	database := 0
	_, err := Dial(address, database)

	if err == nil {
		t.Error("Dial should fail")
	}
}

func TestSendSucceeds(t *testing.T) {
  transport, _ := Dial("localhost:6379", 9)
  event := Event{
    Entity:  "hello",
    Check:   "world",
    State:   "ok",
    Summary: "hello world",
  }

  _, err := transport.Send(event)
  if err != nil {
    t.Error("Error upon sending event: %v", err)
  }

}

func TestSendFails(t *testing.T) {
  transport, _ := Dial("localhost:6379", 9)
  event := Event{}

  _, err := transport.Send(event)
  if err == nil {
    t.Error("Expected error upon sending event: %v", err)
  }
}
<commit_msg>gofmt cleanup<commit_after>package flapjack

import "testing"

func TestDialFails(t *testing.T) {
	address := "localhost:55555" // non-existent Redis server
	database := 0
	_, err := Dial(address, database)

	if err == nil {
		t.Error("Dial should fail")
	}
}

func TestSendSucceeds(t *testing.T) {
	transport, _ := Dial("localhost:6379", 9)
	event := Event{
		Entity:  "hello",
		Check:   "world",
		State:   "ok",
		Summary: "hello world",
	}

	_, err := transport.Send(event)
	if err != nil {
		t.Error("Error upon sending event: %v", err)
	}

}

func TestSendFails(t *testing.T) {
	transport, _ := Dial("localhost:6379", 9)
	event := Event{}

	_, err := transport.Send(event)
	if err == nil {
		t.Error("Expected error upon sending event: %v", err)
	}
}
<|endoftext|>