<commit_before>/*
Copyright 2019 The Ceph-CSI 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 util

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/hashicorp/vault/api"
	loss "github.com/libopenstorage/secrets"
	"github.com/libopenstorage/secrets/vault"
)

const (
	// path to service account token that will be used to authenticate with Vault
	// #nosec
	serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"

	// vault configuration defaults
	vaultDefaultAuthPath       = "/v1/auth/kubernetes/login"
	vaultDefaultRole           = "csi-kubernetes"
	vaultDefaultNamespace      = ""
	vaultDefaultPassphrasePath = ""
	vaultDefaultCAVerify       = "true"
)

var (
	errConfigOptionMissing = errors.New("configuration option not set")
	errConfigOptionInvalid = errors.New("configuration option not valid")
)

/*
VaultKMS represents a Hashicorp Vault KMS configuration

Example JSON structure in the KMS config is,
{
	"local_vault_unique_identifier": {
		"encryptionKMSType": "vault",
		"vaultAddress": "https://127.0.0.1:8500",
		"vaultAuthPath": "/v1/auth/kubernetes/login",
		"vaultRole": "csi-kubernetes",
		"vaultNamespace": "",
		"vaultPassphraseRoot": "/v1/secret",
		"vaultPassphrasePath": "",
		"vaultCAVerify": true,
		"vaultCAFromSecret": "vault-ca"
	},
	...
}.
*/

type vaultConnection struct {
	EncryptionKMSID string
	vaultConfig     map[string]interface{}
	keyContext      map[string]string
}

type VaultKMS struct {
	vaultConnection

	// vaultPassphrasePath (VPP) used to be added before the "key" of the
	// secret (like /v1/secret/data/<VPP>/key)
	vaultPassphrasePath string

	secrets loss.Secrets
}

// setConfigString fetches a value from a configuration map and converts it to
// a string.
//
// If the value is not available, *option is not adjusted and
// errConfigOptionMissing is returned.
// In case the value is available, but can not be converted to a string,
// errConfigOptionInvalid is returned.
func setConfigString(option *string, config map[string]interface{}, key string) error {
	value, ok := config[key]
	if !ok {
		return fmt.Errorf("%w: %s", errConfigOptionMissing, key)
	}

	s, ok := value.(string)
	if !ok {
		return fmt.Errorf("%w: expected string for %q, but got %T",
			errConfigOptionInvalid, key, value)
	}

	*option = s
	return nil
}

func (vc *vaultConnection) initConnection(kmsID string, config map[string]interface{}, secrets map[string]string) error {
	vaultConfig := make(map[string]interface{})
	keyContext := make(map[string]string)

	vc.EncryptionKMSID = kmsID

	vaultAddress := ""
	err := setConfigString(&vaultAddress, config, "vaultAddress")
	if err != nil {
		return err
	}
	vaultConfig[api.EnvVaultAddress] = vaultAddress

	vaultNamespace := vaultDefaultNamespace
	err = setConfigString(&vaultNamespace, config, "vaultNamespace")
	if err != nil {
		return err
	}
	vaultConfig[api.EnvVaultNamespace] = vaultNamespace
	keyContext[loss.KeyVaultNamespace] = vaultNamespace

	verifyCA := vaultDefaultCAVerify
	err = setConfigString(&verifyCA, config, "vaultCAVerify")
	if err != nil {
		return err
	}

	vaultCAVerify, err := strconv.ParseBool(verifyCA)
	if err != nil {
		return fmt.Errorf("failed to parse 'vaultCAVerify': %w", err)
	}
	vaultConfig[api.EnvVaultInsecure] = !vaultCAVerify

	vaultCAFromSecret := ""
	err = setConfigString(&vaultCAFromSecret, config, "vaultCAFromSecret")
	if err == nil && vaultCAFromSecret != "" {
		caPEM, ok := secrets[vaultCAFromSecret]
		if !ok {
			return fmt.Errorf("missing vault CA in secret %s", vaultCAFromSecret)
		}

		var err error
		vaultConfig[api.EnvVaultCACert], err = createTempFile("vault-ca-cert", []byte(caPEM))
		if err != nil {
			return fmt.Errorf("failed to create temporary file for Vault CA: %w", err)
		}
		// TODO: delete f.Name() when vaultConnection is destroyed
	} else if !errors.Is(err, errConfigOptionMissing) {
		return err
	}

	vc.keyContext = keyContext
	vc.vaultConfig = vaultConfig

	return nil
}

// InitVaultKMS returns an interface to HashiCorp Vault KMS.
func InitVaultKMS(kmsID string, config map[string]interface{}, secrets map[string]string) (EncryptionKMS, error) {
	kms := &VaultKMS{}
	err := kms.initConnection(kmsID, config, secrets)
	if err != nil {
		return nil, fmt.Errorf("failed to initialize Vault connection: %w", err)
	}

	vaultAuthPath := vaultDefaultAuthPath
	err = setConfigString(&vaultAuthPath, config, "vaultAuthPath")
	if err != nil {
		return nil, err
	}

	kms.vaultConfig[vault.AuthMountPath], err = detectAuthMountPath(vaultAuthPath)
	if err != nil {
		return nil, fmt.Errorf("failed to set %s in Vault config: %w", vault.AuthMountPath, err)
	}

	vaultRole := vaultDefaultRole
	err = setConfigString(&vaultRole, config, "vaultRole")
	if err != nil {
		return nil, err
	}
	kms.vaultConfig[vault.AuthKubernetesRole] = vaultRole

	// vault.VaultBackendPathKey is "secret/" by default, use vaultPassphraseRoot if configured
	vaultPassphraseRoot := ""
	err = setConfigString(&vaultPassphraseRoot, config, "vaultPassphraseRoot")
	if err == nil {
		// the old example did have "/v1/secret/", convert that format
		if strings.HasPrefix(vaultPassphraseRoot, "/v1/") {
			kms.vaultConfig[vault.VaultBackendPathKey] = strings.TrimPrefix(vaultPassphraseRoot, "/v1/")
		} else {
			kms.vaultConfig[vault.VaultBackendPathKey] = vaultPassphraseRoot
		}
	} else if !errors.Is(err, errConfigOptionMissing) {
		return nil, err
	}

	kms.vaultPassphrasePath = vaultDefaultPassphrasePath
	err = setConfigString(&kms.vaultPassphrasePath, config, "vaultPassphrasePath")
	if err != nil {
		return nil, err
	}

	// FIXME: vault.AuthKubernetesTokenPath is not enough? EnvVaultToken needs to be set?
	kms.vaultConfig[vault.AuthMethod] = vault.AuthMethodKubernetes
	kms.vaultConfig[vault.AuthKubernetesTokenPath] = serviceAccountTokenPath

	v, err := vault.New(kms.vaultConfig)
	if err != nil {
		return nil, fmt.Errorf("failed creating new Vault Secrets: %w", err)
	}
	kms.secrets = v

	return kms, nil
}

// GetID is returning correlation ID to KMS configuration.
func (vc *vaultConnection) GetID() string {
	return vc.EncryptionKMSID
}

// GetPassphrase returns passphrase from Vault. The passphrase is stored in a
// data.data.passphrase structure.
func (kms *VaultKMS) GetPassphrase(key string) (string, error) {
	s, err := kms.secrets.GetSecret(filepath.Join(kms.vaultPassphrasePath, key), kms.keyContext)
	if errors.Is(err, loss.ErrInvalidSecretId) {
		return "", MissingPassphrase{err}
	} else if err != nil {
		return "", err
	}

	data, ok := s["data"].(map[string]interface{})
	if !ok {
		return "", fmt.Errorf("failed parsing data for get passphrase request for %q", key)
	}
	passphrase, ok := data["passphrase"].(string)
	if !ok {
		return "", fmt.Errorf("failed parsing passphrase for get passphrase request for %q", key)
	}

	return passphrase, nil
}

// SavePassphrase saves new passphrase in Vault.
func (kms *VaultKMS) SavePassphrase(key, value string) error {
	data := map[string]interface{}{
		"data": map[string]string{
			"passphrase": value,
		},
	}

	pathKey := filepath.Join(kms.vaultPassphrasePath, key)
	err := kms.secrets.PutSecret(pathKey, data, kms.keyContext)
	if err != nil {
		return fmt.Errorf("saving passphrase at %s request to vault failed: %w", pathKey, err)
	}

	return nil
}

// DeletePassphrase deletes passphrase from Vault.
func (kms *VaultKMS) DeletePassphrase(key string) error {
	pathKey := filepath.Join(kms.vaultPassphrasePath, key)
	err := kms.secrets.DeleteSecret(pathKey, kms.keyContext)
	if err != nil {
		return fmt.Errorf("delete passphrase at %s request to vault failed: %w", pathKey, err)
	}

	return nil
}

// detectAuthMountPath takes the vaultAuthPath configuration option that
// defaults to "/v1/auth/kubernetes/login" and makes it a vault.AuthMountPath
// like "kubernetes".
func detectAuthMountPath(path string) (string, error) {
	var authMountPath string

	if path == "" {
		return "", errors.New("path is empty")
	}

	// add all components between "login" and "auth" to authMountPath
	match := false
	parts := strings.Split(path, "/")
	for _, part := range parts {
		if part == "auth" {
			match = true
			continue
		}
		if part == "login" {
			break
		}
		if match && authMountPath == "" {
			authMountPath = part
		} else if match {
			authMountPath += "/" + part
		}
	}

	// in case authMountPath is empty, return original path as it was
	if authMountPath == "" {
		authMountPath = path
	}

	return authMountPath, nil
}

// createTempFile writes data to a temporary file that contains the pattern in
// the filename (see ioutil.TempFile for details).
func createTempFile(pattern string, data []byte) (string, error) {
	t, err := ioutil.TempFile("", pattern)
	if err != nil {
		return "", fmt.Errorf("failed to create temporary file: %w", err)
	}

	// delete the tmpfile on error
	defer func() {
		if err != nil {
			// ignore error on failure to remove tmpfile (gosec complains)
			_ = os.Remove(t.Name())
		}
	}()

	s, err := t.Write(data)
	if err != nil || s != len(data) {
		return "", fmt.Errorf("failed to write temporary file: %w", err)
	}
	err = t.Close()
	if err != nil {
		return "", fmt.Errorf("failed to close temporary file: %w", err)
	}

	return t.Name(), nil
}
<commit_msg>util: move Secrets to vaultConnection<commit_after>/*
Copyright 2019 The Ceph-CSI 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 util

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/hashicorp/vault/api"
	loss "github.com/libopenstorage/secrets"
	"github.com/libopenstorage/secrets/vault"
)

const (
	// path to service account token that will be used to authenticate with Vault
	// #nosec
	serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"

	// vault configuration defaults
	vaultDefaultAuthPath       = "/v1/auth/kubernetes/login"
	vaultDefaultRole           = "csi-kubernetes"
	vaultDefaultNamespace      = ""
	vaultDefaultPassphrasePath = ""
	vaultDefaultCAVerify       = "true"
)

var (
	errConfigOptionMissing = errors.New("configuration option not set")
	errConfigOptionInvalid = errors.New("configuration option not valid")
)

/*
VaultKMS represents a Hashicorp Vault KMS configuration

Example JSON structure in the KMS config is,
{
	"local_vault_unique_identifier": {
		"encryptionKMSType": "vault",
		"vaultAddress": "https://127.0.0.1:8500",
		"vaultAuthPath": "/v1/auth/kubernetes/login",
		"vaultRole": "csi-kubernetes",
		"vaultNamespace": "",
		"vaultPassphraseRoot": "/v1/secret",
		"vaultPassphrasePath": "",
		"vaultCAVerify": true,
		"vaultCAFromSecret": "vault-ca"
	},
	...
}.
*/

type vaultConnection struct {
	EncryptionKMSID string
	secrets         loss.Secrets
	vaultConfig     map[string]interface{}
	keyContext      map[string]string
}

type VaultKMS struct {
	vaultConnection

	// vaultPassphrasePath (VPP) used to be added before the "key" of the
	// secret (like /v1/secret/data/<VPP>/key)
	vaultPassphrasePath string
}

// setConfigString fetches a value from a configuration map and converts it to
// a string.
//
// If the value is not available, *option is not adjusted and
// errConfigOptionMissing is returned.
// In case the value is available, but can not be converted to a string,
// errConfigOptionInvalid is returned.
func setConfigString(option *string, config map[string]interface{}, key string) error {
	value, ok := config[key]
	if !ok {
		return fmt.Errorf("%w: %s", errConfigOptionMissing, key)
	}

	s, ok := value.(string)
	if !ok {
		return fmt.Errorf("%w: expected string for %q, but got %T",
			errConfigOptionInvalid, key, value)
	}

	*option = s
	return nil
}

func (vc *vaultConnection) initConnection(kmsID string, config map[string]interface{}, secrets map[string]string) error {
	vaultConfig := make(map[string]interface{})
	keyContext := make(map[string]string)

	vc.EncryptionKMSID = kmsID

	vaultAddress := ""
	err := setConfigString(&vaultAddress, config, "vaultAddress")
	if err != nil {
		return err
	}
	vaultConfig[api.EnvVaultAddress] = vaultAddress

	vaultNamespace := vaultDefaultNamespace
	err = setConfigString(&vaultNamespace, config, "vaultNamespace")
	if err != nil {
		return err
	}
	vaultConfig[api.EnvVaultNamespace] = vaultNamespace
	keyContext[loss.KeyVaultNamespace] = vaultNamespace

	verifyCA := vaultDefaultCAVerify
	err = setConfigString(&verifyCA, config, "vaultCAVerify")
	if err != nil {
		return err
	}

	vaultCAVerify, err := strconv.ParseBool(verifyCA)
	if err != nil {
		return fmt.Errorf("failed to parse 'vaultCAVerify': %w", err)
	}
	vaultConfig[api.EnvVaultInsecure] = !vaultCAVerify

	vaultCAFromSecret := ""
	err = setConfigString(&vaultCAFromSecret, config, "vaultCAFromSecret")
	if err == nil && vaultCAFromSecret != "" {
		caPEM, ok := secrets[vaultCAFromSecret]
		if !ok {
			return fmt.Errorf("missing vault CA in secret %s", vaultCAFromSecret)
		}

		vaultConfig[api.EnvVaultCACert], err = createTempFile("vault-ca-cert", []byte(caPEM))
		if err != nil {
			return fmt.Errorf("failed to create temporary file for Vault CA: %w", err)
		}
		// TODO: delete f.Name() when vaultConnection is destroyed
	} else if !errors.Is(err, errConfigOptionMissing) {
		return err
	}

	vc.keyContext = keyContext
	vc.vaultConfig = vaultConfig

	return nil
}

// connectVault creates a new connection to Vault. This should be called after
// filling vc.vaultConfig.
func (vc *vaultConnection) connectVault() error {
	v, err := vault.New(vc.vaultConfig)
	if err != nil {
		return fmt.Errorf("failed creating new Vault Secrets: %w", err)
	}
	vc.secrets = v

	return nil
}

// InitVaultKMS returns an interface to HashiCorp Vault KMS.
func InitVaultKMS(kmsID string, config map[string]interface{}, secrets map[string]string) (EncryptionKMS, error) {
	kms := &VaultKMS{}
	err := kms.initConnection(kmsID, config, secrets)
	if err != nil {
		return nil, fmt.Errorf("failed to initialize Vault connection: %w", err)
	}

	vaultAuthPath := vaultDefaultAuthPath
	err = setConfigString(&vaultAuthPath, config, "vaultAuthPath")
	if err != nil {
		return nil, err
	}

	kms.vaultConfig[vault.AuthMountPath], err = detectAuthMountPath(vaultAuthPath)
	if err != nil {
		return nil, fmt.Errorf("failed to set %s in Vault config: %w", vault.AuthMountPath, err)
	}

	vaultRole := vaultDefaultRole
	err = setConfigString(&vaultRole, config, "vaultRole")
	if err != nil {
		return nil, err
	}
	kms.vaultConfig[vault.AuthKubernetesRole] = vaultRole

	// vault.VaultBackendPathKey is "secret/" by default, use vaultPassphraseRoot if configured
	vaultPassphraseRoot := ""
	err = setConfigString(&vaultPassphraseRoot, config, "vaultPassphraseRoot")
	if err == nil {
		// the old example did have "/v1/secret/", convert that format
		if strings.HasPrefix(vaultPassphraseRoot, "/v1/") {
			kms.vaultConfig[vault.VaultBackendPathKey] = strings.TrimPrefix(vaultPassphraseRoot, "/v1/")
		} else {
			kms.vaultConfig[vault.VaultBackendPathKey] = vaultPassphraseRoot
		}
	} else if !errors.Is(err, errConfigOptionMissing) {
		return nil, err
	}

	kms.vaultPassphrasePath = vaultDefaultPassphrasePath
	err = setConfigString(&kms.vaultPassphrasePath, config, "vaultPassphrasePath")
	if err != nil {
		return nil, err
	}

	// FIXME: vault.AuthKubernetesTokenPath is not enough? EnvVaultToken needs to be set?
	kms.vaultConfig[vault.AuthMethod] = vault.AuthMethodKubernetes
	kms.vaultConfig[vault.AuthKubernetesTokenPath] = serviceAccountTokenPath

	err = kms.connectVault()
	if err != nil {
		return nil, err
	}

	return kms, nil
}

// GetID is returning correlation ID to KMS configuration.
func (vc *vaultConnection) GetID() string {
	return vc.EncryptionKMSID
}

// GetPassphrase returns passphrase from Vault. The passphrase is stored in a
// data.data.passphrase structure.
func (kms *VaultKMS) GetPassphrase(key string) (string, error) {
	s, err := kms.secrets.GetSecret(filepath.Join(kms.vaultPassphrasePath, key), kms.keyContext)
	if errors.Is(err, loss.ErrInvalidSecretId) {
		return "", MissingPassphrase{err}
	} else if err != nil {
		return "", err
	}

	data, ok := s["data"].(map[string]interface{})
	if !ok {
		return "", fmt.Errorf("failed parsing data for get passphrase request for %q", key)
	}
	passphrase, ok := data["passphrase"].(string)
	if !ok {
		return "", fmt.Errorf("failed parsing passphrase for get passphrase request for %q", key)
	}

	return passphrase, nil
}

// SavePassphrase saves new passphrase in Vault.
func (kms *VaultKMS) SavePassphrase(key, value string) error {
	data := map[string]interface{}{
		"data": map[string]string{
			"passphrase": value,
		},
	}

	pathKey := filepath.Join(kms.vaultPassphrasePath, key)
	err := kms.secrets.PutSecret(pathKey, data, kms.keyContext)
	if err != nil {
		return fmt.Errorf("saving passphrase at %s request to vault failed: %w", pathKey, err)
	}

	return nil
}

// DeletePassphrase deletes passphrase from Vault.
func (kms *VaultKMS) DeletePassphrase(key string) error {
	pathKey := filepath.Join(kms.vaultPassphrasePath, key)
	err := kms.secrets.DeleteSecret(pathKey, kms.keyContext)
	if err != nil {
		return fmt.Errorf("delete passphrase at %s request to vault failed: %w", pathKey, err)
	}

	return nil
}

// detectAuthMountPath takes the vaultAuthPath configuration option that
// defaults to "/v1/auth/kubernetes/login" and makes it a vault.AuthMountPath
// like "kubernetes".
func detectAuthMountPath(path string) (string, error) {
	var authMountPath string

	if path == "" {
		return "", errors.New("path is empty")
	}

	// add all components between "login" and "auth" to authMountPath
	match := false
	parts := strings.Split(path, "/")
	for _, part := range parts {
		if part == "auth" {
			match = true
			continue
		}
		if part == "login" {
			break
		}
		if match && authMountPath == "" {
			authMountPath = part
		} else if match {
			authMountPath += "/" + part
		}
	}

	// in case authMountPath is empty, return original path as it was
	if authMountPath == "" {
		authMountPath = path
	}

	return authMountPath, nil
}

// createTempFile writes data to a temporary file that contains the pattern in
// the filename (see ioutil.TempFile for details).
func createTempFile(pattern string, data []byte) (string, error) {
	t, err := ioutil.TempFile("", pattern)
	if err != nil {
		return "", fmt.Errorf("failed to create temporary file: %w", err)
	}

	// delete the tmpfile on error
	defer func() {
		if err != nil {
			// ignore error on failure to remove tmpfile (gosec complains)
			_ = os.Remove(t.Name())
		}
	}()

	s, err := t.Write(data)
	if err != nil || s != len(data) {
		return "", fmt.Errorf("failed to write temporary file: %w", err)
	}
	err = t.Close()
	if err != nil {
		return "", fmt.Errorf("failed to close temporary file: %w", err)
	}

	return t.Name(), nil
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"log"
	"math/rand"
	"runtime"
	"testing"
	"time"
)

func tic() time.Time {
	return time.Now()
}

func toc(ts time.Time) time.Duration {
	return time.Now().Sub(ts)
}

func TestInit(t *testing.T) {
	var d, w int64
	d = 75
	w = 700
	cms := NewCMSketch(d, w)
	if cms.Width != w {
		t.Errorf("Width: %d != %d\n", w, cms.Width)
	}
	if cms.Depth != d {
		t.Errorf("Depth: %d != %d\n", d, cms.Depth)
	}
}

func TestInsert(t *testing.T) {
	cms := NewCMSketch(2, 5)
	r := rand.New(rand.NewSource(0))
	rh := RandomHashes(r, 2)
	cms.Hash = rh
	if rh[0].Apply(1)%5 != 2 {
		t.Errorf("hash[0](1) %% 5 != 2\n")
	}
	if rh[1].Apply(1)%5 != 3 {
		t.Errorf("hash[0](1) %% 5 != 2\n")
	}
	fmt.Printf("%v\n", cms)
	t.Logf("Before:\n%v\n", cms.Counter.String())
	cms.UpdateSerial(1, 1)
	t.Logf("After:\n%v\n", cms.Counter.String())
	if cms.Counter.Read(0, 2) != 1 {
		t.Errorf("Bad first row\n")
	}
	if cms.Counter.Read(1, 3) != 1 {
		t.Errorf("Bad second row\n")
	}
}

//loss: implements a generic loss function for scoring the implementation.
func loss(cj, qj float64) float64 {
	return (cj - qj) * (cj - qj)
}

//makeCMS: Initialize the CMSKETCH to standard values for test
func makeCMS(r *rand.Rand) *CMSketch {
	var Depth, Width int64
	Depth = 80
	Width = 160
	hslice := RandomHashes(r, Depth)
	cms := NewCMSketch(Depth, Width)
	cms.Hash = hslice
	return &cms
}

//makeCMSparams: Initialize the CMSKETCH to parameters that are passed
func makeCMSparams(r *rand.Rand, Depth int64, Width int64) *CMSketch {
	hslice := RandomHashes(r, Depth)
	cms := NewCMSketch(Depth, Width)
	cms.Hash = hslice
	return &cms
}

//makeZipfer: Initialize the stream of random elements for the tests.
func makeZipfer(r *rand.Rand) *rand.Zipf {
	//Make the zipf distribution of random input
	var s, v float64
	var imax uint64
	s = 1.2
	v = 1.0
	imax = 2 << 10
	zipfer := rand.NewZipf(r, s, v, imax)
	return zipfer
}

//TestSpeed: Test the speed for Serial insertions
func TestSpeed(t *testing.T) {
	src := rand.NewSource(0)
	r := rand.New(src)
	cms := *makeCMS(r)
	var efactor, numElements int64
	efactor = 10
	numElements = cms.Depth * cms.Width * efactor
	zipfer := makeZipfer(r)
	log.Printf("Inserting\n")
	ts := time.Now()
	var j, z int64
	for j = 0; j < numElements; j++ {
		z = int64(zipfer.Uint64())
		cms.UpdateSerial(z, 1)
	}
	te := time.Now().Sub(ts)
	fmt.Printf("time Serial: %v\n", te)
	numProcs := runtime.NumCPU()
	runtime.GOMAXPROCS(numProcs)
	ts = time.Now()
	for j = 0; j < numElements; j++ {
		z = int64(zipfer.Uint64())
		cms.UpdateDepthParallel(z, 1, int64(numProcs))
	}
	te = time.Now().Sub(ts)
	fmt.Printf("time parallel: %v\n", te)
}

//TestAccuracy: Run a test storing the right answers
//compute the loss using the function loss
func TestAccuracy(t *testing.T) {
	src := rand.NewSource(0)
	r := rand.New(src)
	cms := *makeCMS(r)
	var efactor, numElements int64
	efactor = 10
	numElements = cms.Depth * cms.Width * efactor
	zipfer := makeZipfer(r)

	//Use set to store the exact answers
	set := make(map[int64]int64)
	log.Printf("Inserting\n")
	ts := time.Now()
	var j, z int64
	for j = 0; j < numElements; j++ {
		z = int64(zipfer.Uint64())
		set[z] += 1
		//fmt.Println(z)
		cms.UpdateSerial(z, 1)
	}
	te := time.Now().Sub(ts)
	fmt.Printf("time accuracy: %v\n", te)
	//fmt.Printf("%s\n", cms.Counter.String())
	var qj int64 //approximate answers
	var totalLoss float64
	for j, cj := range set {
		qj = cms.PointQuery(j)
		//fmt.Printf("results:%d %d %d %f\n", j, qj, cj, float64(qj)/float64(cj))
		totalLoss += loss(float64(cj), float64(qj))
	}
	fmt.Printf("Total Loss: %f/%d\n", totalLoss, numElements)
}

//drawZipf: initialize a zipf distribution generator and draw batchsize samples.
//Not for use in external modules because it does not set the seed properly.
func drawZipf(batchsize int64) []int64 {
	src := rand.NewSource(0)
	r := rand.New(src)
	zipfer := makeZipfer(r)

	elements := make([]int64, batchsize)
	var i, z int64
	var ts time.Time
	var te time.Duration
	ts = tic()
	for i = 0; i < batchsize; i++ {
		z = int64(zipfer.Uint64())
		elements[i] = z
	}
	te = toc(ts)
	fmt.Printf("time zipfer: %s\n", te)
	return elements
}

//TestBatchInsert: Compare the data structures after making batch inserts to one
//where we have made serial insertions to test that they produce the same result
func TestBatchInsert(t *testing.T) {
	var batchsize int64
	batchsize = 1000000
	elements := drawZipf(batchsize)
	src := rand.NewSource(4)
	r := rand.New(src)
	cms := makeCMS(r)
	ts := tic()
	for _, z := range elements {
		cms.UpdateSerial(z, 1)
	}
	te := toc(ts)
	fmt.Printf("time single insertions: %v\n", te)
	t.Logf("cms:\n%v\n", cms)
	batchcms := cms.Clone()
	ch := make(chan int, cms.Depth)
	NumCPU := runtime.NumCPU()
	runtime.GOMAXPROCS(NumCPU)
	ts = tic()
	batchcms.BatchUpdate(elements, false, ch)
	te = toc(ts)
	fmt.Printf("time batch insertions: %v\n", te)
	result := cms.Equal(&batchcms)
	if !result {
		t.Errorf("the sketches did not come up equal\n")
		t.Logf("batchcms:\n%v\n", batchcms)
	}
	t.Logf("Working on sorted batch updates")
	sbatchcms := cms.Clone()
	ts = tic()
	sbatchcms.BatchUpdate(elements, true, ch)
	te = toc(ts)
	fmt.Printf("time sorted batch insertions: %v\n", te)
	result = cms.Equal(&sbatchcms)
	if !result {
		t.Errorf("sortedbatch sketch != regular sketch\n")
		t.Logf("sbatchcms:\n%v\n", sbatchcms)
	}
}

func sampleZipf(zipfer *rand.Zipf, batchsize int64, elements []int64) {
	var i int64
	var z int64
	for i = 0; i < batchsize; i++ {
		z = int64(zipfer.Uint64())
		elements[i] = z
	}
}

var cmsDepth int64 = 2 << 10
var cmsWidth int64 = 2 << 13
var batchsize int64 = 2 << 15

func BenchmarkSequeInsert(b *testing.B) {
	src := rand.NewSource(4)
	r := rand.New(src)
	cms := makeCMSparams(r, cmsDepth, cmsWidth)
	zipfer := makeZipfer(r)
	elements := make([]int64, batchsize)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		sampleZipf(zipfer, batchsize, elements)
		for _, z := range elements {
			cms.UpdateSerial(z, 1)
		}
	}

}

func benchmarkBatchInsert(batchsize int64, b *testing.B) {
	sort := false
	src := rand.NewSource(4)
	r := rand.New(src)
	cms := makeCMSparams(r, cmsDepth, cmsWidth)
	ch := make(chan int, cms.Depth)
	zipfer := makeZipfer(r)
	elements := make([]int64, batchsize)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		sampleZipf(zipfer, batchsize, elements)
		cms.BatchUpdate(elements, sort, ch)
	}
}

func BenchmarkBatchInsert(b *testing.B) { benchmarkBatchInsert(batchsize, b) }

func TestDecrement(t *testing.T) {
	t.Logf("Decrement not implemented yet")
}
<commit_msg>adding benchmark for weak scaling.<commit_after>package main

import (
	"fmt"
	"log"
	"math/rand"
	"os"
	"runtime"
	"strconv"
	"testing"
	"time"
)

func tic() time.Time {
	return time.Now()
}

func toc(ts time.Time) time.Duration {
	return time.Now().Sub(ts)
}

func TestInit(t *testing.T) {
	var d, w int64
	d = 75
	w = 700
	cms := NewCMSketch(d, w)
	if cms.Width != w {
		t.Errorf("Width: %d != %d\n", w, cms.Width)
	}
	if cms.Depth != d {
		t.Errorf("Depth: %d != %d\n", d, cms.Depth)
	}
}

func TestInsert(t *testing.T) {
	cms := NewCMSketch(2, 5)
	r := rand.New(rand.NewSource(0))
	rh := RandomHashes(r, 2)
	cms.Hash = rh
	if rh[0].Apply(1)%5 != 2 {
		t.Errorf("hash[0](1) %% 5 != 2\n")
	}
	if rh[1].Apply(1)%5 != 3 {
		t.Errorf("hash[0](1) %% 5 != 2\n")
	}
	fmt.Printf("%v\n", cms)
	t.Logf("Before:\n%v\n", cms.Counter.String())
	cms.UpdateSerial(1, 1)
	t.Logf("After:\n%v\n", cms.Counter.String())
	if cms.Counter.Read(0, 2) != 1 {
		t.Errorf("Bad first row\n")
	}
	if cms.Counter.Read(1, 3) != 1 {
		t.Errorf("Bad second row\n")
	}
}

//loss: implements a generic loss function for scoring the implementation.
func loss(cj, qj float64) float64 {
	return (cj - qj) * (cj - qj)
}

//makeCMS: Initialize the CMSKETCH to standard values for test
func makeCMS(r *rand.Rand) *CMSketch {
	var Depth, Width int64
	Depth = 80
	Width = 160
	hslice := RandomHashes(r, Depth)
	cms := NewCMSketch(Depth, Width)
	cms.Hash = hslice
	return &cms
}

//makeCMSparams: Initialize the CMSKETCH to parameters that are passed
func makeCMSparams(r *rand.Rand, Depth int64, Width int64) *CMSketch {
	hslice := RandomHashes(r, Depth)
	cms := NewCMSketch(Depth, Width)
	cms.Hash = hslice
	return &cms
}

//makeZipfer: Initialize the stream of random elements for the tests.
func makeZipfer(r *rand.Rand) *rand.Zipf {
	//Make the zipf distribution of random input
	var s, v float64
	var imax uint64
	s = 1.2
	v = 1.0
	imax = 2 << 10
	zipfer := rand.NewZipf(r, s, v, imax)
	return zipfer
}

//TestSpeed: Test the speed for Serial insertions
func TestSpeed(t *testing.T) {
	src := rand.NewSource(0)
	r := rand.New(src)
	cms := *makeCMS(r)
	var efactor, numElements int64
	efactor = 10
	numElements = cms.Depth * cms.Width * efactor
	zipfer := makeZipfer(r)
	log.Printf("Inserting\n")
	ts := time.Now()
	var j, z int64
	for j = 0; j < numElements; j++ {
		z = int64(zipfer.Uint64())
		cms.UpdateSerial(z, 1)
	}
	te := time.Now().Sub(ts)
	fmt.Printf("time Serial: %v\n", te)
	numProcs := runtime.NumCPU()
	runtime.GOMAXPROCS(numProcs)
	ts = time.Now()
	for j = 0; j < numElements; j++ {
		z = int64(zipfer.Uint64())
		cms.UpdateDepthParallel(z, 1, int64(numProcs))
	}
	te = time.Now().Sub(ts)
	fmt.Printf("time parallel: %v\n", te)
}

//TestAccuracy: Run a test storing the right answers
//compute the loss using the function loss
func TestAccuracy(t *testing.T) {
	src := rand.NewSource(0)
	r := rand.New(src)
	cms := *makeCMS(r)
	var efactor, numElements int64
	efactor = 10
	numElements = cms.Depth * cms.Width * efactor
	zipfer := makeZipfer(r)

	//Use set to store the exact answers
	set := make(map[int64]int64)
	log.Printf("Inserting\n")
	ts := time.Now()
	var j, z int64
	for j = 0; j < numElements; j++ {
		z = int64(zipfer.Uint64())
		set[z] += 1
		//fmt.Println(z)
		cms.UpdateSerial(z, 1)
	}
	te := time.Now().Sub(ts)
	fmt.Printf("time accuracy: %v\n", te)
	//fmt.Printf("%s\n", cms.Counter.String())
	var qj int64 //approximate answers
	var totalLoss float64
	for j, cj := range set {
		qj = cms.PointQuery(j)
		//fmt.Printf("results:%d %d %d %f\n", j, qj, cj, float64(qj)/float64(cj))
		totalLoss += loss(float64(cj), float64(qj))
	}
	fmt.Printf("Total Loss: %f/%d\n", totalLoss, numElements)
}

//drawZipf: initialize a zipf distribution generator and draw batchsize samples.
//Not for use in external modules because it does not set the seed properly.
func drawZipf(batchsize int64) []int64 {
	src := rand.NewSource(0)
	r := rand.New(src)
	zipfer := makeZipfer(r)

	elements := make([]int64, batchsize)
	var i, z int64
	var ts time.Time
	var te time.Duration
	ts = tic()
	for i = 0; i < batchsize; i++ {
		z = int64(zipfer.Uint64())
		elements[i] = z
	}
	te = toc(ts)
	fmt.Printf("time zipfer: %s\n", te)
	return elements
}

//TestBatchInsert: Compare the data structures after making batch inserts to one
//where we have made serial insertions to test that they produce the same result
func TestBatchInsert(t *testing.T) {
	var batchsize int64
	batchsize = 1000000
	elements := drawZipf(batchsize)
	src := rand.NewSource(4)
	r := rand.New(src)
	cms := makeCMS(r)
	ts := tic()
	for _, z := range elements {
		cms.UpdateSerial(z, 1)
	}
	te := toc(ts)
	fmt.Printf("time single insertions: %v\n", te)
	t.Logf("cms:\n%v\n", cms)
	batchcms := cms.Clone()
	ch := make(chan int, cms.Depth)
	NumCPU := runtime.NumCPU()
	runtime.GOMAXPROCS(NumCPU)
	ts = tic()
	batchcms.BatchUpdate(elements, false, ch)
	te = toc(ts)
	fmt.Printf("time batch insertions: %v\n", te)
	result := cms.Equal(&batchcms)
	if !result {
		t.Errorf("the sketches did not come up equal\n")
		t.Logf("batchcms:\n%v\n", batchcms)
	}
	t.Logf("Working on sorted batch updates")
	sbatchcms := cms.Clone()
	ts = tic()
	sbatchcms.BatchUpdate(elements, true, ch)
	te = toc(ts)
	fmt.Printf("time sorted batch insertions: %v\n", te)
	result = cms.Equal(&sbatchcms)
	if !result {
		t.Errorf("sortedbatch sketch != regular sketch\n")
		t.Logf("sbatchcms:\n%v\n", sbatchcms)
	}
}

func sampleZipf(zipfer *rand.Zipf, batchsize int64, elements []int64) {
	var i int64
	var z int64
	for i = 0; i < batchsize; i++ {
		z = int64(zipfer.Uint64())
		elements[i] = z
	}
}

var cmsDepth int64 = 2 << 10
var cmsWidth int64 = 2 << 13
var batchsize int64 = 2 << 15

func BenchmarkSequeInsert(b *testing.B) {
	src := rand.NewSource(4)
	r := rand.New(src)
	cms := makeCMSparams(r, cmsDepth, cmsWidth)
	zipfer := makeZipfer(r)
	elements := make([]int64, batchsize)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		sampleZipf(zipfer, batchsize, elements)
		for _, z := range elements {
			cms.UpdateSerial(z, 1)
		}
	}

}

func benchmarkBatchInsert(batchsize int64, b *testing.B) {
	sort := false
	src := rand.NewSource(4)
	r := rand.New(src)
	cms := makeCMSparams(r, cmsDepth, cmsWidth)
	ch := make(chan int, cms.Depth)
	zipfer := makeZipfer(r)
	elements := make([]int64, batchsize)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		sampleZipf(zipfer, batchsize, elements)
		cms.BatchUpdate(elements, sort, ch)
	}
}

/* weakScaling: measures the speedup as the problem size grows with the processor count. */
func weakScaling(numProcs int64, batchsize int64, drate int64, wrate int64, numbatches int64, b *testing.B) {
	sort := false
	src := rand.NewSource(4)
	r := rand.New(src)
	cms := makeCMSparams(r, cmsDepth+drate*numProcs, cmsWidth+wrate*numProcs)
	ch := make(chan int, cms.Depth)
	zipfer := makeZipfer(r)
	elements := make([]int64, batchsize)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		sampleZipf(zipfer, batchsize, elements)
		cms.BatchUpdate(elements, sort, ch)
	}
}

func BenchmarkWeakScaling(b *testing.B) {
	gmp, err := strconv.Atoi(os.Getenv("GOMAXPROCS"))
	if err != nil {
		b.Errorf("GOMAXPROCS was not an integer; %s", gmp)
	}
	//fmt.Printf("GOMAXPROCS: %d \n", gmp)
	//ts := tic()
	weakScaling(int64(gmp), batchsize, cmsDepth/8, 0, 1, b)
	//te := toc(ts)
	//fmt.Println(te)
}

func TestGOMAXPROCS(t *testing.T) {
	gmp, err := strconv.Atoi(os.Getenv("GOMAXPROCS"))
	if err != nil {
		t.Errorf("GOMAXPROCS was not an integer; %s", gmp)
	}
	fmt.Printf("logical core count: %d\n", runtime.NumCPU())
	fmt.Printf("GOMAXPROCS: %d \n", gmp)
}

func BenchmarkBatchInsert(b *testing.B) { benchmarkBatchInsert(batchsize, b) }

func TestDecrement(t *testing.T) {
	t.Logf("Decrement not implemented yet")
}
<|endoftext|>


<commit_before>package server

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/google/go-github/v32/github"

	"github.com/reviewdog/reviewdog/diff"
	"github.com/reviewdog/reviewdog/doghouse"
	"github.com/reviewdog/reviewdog/filter"
	"github.com/reviewdog/reviewdog/proto/rdf"
	"github.com/reviewdog/reviewdog/service/github/githubutils"
)

// GitHub check runs API cannot handle too large requests.
// Set max number of filtered findings to be shown in check-run summary.
// ERROR:
//  https://api.github.com/repos/easymotion/vim-easymotion/check-runs: 422
//  Invalid request.
//  Only 65535 characters are allowed; 250684 were supplied. []
const maxFilteredFinding = 150

// > The Checks API limits the number of annotations to a maximum of 50 per API
// > request.
// https://developer.github.com/v3/checks/runs/#output-object
const maxAnnotationsPerRequest = 50

type Checker struct {
	req *doghouse.CheckRequest
	gh  checkerGitHubClientInterface
}

func NewChecker(req *doghouse.CheckRequest, gh *github.Client) *Checker {
	return &Checker{req: req, gh: &checkerGitHubClient{Client: gh}}
}

func (ch *Checker) Check(ctx context.Context) (*doghouse.CheckResponse, error) {
	var filediffs []*diff.FileDiff
	if ch.req.PullRequest != 0 {
		var err error
		filediffs, err = ch.pullRequestDiff(ctx, ch.req.PullRequest)
		if err != nil {
			return nil, fmt.Errorf("fail to parse diff: %w", err)
		}
	}

	results := annotationsToDiagnostics(ch.req.Annotations)
	filterMode := ch.req.FilterMode
	//lint:ignore SA1019 Need to support OutsideDiff for backward compatibility.
	if ch.req.PullRequest == 0 || ch.req.OutsideDiff {
		// If it's not Pull Request run, do not filter results by diff regardless
		// of the filter mode.
		filterMode = filter.ModeNoFilter
	}
	filtered := filter.FilterCheck(results, filediffs, 1, "", filterMode)
	check, err := ch.createCheck(ctx)
	if err != nil {
		// If this error is StatusForbidden (403) here, it means reviewdog is
		// running on GitHub Actions and has only read permission (because it's
		// running for Pull Requests from forked repository). If the token itself
		// is invalid, reviewdog should return an error earlier (e.g. when reading
		// Pull Requests diff), so it should be ok not to return error here and
		// return results instead.
		if err, ok := err.(*github.ErrorResponse); ok && err.Response.StatusCode == http.StatusForbidden {
			return &doghouse.CheckResponse{CheckedResults: filtered}, nil
		}
		return nil, fmt.Errorf("failed to create check: %w", err)
	}

	checkRun, conclusion, err := ch.postCheck(ctx, check.GetID(), filtered)
	if err != nil {
		return nil, fmt.Errorf("failed to post result: %w", err)
	}
	res := &doghouse.CheckResponse{
		ReportURL:  checkRun.GetHTMLURL(),
		Conclusion: conclusion,
	}
	return res, nil
}

func (ch *Checker) postCheck(ctx context.Context, checkID int64, checks []*filter.FilteredDiagnostic) (*github.CheckRun, string, error) {
	var annotations []*github.CheckRunAnnotation
	for _, c := range checks {
		if !c.ShouldReport {
			continue
		}
		annotations = append(annotations, ch.toCheckRunAnnotation(c))
	}
	if len(annotations) > 0 {
		if err := ch.postAnnotations(ctx, checkID, annotations); err != nil {
			return nil, "", fmt.Errorf("failed to post annotations: %w", err)
		}
	}

	conclusion := "success"
	if len(annotations) > 0 {
		conclusion = ch.conclusion()
	}
	opt := github.UpdateCheckRunOptions{
		Name:        ch.checkName(),
		Status:      github.String("completed"),
		Conclusion:  github.String(conclusion),
		CompletedAt: &github.Timestamp{Time: time.Now()},
		Output: &github.CheckRunOutput{
			Title:   github.String(ch.checkTitle()),
			Summary: github.String(ch.summary(checks)),
		},
	}
	checkRun, err := ch.gh.UpdateCheckRun(ctx, ch.req.Owner, ch.req.Repo, checkID, opt)
	if err != nil {
		return nil, "", err
	}
	return checkRun, conclusion, nil
}

func (ch *Checker) createCheck(ctx context.Context) (*github.CheckRun, error) {
	opt := github.CreateCheckRunOptions{
		Name:    ch.checkName(),
		HeadSHA: ch.req.SHA,
		Status:  github.String("in_progress"),
	}
	return ch.gh.CreateCheckRun(ctx, ch.req.Owner, ch.req.Repo, opt)
}

func (ch *Checker) postAnnotations(ctx context.Context, checkID int64, annotations []*github.CheckRunAnnotation) error {
	opt := github.UpdateCheckRunOptions{
		Name: ch.checkName(),
		Output: &github.CheckRunOutput{
			Title:       github.String(ch.checkTitle()),
			Summary:     github.String(""), // Post summary with the last request.
			Annotations: annotations[:min(maxAnnotationsPerRequest, len(annotations))],
		},
	}
	if _, err := ch.gh.UpdateCheckRun(ctx, ch.req.Owner, ch.req.Repo, checkID, opt); err != nil {
		return err
	}
	if len(annotations) > maxAnnotationsPerRequest {
		return ch.postAnnotations(ctx, checkID, annotations[maxAnnotationsPerRequest:])
	}
	return nil
}

func (ch *Checker) checkName() string {
	if ch.req.Name != "" {
		return ch.req.Name
	}
	return "reviewdog"
}

func (ch *Checker) checkTitle() string {
	if name := ch.checkName(); name != "reviewdog" {
		return fmt.Sprintf("reviewdog [%s] report", name)
	}
	return "reviewdog report"
}

// https://developer.github.com/v3/checks/runs/#parameters-1
func (ch *Checker) conclusion() string {
	switch strings.ToLower(ch.req.Level) {
	case "info", "warning":
		return "neutral"
	}
	return "failure"
}

// https://developer.github.com/v3/checks/runs/#annotations-object
func (ch *Checker) annotationLevel(s rdf.Severity) string {
	switch s {
	case rdf.Severity_ERROR:
		return "failure"
	case rdf.Severity_WARNING:
		return "warning"
	case rdf.Severity_INFO:
		return "notice"
	default:
		return ch.reqAnnotationLevel()
	}
}

func (ch *Checker) reqAnnotationLevel() string {
	switch strings.ToLower(ch.req.Level) {
	case "info":
		return "notice"
	case "warning":
		return "warning"
	case "failure":
		return "failure"
	}
	return "failure"
}

func (ch *Checker) summary(checks []*filter.FilteredDiagnostic) string {
	var lines []string
	lines = append(lines, "reported by [reviewdog](https://github.com/reviewdog/reviewdog) :dog:")

	var findings []*filter.FilteredDiagnostic
	var filteredFindings []*filter.FilteredDiagnostic
	for _, c := range checks {
		if c.ShouldReport {
			findings = append(findings, c)
		} else {
			filteredFindings = append(filteredFindings, c)
		}
	}
	lines = append(lines, ch.summaryFindings("Findings", findings)...)
	lines = append(lines, ch.summaryFindings("Filtered Findings", filteredFindings)...)

	return strings.Join(lines, "\n")
}

func (ch *Checker) summaryFindings(name string, checks []*filter.FilteredDiagnostic) []string {
	var lines []string
	lines = append(lines, "")
	lines = append(lines, fmt.Sprintf("%s (%d)", name, len(checks)))
	lines = append(lines, "")
	for i, c := range checks {
		if i >= maxFilteredFinding {
			lines = append(lines, "... (Too many findings. Dropped some findings)")
			break
		}
		lines = append(lines, githubutils.LinkedMarkdownDiagnostic(
			ch.req.Owner, ch.req.Repo, ch.req.SHA, c.Diagnostic))
	}
	lines = append(lines, "")
	return lines
}

func (ch *Checker) toCheckRunAnnotation(c *filter.FilteredDiagnostic) *github.CheckRunAnnotation {
	loc := c.Diagnostic.GetLocation()
	startLine := int(loc.GetRange().GetStart().GetLine())
	endLine := int(loc.GetRange().GetEnd().GetLine())
	if endLine == 0 {
		endLine = startLine
	}
	a := &github.CheckRunAnnotation{
		Path:            github.String(loc.GetPath()),
		StartLine:       github.Int(startLine),
		EndLine:         github.Int(endLine),
		AnnotationLevel: github.String(ch.annotationLevel(c.Diagnostic.Severity)),
		Message:         github.String(c.Diagnostic.GetMessage()),
	}
	// Annotations only support start_column and end_column on the same line.
	if startLine == endLine {
		if s, e := loc.GetRange().GetStart().GetColumn(), loc.GetRange().GetEnd().GetColumn(); s != 0 && e != 0 {
			a.StartColumn = github.Int(int(s))
			a.EndColumn = github.Int(int(e))
		}
	}
	toolName := c.Diagnostic.GetSource().GetName()
	if toolName == "" {
		toolName = ch.req.Name
	}
	if toolName != "" {
		line := fmt.Sprintf("L%d", startLine)
		if startLine < endLine {
			line += fmt.Sprintf("-L%d", endLine)
		}
		a.Title = github.String(fmt.Sprintf("[%s] %s#%s", toolName, loc.GetPath(), line))
	}
	if s := c.Diagnostic.GetOriginalOutput(); s != "" {
		a.RawDetails = github.String(s)
	}
	return a
}

func (ch *Checker) pullRequestDiff(ctx context.Context, pr int) ([]*diff.FileDiff, error) {
	d, err := ch.rawPullRequestDiff(ctx, pr)
	if err != nil {
		return nil, err
	}
	filediffs, err := diff.ParseMultiFile(bytes.NewReader(d))
	if err != nil {
		return nil, fmt.Errorf("fail to parse diff: %w", err)
	}
	return filediffs, nil
}

func (ch *Checker) rawPullRequestDiff(ctx context.Context, pr int) ([]byte, error) {
	d, err := ch.gh.GetPullRequestDiff(ctx, ch.req.Owner, ch.req.Repo, pr)
	if err != nil {
		return nil, err
	}
	return d, nil
}

func annotationsToDiagnostics(as []*doghouse.Annotation) []*rdf.Diagnostic {
	ds := make([]*rdf.Diagnostic, 0, len(as))
	for _, a := range as {
		ds = append(ds, annotationToDiagnostic(a))
	}
	return ds
}

func annotationToDiagnostic(a *doghouse.Annotation) *rdf.Diagnostic {
	if a.Diagnostic != nil {
		return a.Diagnostic
	}
	// Old reviwedog CLI doesn't have the Diagnostic field.
	return &rdf.Diagnostic{
		Location: &rdf.Location{
			Path: a.Path,
			Range: &rdf.Range{
				Start: &rdf.Position{
					Line: int32(a.Line),
				},
			},
		},
		Message:        a.Message,
		OriginalOutput: a.RawMessage,
	}
}

func min(x, y int) int {
	if x > y {
		return y
	}
	return x
}
<commit_msg>github-check: refactor building annotation title<commit_after>package server

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/google/go-github/v32/github"

	"github.com/reviewdog/reviewdog/diff"
	"github.com/reviewdog/reviewdog/doghouse"
	"github.com/reviewdog/reviewdog/filter"
	"github.com/reviewdog/reviewdog/proto/rdf"
	"github.com/reviewdog/reviewdog/service/github/githubutils"
)

// GitHub check runs API cannot handle too large requests.
// Set max number of filtered findings to be shown in check-run summary.
// ERROR:
//  https://api.github.com/repos/easymotion/vim-easymotion/check-runs: 422
//  Invalid request.
//  Only 65535 characters are allowed; 250684 were supplied. []
const maxFilteredFinding = 150

// > The Checks API limits the number of annotations to a maximum of 50 per API
// > request.
// https://developer.github.com/v3/checks/runs/#output-object
const maxAnnotationsPerRequest = 50

type Checker struct {
	req *doghouse.CheckRequest
	gh  checkerGitHubClientInterface
}

func NewChecker(req *doghouse.CheckRequest, gh *github.Client) *Checker {
	return &Checker{req: req, gh: &checkerGitHubClient{Client: gh}}
}

func (ch *Checker) Check(ctx context.Context) (*doghouse.CheckResponse, error) {
	var filediffs []*diff.FileDiff
	if ch.req.PullRequest != 0 {
		var err error
		filediffs, err = ch.pullRequestDiff(ctx, ch.req.PullRequest)
		if err != nil {
			return nil, fmt.Errorf("fail to parse diff: %w", err)
		}
	}

	results := annotationsToDiagnostics(ch.req.Annotations)
	filterMode := ch.req.FilterMode
	//lint:ignore SA1019 Need to support OutsideDiff for backward compatibility.
	if ch.req.PullRequest == 0 || ch.req.OutsideDiff {
		// If it's not Pull Request run, do not filter results by diff regardless
		// of the filter mode.
		filterMode = filter.ModeNoFilter
	}
	filtered := filter.FilterCheck(results, filediffs, 1, "", filterMode)
	check, err := ch.createCheck(ctx)
	if err != nil {
		// If this error is StatusForbidden (403) here, it means reviewdog is
		// running on GitHub Actions and has only read permission (because it's
		// running for Pull Requests from forked repository). If the token itself
		// is invalid, reviewdog should return an error earlier (e.g. when reading
		// Pull Requests diff), so it should be ok not to return error here and
		// return results instead.
		if err, ok := err.(*github.ErrorResponse); ok && err.Response.StatusCode == http.StatusForbidden {
			return &doghouse.CheckResponse{CheckedResults: filtered}, nil
		}
		return nil, fmt.Errorf("failed to create check: %w", err)
	}

	checkRun, conclusion, err := ch.postCheck(ctx, check.GetID(), filtered)
	if err != nil {
		return nil, fmt.Errorf("failed to post result: %w", err)
	}
	res := &doghouse.CheckResponse{
		ReportURL:  checkRun.GetHTMLURL(),
		Conclusion: conclusion,
	}
	return res, nil
}

func (ch *Checker) postCheck(ctx context.Context, checkID int64, checks []*filter.FilteredDiagnostic) (*github.CheckRun, string, error) {
	var annotations []*github.CheckRunAnnotation
	for _, c := range checks {
		if !c.ShouldReport {
			continue
		}
		annotations = append(annotations, ch.toCheckRunAnnotation(c))
	}
	if len(annotations) > 0 {
		if err := ch.postAnnotations(ctx, checkID, annotations); err != nil {
			return nil, "", fmt.Errorf("failed to post annotations: %w", err)
		}
	}

	conclusion := "success"
	if len(annotations) > 0 {
		conclusion = ch.conclusion()
	}
	opt := github.UpdateCheckRunOptions{
		Name:        ch.checkName(),
		Status:      github.String("completed"),
		Conclusion:  github.String(conclusion),
		CompletedAt: &github.Timestamp{Time: time.Now()},
		Output: &github.CheckRunOutput{
			Title:   github.String(ch.checkTitle()),
			Summary: github.String(ch.summary(checks)),
		},
	}
	checkRun, err := ch.gh.UpdateCheckRun(ctx, ch.req.Owner, ch.req.Repo, checkID, opt)
	if err != nil {
		return nil, "", err
	}
	return checkRun, conclusion, nil
}

func (ch *Checker) createCheck(ctx context.Context) (*github.CheckRun, error) {
	opt := github.CreateCheckRunOptions{
		Name:    ch.checkName(),
		HeadSHA: ch.req.SHA,
		Status:  github.String("in_progress"),
	}
	return ch.gh.CreateCheckRun(ctx, ch.req.Owner, ch.req.Repo, opt)
}

func (ch *Checker) postAnnotations(ctx context.Context, checkID int64, annotations []*github.CheckRunAnnotation) error {
	opt := github.UpdateCheckRunOptions{
		Name: ch.checkName(),
		Output: &github.CheckRunOutput{
			Title:       github.String(ch.checkTitle()),
			Summary:     github.String(""), // Post summary with the last request.
			Annotations: annotations[:min(maxAnnotationsPerRequest, len(annotations))],
		},
	}
	if _, err := ch.gh.UpdateCheckRun(ctx, ch.req.Owner, ch.req.Repo, checkID, opt); err != nil {
		return err
	}
	if len(annotations) > maxAnnotationsPerRequest {
		return ch.postAnnotations(ctx, checkID, annotations[maxAnnotationsPerRequest:])
	}
	return nil
}

func (ch *Checker) checkName() string {
	if ch.req.Name != "" {
		return ch.req.Name
	}
	return "reviewdog"
}

func (ch *Checker) checkTitle() string {
	if name := ch.checkName(); name != "reviewdog" {
		return fmt.Sprintf("reviewdog [%s] report", name)
	}
	return "reviewdog report"
}

// https://developer.github.com/v3/checks/runs/#parameters-1
func (ch *Checker) conclusion() string {
	switch strings.ToLower(ch.req.Level) {
	case "info", "warning":
		return "neutral"
	}
	return "failure"
}

// https://developer.github.com/v3/checks/runs/#annotations-object
func (ch *Checker) annotationLevel(s rdf.Severity) string {
	switch s {
	case rdf.Severity_ERROR:
		return "failure"
	case rdf.Severity_WARNING:
		return "warning"
	case rdf.Severity_INFO:
		return "notice"
	default:
		return ch.reqAnnotationLevel()
	}
}

func (ch *Checker) reqAnnotationLevel() string {
	switch strings.ToLower(ch.req.Level) {
	case "info":
		return "notice"
	case "warning":
		return "warning"
	case "failure":
		return "failure"
	}
	return "failure"
}

func (ch *Checker) summary(checks []*filter.FilteredDiagnostic) string {
	var lines []string
	lines = append(lines, "reported by [reviewdog](https://github.com/reviewdog/reviewdog) :dog:")

	var findings []*filter.FilteredDiagnostic
	var filteredFindings []*filter.FilteredDiagnostic
	for _, c := range checks {
		if c.ShouldReport {
			findings = append(findings, c)
		} else {
			filteredFindings = append(filteredFindings, c)
		}
	}
	lines = append(lines, ch.summaryFindings("Findings", findings)...)
	lines = append(lines, ch.summaryFindings("Filtered Findings", filteredFindings)...)

	return strings.Join(lines, "\n")
}

func (ch *Checker) summaryFindings(name string, checks []*filter.FilteredDiagnostic) []string {
	var lines []string
	lines = append(lines, "")
	lines = append(lines, fmt.Sprintf("%s (%d)", name, len(checks)))
	lines = append(lines, "")
	for i, c := range checks {
		if i >= maxFilteredFinding {
			lines = append(lines, "... (Too many findings. Dropped some findings)")
			break
		}
		lines = append(lines, githubutils.LinkedMarkdownDiagnostic(
			ch.req.Owner, ch.req.Repo, ch.req.SHA, c.Diagnostic))
	}
	lines = append(lines, "")
	return lines
}

func (ch *Checker) toCheckRunAnnotation(c *filter.FilteredDiagnostic) *github.CheckRunAnnotation {
	loc := c.Diagnostic.GetLocation()
	startLine := int(loc.GetRange().GetStart().GetLine())
	endLine := int(loc.GetRange().GetEnd().GetLine())
	if endLine == 0 {
		endLine = startLine
	}
	a := &github.CheckRunAnnotation{
		Path:            github.String(loc.GetPath()),
		StartLine:       github.Int(startLine),
		EndLine:         github.Int(endLine),
		AnnotationLevel: github.String(ch.annotationLevel(c.Diagnostic.Severity)),
		Message:         github.String(c.Diagnostic.GetMessage()),
		Title:           github.String(ch.buildTitle(c)),
	}
	// Annotations only support start_column and end_column on the same line.
	if startLine == endLine {
		if s, e := loc.GetRange().GetStart().GetColumn(), loc.GetRange().GetEnd().GetColumn(); s != 0 && e != 0 {
			a.StartColumn = github.Int(int(s))
			a.EndColumn = github.Int(int(e))
		}
	}
	if s := c.Diagnostic.GetOriginalOutput(); s != "" {
		a.RawDetails = github.String(s)
	}
	return a
}

func (ch *Checker) buildTitle(c *filter.FilteredDiagnostic) string {
	var sb strings.Builder
	toolName := c.Diagnostic.GetSource().GetName()
	if toolName == "" {
		toolName = ch.req.Name
	}
	if toolName != "" {
		sb.WriteString(fmt.Sprintf("[%s] ", toolName))
	}
	loc := c.Diagnostic.GetLocation()
	sb.WriteString(loc.GetPath())
	if startLine := int(loc.GetRange().GetStart().GetLine()); startLine > 0 {
		sb.WriteString(fmt.Sprintf("#L%d", startLine))
		if endLine := int(loc.GetRange().GetEnd().GetLine()); startLine < endLine {
			sb.WriteString(fmt.Sprintf("-L%d", endLine))
		}
	}
	return sb.String()
}

func (ch *Checker) pullRequestDiff(ctx context.Context, pr int) ([]*diff.FileDiff, error) {
	d, err := ch.rawPullRequestDiff(ctx, pr)
	if err != nil {
		return nil, err
	}
	filediffs, err := diff.ParseMultiFile(bytes.NewReader(d))
	if err != nil {
		return nil, fmt.Errorf("fail to parse diff: %w", err)
	}
	return filediffs, nil
}

func (ch *Checker) rawPullRequestDiff(ctx context.Context, pr int) ([]byte, error) {
	d, err := ch.gh.GetPullRequestDiff(ctx, ch.req.Owner, ch.req.Repo, pr)
	if err != nil {
		return nil, err
	}
	return d, nil
}

func annotationsToDiagnostics(as []*doghouse.Annotation) []*rdf.Diagnostic {
	ds := make([]*rdf.Diagnostic, 0, len(as))
	for _, a := range as {
		ds = append(ds, annotationToDiagnostic(a))
	}
	return ds
}

func annotationToDiagnostic(a *doghouse.Annotation) *rdf.Diagnostic {
	if a.Diagnostic != nil {
		return a.Diagnostic
	}
	// Old reviwedog CLI doesn't have the Diagnostic field.
	return &rdf.Diagnostic{
		Location: &rdf.Location{
			Path: a.Path,
			Range: &rdf.Range{
				Start: &rdf.Position{
					Line: int32(a.Line),
				},
			},
		},
		Message:        a.Message,
		OriginalOutput: a.RawMessage,
	}
}

func min(x, y int) int {
	if x > y {
		return y
	}
	return x
}
<|endoftext|>


<commit_before>package db

// OperationType is a numeric code indentifying the type of an Operation.
type OperationType int64

// Possible values for OperationType
//
// WARNING: The type codes are stored in the database, so this list of
//          definitions should be normally append-only. Any other change
//          requires a database update.
const (
	OperationUnknown OperationType = iota
	OperationClusterBootstrap
	OperationClusterJoin
	OperationBackupCreate
	OperationBackupRename
	OperationBackupRestore
	OperationBackupRemove
	OperationConsoleShow
	OperationInstanceCreate
	OperationInstanceUpdate
	OperationInstanceRename
	OperationInstanceMigrate
	OperationInstanceLiveMigrate
	OperationInstanceFreeze
	OperationInstanceUnfreeze
	OperationInstanceDelete
	OperationInstanceStart
	OperationInstanceStop
	OperationInstanceRestart
	OperationCommandExec
	OperationSnapshotCreate
	OperationSnapshotRename
	OperationSnapshotRestore
	OperationSnapshotTransfer
	OperationSnapshotUpdate
	OperationSnapshotDelete
	OperationImageDownload
	OperationImageDelete
	OperationImageToken
	OperationImageRefresh
	OperationVolumeCopy
	OperationVolumeCreate
	OperationVolumeMigrate
	OperationVolumeMove
	OperationVolumeSnapshotCreate
	OperationVolumeSnapshotDelete
	OperationVolumeSnapshotUpdate
	OperationProjectRename
	OperationImagesExpire
	OperationImagesPruneLeftover
	OperationImagesUpdate
	OperationImagesSynchronize
	OperationLogsExpire
	OperationInstanceTypesUpdate
	OperationBackupsExpire
	OperationSnapshotsExpire
	OperationCustomVolumeSnapshotsExpire
	OperationCustomVolumeBackupCreate
	OperationCustomVolumeBackupRemove
	OperationCustomVolumeBackupRename
	OperationCustomVolumeBackupRestore
	OperationWarningsPruneResolved
	OperationClusterJoinToken
	OperationVolumeSnapshotRename
	OperationClusterMemberEvacuate
	OperationClusterMemberRestore
)

// Description return a human-readable description of the operation type.
func (t OperationType) Description() string {
	switch t {
	case OperationClusterBootstrap:
		return "Creating bootstrap node"
	case OperationClusterJoin:
		return "Joining cluster"
	case OperationBackupCreate:
		return "Backing up instance"
	case OperationBackupRename:
		return "Renaming instance backup"
	case OperationBackupRestore:
		return "Restoring backup"
	case OperationBackupRemove:
		return "Removing instance backup"
	case OperationConsoleShow:
		return "Showing console"
	case OperationInstanceCreate:
		return "Creating instance"
	case OperationInstanceUpdate:
		return "Updating instance"
	case OperationInstanceRename:
		return "Renaming instance"
	case OperationInstanceMigrate:
		return "Migrating instance"
	case OperationInstanceLiveMigrate:
		return "Live-migrating instance"
	case OperationInstanceFreeze:
		return "Freezing instance"
	case OperationInstanceUnfreeze:
		return "Unfreezing instance"
	case OperationInstanceDelete:
		return "Deleting instance"
	case OperationInstanceStart:
		return "Starting instance"
	case OperationInstanceStop:
		return "Stopping instance"
	case OperationInstanceRestart:
		return "Restarting instance"
	case OperationCommandExec:
		return "Executing command"
	case OperationSnapshotCreate:
		return "Snapshotting instance"
	case OperationSnapshotRename:
		return "Renaming snapshot"
	case OperationSnapshotRestore:
		return "Restoring snapshot"
	case OperationSnapshotTransfer:
		return "Transferring snapshot"
	case OperationSnapshotUpdate:
		return "Updating snapshot"
	case OperationSnapshotDelete:
		return "Deleting snapshot"
	case OperationImageDownload:
		return "Downloading image"
	case OperationImageDelete:
		return "Deleting image"
	case OperationImageToken:
		return "Image download token"
	case OperationImageRefresh:
		return "Refreshing image"
	case OperationVolumeCopy:
		return "Copying storage volume"
	case OperationVolumeCreate:
		return "Creating storage volume"
	case OperationVolumeMigrate:
		return "Migrating storage volume"
	case OperationVolumeMove:
		return "Moving storage volume"
	case OperationVolumeSnapshotCreate:
		return "Creating storage volume snapshot"
	case OperationVolumeSnapshotDelete:
		return "Deleting storage volume snapshot"
	case OperationVolumeSnapshotUpdate:
		return "Updating storage volume snapshot"
	case OperationVolumeSnapshotRename:
		return "Renaming storage volume snapshot"
	case OperationProjectRename:
		return "Renaming project"
	case OperationImagesExpire:
		return "Cleaning up expired images"
	case OperationImagesPruneLeftover:
		return "Pruning leftover image files"
	case OperationImagesUpdate:
		return "Updating images"
	case OperationImagesSynchronize:
		return "Synchronizing images"
	case OperationLogsExpire:
		return "Expiring log files"
	case OperationInstanceTypesUpdate:
		return "Updating instance types"
	case OperationBackupsExpire:
		return "Cleaning up expired instance backups"
	case OperationSnapshotsExpire:
		return "Cleaning up expired instance snapshots"
	case OperationCustomVolumeSnapshotsExpire:
		return "Cleaning up expired volume snapshots"
	case OperationCustomVolumeBackupCreate:
		return "Creating custom volume backup"
	case OperationCustomVolumeBackupRemove:
		return "Deleting custom volume backup"
	case OperationCustomVolumeBackupRename:
		return "Renaming custom volume backup"
	case OperationCustomVolumeBackupRestore:
		return "Restoring custom volume backup"
	case OperationWarningsPruneResolved:
		return "Pruning resolved warnings"
	case OperationClusterMemberEvacuate:
		return "Evacuating cluster member"
	case OperationClusterMemberRestore:
		return "Restoring cluster member"
	default:
		return "Executing operation"
	}
}

// Permission returns the needed RBAC permission to cancel the oepration
func (t OperationType) Permission() string {
	switch t {
	case OperationBackupCreate:
		return "operate-containers"
	case OperationBackupRename:
		return "operate-containers"
	case OperationBackupRestore:
		return "operate-containers"
	case OperationBackupRemove:
		return "operate-containers"
	case OperationConsoleShow:
		return "operate-containers"
	case OperationInstanceFreeze:
		return "operate-containers"
	case OperationInstanceUnfreeze:
		return "operate-containers"
	case OperationInstanceStart:
		return "operate-containers"
	case OperationInstanceStop:
		return "operate-containers"
	case OperationInstanceRestart:
		return "operate-containers"
	case OperationCommandExec:
		return "operate-containers"
	case OperationSnapshotCreate:
		return "operate-containers"
	case OperationSnapshotRename:
		return "operate-containers"
	case OperationSnapshotTransfer:
		return "operate-containers"
	case OperationSnapshotUpdate:
		return "operate-containers"
	case OperationSnapshotDelete:
		return "operate-containers"

	case OperationInstanceCreate:
		return "manage-containers"
	case OperationInstanceUpdate:
		return "manage-containers"
	case OperationInstanceRename:
		return "manage-containers"
	case OperationInstanceMigrate:
		return "manage-containers"
	case OperationInstanceLiveMigrate:
		return "manage-containers"
	case OperationInstanceDelete:
		return "manage-containers"
	case OperationSnapshotRestore:
		return "manage-containers"

	case OperationImageDownload:
		return "manage-images"
	case OperationImageDelete:
		return "manage-images"
	case OperationImageToken:
		return "manage-images"
	case OperationImageRefresh:
		return "manage-images"
	case OperationImagesUpdate:
		return "manage-images"
	case OperationImagesSynchronize:
		return "manage-images"

	case OperationCustomVolumeSnapshotsExpire:
		return "operate-volumes"
	case OperationCustomVolumeBackupCreate:
		return "manage-storage-volumes"
	case OperationCustomVolumeBackupRemove:
		return "manage-storage-volumes"
	case OperationCustomVolumeBackupRename:
		return "manage-storage-volumes"
	case OperationCustomVolumeBackupRestore:
		return "manage-storage-volumes"
	}

	return ""
}
<commit_msg>lxd/db: Add OperationCertificateAddToken<commit_after>package db

// OperationType is a numeric code indentifying the type of an Operation.
type OperationType int64

// Possible values for OperationType
//
// WARNING: The type codes are stored in the database, so this list of
//          definitions should be normally append-only. Any other change
//          requires a database update.
const (
	OperationUnknown OperationType = iota
	OperationClusterBootstrap
	OperationClusterJoin
	OperationBackupCreate
	OperationBackupRename
	OperationBackupRestore
	OperationBackupRemove
	OperationConsoleShow
	OperationInstanceCreate
	OperationInstanceUpdate
	OperationInstanceRename
	OperationInstanceMigrate
	OperationInstanceLiveMigrate
	OperationInstanceFreeze
	OperationInstanceUnfreeze
	OperationInstanceDelete
	OperationInstanceStart
	OperationInstanceStop
	OperationInstanceRestart
	OperationCommandExec
	OperationSnapshotCreate
	OperationSnapshotRename
	OperationSnapshotRestore
	OperationSnapshotTransfer
	OperationSnapshotUpdate
	OperationSnapshotDelete
	OperationImageDownload
	OperationImageDelete
	OperationImageToken
	OperationImageRefresh
	OperationVolumeCopy
	OperationVolumeCreate
	OperationVolumeMigrate
	OperationVolumeMove
	OperationVolumeSnapshotCreate
	OperationVolumeSnapshotDelete
	OperationVolumeSnapshotUpdate
	OperationProjectRename
	OperationImagesExpire
	OperationImagesPruneLeftover
	OperationImagesUpdate
	OperationImagesSynchronize
	OperationLogsExpire
	OperationInstanceTypesUpdate
	OperationBackupsExpire
	OperationSnapshotsExpire
	OperationCustomVolumeSnapshotsExpire
	OperationCustomVolumeBackupCreate
	OperationCustomVolumeBackupRemove
	OperationCustomVolumeBackupRename
	OperationCustomVolumeBackupRestore
	OperationWarningsPruneResolved
	OperationClusterJoinToken
	OperationVolumeSnapshotRename
	OperationClusterMemberEvacuate
	OperationClusterMemberRestore
	OperationCertificateAddToken
)

// Description return a human-readable description of the operation type.
func (t OperationType) Description() string {
	switch t {
	case OperationClusterBootstrap:
		return "Creating bootstrap node"
	case OperationClusterJoin:
		return "Joining cluster"
	case OperationBackupCreate:
		return "Backing up instance"
	case OperationBackupRename:
		return "Renaming instance backup"
	case OperationBackupRestore:
		return "Restoring backup"
	case OperationBackupRemove:
		return "Removing instance backup"
	case OperationConsoleShow:
		return "Showing console"
	case OperationInstanceCreate:
		return "Creating instance"
	case OperationInstanceUpdate:
		return "Updating instance"
	case OperationInstanceRename:
		return "Renaming instance"
	case OperationInstanceMigrate:
		return "Migrating instance"
	case OperationInstanceLiveMigrate:
		return "Live-migrating instance"
	case OperationInstanceFreeze:
		return "Freezing instance"
	case OperationInstanceUnfreeze:
		return "Unfreezing instance"
	case OperationInstanceDelete:
		return "Deleting instance"
	case OperationInstanceStart:
		return "Starting instance"
	case OperationInstanceStop:
		return "Stopping instance"
	case OperationInstanceRestart:
		return "Restarting instance"
	case OperationCommandExec:
		return "Executing command"
	case OperationSnapshotCreate:
		return "Snapshotting instance"
	case OperationSnapshotRename:
		return "Renaming snapshot"
	case OperationSnapshotRestore:
		return "Restoring snapshot"
	case OperationSnapshotTransfer:
		return "Transferring snapshot"
	case OperationSnapshotUpdate:
		return "Updating snapshot"
	case OperationSnapshotDelete:
		return "Deleting snapshot"
	case OperationImageDownload:
		return "Downloading image"
	case OperationImageDelete:
		return "Deleting image"
	case OperationImageToken:
		return "Image download token"
	case OperationImageRefresh:
		return "Refreshing image"
	case OperationVolumeCopy:
		return "Copying storage volume"
	case OperationVolumeCreate:
		return "Creating storage volume"
	case OperationVolumeMigrate:
		return "Migrating storage volume"
	case OperationVolumeMove:
		return "Moving storage volume"
	case OperationVolumeSnapshotCreate:
		return "Creating storage volume snapshot"
	case OperationVolumeSnapshotDelete:
		return "Deleting storage volume snapshot"
	case OperationVolumeSnapshotUpdate:
		return "Updating storage volume snapshot"
	case OperationVolumeSnapshotRename:
		return "Renaming storage volume snapshot"
	case OperationProjectRename:
		return "Renaming project"
	case OperationImagesExpire:
		return "Cleaning up expired images"
	case OperationImagesPruneLeftover:
		return "Pruning leftover image files"
	case OperationImagesUpdate:
		return "Updating images"
	case OperationImagesSynchronize:
		return "Synchronizing images"
	case OperationLogsExpire:
		return "Expiring log files"
	case OperationInstanceTypesUpdate:
		return "Updating instance types"
	case OperationBackupsExpire:
		return "Cleaning up expired instance backups"
	case OperationSnapshotsExpire:
		return "Cleaning up expired instance snapshots"
	case OperationCustomVolumeSnapshotsExpire:
		return "Cleaning up expired volume snapshots"
	case OperationCustomVolumeBackupCreate:
		return "Creating custom volume backup"
	case OperationCustomVolumeBackupRemove:
		return "Deleting custom volume backup"
	case OperationCustomVolumeBackupRename:
		return "Renaming custom volume backup"
	case OperationCustomVolumeBackupRestore:
		return "Restoring custom volume backup"
	case OperationWarningsPruneResolved:
		return "Pruning resolved warnings"
	case OperationClusterMemberEvacuate:
		return "Evacuating cluster member"
	case OperationClusterMemberRestore:
		return "Restoring cluster member"
	default:
		return "Executing operation"
	}
}

// Permission returns the needed RBAC permission to cancel the oepration
func (t OperationType) Permission() string {
	switch t {
	case OperationBackupCreate:
		return "operate-containers"
	case OperationBackupRename:
		return "operate-containers"
	case OperationBackupRestore:
		return "operate-containers"
	case OperationBackupRemove:
		return "operate-containers"
	case OperationConsoleShow:
		return "operate-containers"
	case OperationInstanceFreeze:
		return "operate-containers"
	case OperationInstanceUnfreeze:
		return "operate-containers"
	case OperationInstanceStart:
		return "operate-containers"
	case OperationInstanceStop:
		return "operate-containers"
	case OperationInstanceRestart:
		return "operate-containers"
	case OperationCommandExec:
		return "operate-containers"
	case OperationSnapshotCreate:
		return "operate-containers"
	case OperationSnapshotRename:
		return "operate-containers"
	case OperationSnapshotTransfer:
		return "operate-containers"
	case OperationSnapshotUpdate:
		return "operate-containers"
	case OperationSnapshotDelete:
		return "operate-containers"

	case OperationInstanceCreate:
		return "manage-containers"
	case OperationInstanceUpdate:
		return "manage-containers"
	case OperationInstanceRename:
		return "manage-containers"
	case OperationInstanceMigrate:
		return "manage-containers"
	case OperationInstanceLiveMigrate:
		return "manage-containers"
	case OperationInstanceDelete:
		return "manage-containers"
	case OperationSnapshotRestore:
		return "manage-containers"

	case OperationImageDownload:
		return "manage-images"
	case OperationImageDelete:
		return "manage-images"
	case OperationImageToken:
		return "manage-images"
	case OperationImageRefresh:
		return "manage-images"
	case OperationImagesUpdate:
		return "manage-images"
	case OperationImagesSynchronize:
		return "manage-images"

	case OperationCustomVolumeSnapshotsExpire:
		return "operate-volumes"
	case OperationCustomVolumeBackupCreate:
		return "manage-storage-volumes"
	case OperationCustomVolumeBackupRemove:
		return "manage-storage-volumes"
	case OperationCustomVolumeBackupRename:
		return "manage-storage-volumes"
	case OperationCustomVolumeBackupRestore:
		return "manage-storage-volumes"
	}

	return ""
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"

	"github.com/lxc/lxd/lxd/cluster/request"
	"github.com/lxc/lxd/lxd/db"
	"github.com/lxc/lxd/lxd/revert"
	"github.com/lxc/lxd/lxd/state"
	storagePools "github.com/lxc/lxd/lxd/storage"
	"github.com/lxc/lxd/shared"
	"github.com/lxc/lxd/shared/api"
	"github.com/lxc/lxd/shared/logger"
)

// storagePoolDBCreate creates a storage pool DB entry and returns the created Pool ID.
func storagePoolDBCreate(s *state.State, poolName string, poolDescription string, driver string, config map[string]string) (int64, error) {
	// Check that the storage pool does not already exist.
	_, err := s.Cluster.GetStoragePoolID(poolName)
	if err == nil {
		return -1, fmt.Errorf("The storage pool already exists: %w", db.ErrAlreadyDefined)
	}

	// Make sure that we don't pass a nil to the next function.
	if config == nil {
		config = map[string]string{}
	}

	err = storagePoolValidate(s, poolName, driver, config)
	if err != nil {
		return -1, err
	}

	// Fill in the defaults.
	err = storagePoolFillDefault(poolName, driver, config)
	if err != nil {
		return -1, err
	}

	// Create the database entry for the storage pool.
	id, err := dbStoragePoolCreateAndUpdateCache(s, poolName, poolDescription, driver, config)
	if err != nil {
		return -1, fmt.Errorf("Error inserting %s into database: %w", poolName, err)
	}

	return id, nil
}

func storagePoolValidate(s *state.State, poolName string, driverName string, config map[string]string) error {
	poolType, err := storagePools.LoadByType(s, driverName)
	if err != nil {
		return err
	}

	// Check if the storage pool name is valid.
	err = poolType.ValidateName(poolName)
	if err != nil {
		return err
	}

	// Validate the requested storage pool configuration.
	err = poolType.Validate(config)
	if err != nil {
		return err
	}

	return nil
}

func storagePoolCreateGlobal(state *state.State, req api.StoragePoolsPost, clientType request.ClientType) error {
	// Create the database entry.
	id, err := storagePoolDBCreate(state, req.Name, req.Description, req.Driver, req.Config)
	if err != nil {
		return err
	}

	// Define a function which reverts everything.  Defer this function
	// so that it doesn't need to be explicitly called in every failing
	// return path. Track whether or not we want to undo the changes
	// using a closure.
	revert := revert.New()
	defer revert.Fail()

	revert.Add(func() { dbStoragePoolDeleteAndUpdateCache(state, req.Name) })

	_, err = storagePoolCreateLocal(state, id, req, clientType)
	if err != nil {
		return err
	}

	revert.Success()
	return nil
}

// This performs local pool setup and updates DB record if config was changed during pool setup.
// Returns resulting config.
func storagePoolCreateLocal(state *state.State, poolID int64, req api.StoragePoolsPost, clientType request.ClientType) (map[string]string, error) {
	// Setup revert.
	revert := revert.New()
	defer revert.Fail()

	// Make a copy of the req for later diff.
	var updatedReq api.StoragePoolsPost
	shared.DeepCopy(&req, &updatedReq)

	// Make sure that we don't pass a nil to the next function.
	if updatedReq.Config == nil {
		updatedReq.Config = map[string]string{}
	}

	// Fill in the node specific defaults.
	err := storagePoolFillDefault(updatedReq.Name, updatedReq.Driver, updatedReq.Config)
	if err != nil {
		return nil, err
	}

	configDiff, _ := storagePools.ConfigDiff(req.Config, updatedReq.Config)
	if len(configDiff) > 0 {
		// Update the database entry for the storage pool.
		err = state.Cluster.UpdateStoragePool(req.Name, req.Description, updatedReq.Config)
		if err != nil {
			return nil, fmt.Errorf("Error updating storage pool config after local fill defaults for %q: %w", req.Name, err)
		}
	}

	// Load pool record.
	pool, err := storagePools.LoadByName(state, updatedReq.Name)
	if err != nil {
		return nil, err
	}

	if pool.LocalStatus() == api.NetworkStatusCreated {
		logger.Debug("Skipping local storage pool create as already created", logger.Ctx{"pool": pool.Name()})

		return pool.Driver().Config(), nil
	}

	// Create the pool.
	err = pool.Create(clientType, nil)
	if err != nil {
		return nil, err
	}

	revert.Add(func() { pool.Delete(clientType, nil) })

	// Mount the pool.
	_, err = pool.Mount()
	if err != nil {
		return nil, err
	}

	// In case the storage pool config was changed during the pool creation, we need to update the database to
	// reflect this change. This can e.g. happen, when we create a loop file image. This means we append ".img"
	// to the path the user gave us and update the config in the storage callback. So diff the config here to
	// see if something like this has happened.
	configDiff, _ = storagePools.ConfigDiff(updatedReq.Config, pool.Driver().Config())
	if len(configDiff) > 0 {
		// Update the database entry for the storage pool.
		err = state.Cluster.UpdateStoragePool(req.Name, req.Description, pool.Driver().Config())
		if err != nil {
			return nil, fmt.Errorf("Error updating storage pool config after local create for %q: %w", req.Name, err)
		}
	}

	// Set storage pool node to storagePoolCreated.
	err = state.Cluster.Transaction(func(tx *db.ClusterTx) error {
		return tx.StoragePoolNodeCreated(poolID)
	})
	if err != nil {
		return nil, err
	}
	logger.Debug("Marked storage pool local status as created", logger.Ctx{"pool": req.Name})

	revert.Success()
	return pool.Driver().Config(), nil
}

// Helper around the low-level DB API, which also updates the driver names cache.
func dbStoragePoolCreateAndUpdateCache(s *state.State, poolName string, poolDescription string, poolDriver string, poolConfig map[string]string) (int64, error) {
	id, err := s.Cluster.CreateStoragePool(poolName, poolDescription, poolDriver, poolConfig)
	if err != nil {
		return id, err
	}

	// Update the storage drivers cache in api_1.0.go.
	storagePoolDriversCacheUpdate(s)

	return id, nil
}

// Helper around the low-level DB API, which also updates the driver names
// cache.
func dbStoragePoolDeleteAndUpdateCache(s *state.State, poolName string) error {
	_, err := s.Cluster.RemoveStoragePool(poolName)
	if err != nil {
		return err
	}

	// Update the storage drivers cache in api_1.0.go.
	storagePoolDriversCacheUpdate(s)

	return err
}
<commit_msg>lxd/storage/pools/utils: Removes call to storagePoolFillDefault in storagePoolDBCreate<commit_after>package main

import (
	"fmt"

	"github.com/lxc/lxd/lxd/cluster/request"
	"github.com/lxc/lxd/lxd/db"
	"github.com/lxc/lxd/lxd/revert"
	"github.com/lxc/lxd/lxd/state"
	storagePools "github.com/lxc/lxd/lxd/storage"
	"github.com/lxc/lxd/shared"
	"github.com/lxc/lxd/shared/api"
	"github.com/lxc/lxd/shared/logger"
)

// storagePoolDBCreate creates a storage pool DB entry and returns the created Pool ID.
func storagePoolDBCreate(s *state.State, poolName string, poolDescription string, driver string, config map[string]string) (int64, error) {
	// Check that the storage pool does not already exist.
	_, err := s.Cluster.GetStoragePoolID(poolName)
	if err == nil {
		return -1, fmt.Errorf("The storage pool already exists: %w", db.ErrAlreadyDefined)
	}

	// Make sure that we don't pass a nil to the next function.
	if config == nil {
		config = map[string]string{}
	}

	err = storagePoolValidate(s, poolName, driver, config)
	if err != nil {
		return -1, err
	}

	// Create the database entry for the storage pool.
	id, err := dbStoragePoolCreateAndUpdateCache(s, poolName, poolDescription, driver, config)
	if err != nil {
		return -1, fmt.Errorf("Error inserting %s into database: %w", poolName, err)
	}

	return id, nil
}

func storagePoolValidate(s *state.State, poolName string, driverName string, config map[string]string) error {
	poolType, err := storagePools.LoadByType(s, driverName)
	if err != nil {
		return err
	}

	// Check if the storage pool name is valid.
	err = poolType.ValidateName(poolName)
	if err != nil {
		return err
	}

	// Validate the requested storage pool configuration.
	err = poolType.Validate(config)
	if err != nil {
		return err
	}

	return nil
}

func storagePoolCreateGlobal(state *state.State, req api.StoragePoolsPost, clientType request.ClientType) error {
	// Create the database entry.
	id, err := storagePoolDBCreate(state, req.Name, req.Description, req.Driver, req.Config)
	if err != nil {
		return err
	}

	// Define a function which reverts everything.  Defer this function
	// so that it doesn't need to be explicitly called in every failing
	// return path. Track whether or not we want to undo the changes
	// using a closure.
	revert := revert.New()
	defer revert.Fail()

	revert.Add(func() { dbStoragePoolDeleteAndUpdateCache(state, req.Name) })

	_, err = storagePoolCreateLocal(state, id, req, clientType)
	if err != nil {
		return err
	}

	revert.Success()
	return nil
}

// This performs local pool setup and updates DB record if config was changed during pool setup.
// Returns resulting config.
func storagePoolCreateLocal(state *state.State, poolID int64, req api.StoragePoolsPost, clientType request.ClientType) (map[string]string, error) {
	// Setup revert.
	revert := revert.New()
	defer revert.Fail()

	// Make a copy of the req for later diff.
	var updatedReq api.StoragePoolsPost
	shared.DeepCopy(&req, &updatedReq)

	// Make sure that we don't pass a nil to the next function.
	if updatedReq.Config == nil {
		updatedReq.Config = map[string]string{}
	}

	// Fill in the node specific defaults.
	err := storagePoolFillDefault(updatedReq.Name, updatedReq.Driver, updatedReq.Config)
	if err != nil {
		return nil, err
	}

	configDiff, _ := storagePools.ConfigDiff(req.Config, updatedReq.Config)
	if len(configDiff) > 0 {
		// Update the database entry for the storage pool.
		err = state.Cluster.UpdateStoragePool(req.Name, req.Description, updatedReq.Config)
		if err != nil {
			return nil, fmt.Errorf("Error updating storage pool config after local fill defaults for %q: %w", req.Name, err)
		}
	}

	// Load pool record.
	pool, err := storagePools.LoadByName(state, updatedReq.Name)
	if err != nil {
		return nil, err
	}

	if pool.LocalStatus() == api.NetworkStatusCreated {
		logger.Debug("Skipping local storage pool create as already created", logger.Ctx{"pool": pool.Name()})

		return pool.Driver().Config(), nil
	}

	// Create the pool.
	err = pool.Create(clientType, nil)
	if err != nil {
		return nil, err
	}

	revert.Add(func() { pool.Delete(clientType, nil) })

	// Mount the pool.
	_, err = pool.Mount()
	if err != nil {
		return nil, err
	}

	// In case the storage pool config was changed during the pool creation, we need to update the database to
	// reflect this change. This can e.g. happen, when we create a loop file image. This means we append ".img"
	// to the path the user gave us and update the config in the storage callback. So diff the config here to
	// see if something like this has happened.
	configDiff, _ = storagePools.ConfigDiff(updatedReq.Config, pool.Driver().Config())
	if len(configDiff) > 0 {
		// Update the database entry for the storage pool.
		err = state.Cluster.UpdateStoragePool(req.Name, req.Description, pool.Driver().Config())
		if err != nil {
			return nil, fmt.Errorf("Error updating storage pool config after local create for %q: %w", req.Name, err)
		}
	}

	// Set storage pool node to storagePoolCreated.
	err = state.Cluster.Transaction(func(tx *db.ClusterTx) error {
		return tx.StoragePoolNodeCreated(poolID)
	})
	if err != nil {
		return nil, err
	}
	logger.Debug("Marked storage pool local status as created", logger.Ctx{"pool": req.Name})

	revert.Success()
	return pool.Driver().Config(), nil
}

// Helper around the low-level DB API, which also updates the driver names cache.
func dbStoragePoolCreateAndUpdateCache(s *state.State, poolName string, poolDescription string, poolDriver string, poolConfig map[string]string) (int64, error) {
	id, err := s.Cluster.CreateStoragePool(poolName, poolDescription, poolDriver, poolConfig)
	if err != nil {
		return id, err
	}

	// Update the storage drivers cache in api_1.0.go.
	storagePoolDriversCacheUpdate(s)

	return id, nil
}

// Helper around the low-level DB API, which also updates the driver names
// cache.
func dbStoragePoolDeleteAndUpdateCache(s *state.State, poolName string) error {
	_, err := s.Cluster.RemoveStoragePool(poolName)
	if err != nil {
		return err
	}

	// Update the storage drivers cache in api_1.0.go.
	storagePoolDriversCacheUpdate(s)

	return err
}
<|endoftext|>


<commit_before>package survey

import (
	"errors"
	"fmt"

	"github.com/AlecAivazis/survey/v2/core"
	"github.com/AlecAivazis/survey/v2/terminal"
)

/*
MultiSelect is a prompt that presents a list of various options to the user
for them to select using the arrow keys and enter. Response type is a slice of strings.

	days := []string{}
	prompt := &survey.MultiSelect{
		Message: "What days do you prefer:",
		Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
	}
	survey.AskOne(prompt, &days)
*/
type MultiSelect struct {
	Renderer
	Message       string
	Options       []string
	Default       interface{}
	Help          string
	PageSize      int
	VimMode       bool
	FilterMessage string
	Filter        func(filter string, value string, index int) bool
	filter        string
	selectedIndex int
	checked       map[int]bool
	showingHelp   bool
}

// data available to the templates when processing
type MultiSelectTemplateData struct {
	MultiSelect
	Answer        string
	ShowAnswer    bool
	Checked       map[int]bool
	SelectedIndex int
	ShowHelp      bool
	PageEntries   []core.OptionAnswer
	Config        *PromptConfig
}

var MultiSelectQuestionTemplate = `
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
{{- else }}
	{{- "  "}}{{- color "cyan"}}[Use arrows to move, space to select, <right> to all, <left> to none, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
  {{- "\n"}}
  {{- range $ix, $option := .PageEntries}}
    {{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
    {{- if index $.Checked $option.Index }}{{color $.Config.Icons.MarkedOption.Format }} {{ $.Config.Icons.MarkedOption.Text }} {{else}}{{color $.Config.Icons.UnmarkedOption.Format }} {{ $.Config.Icons.UnmarkedOption.Text }} {{end}}
    {{- color "reset"}}
    {{- " "}}{{$option.Value}}{{"\n"}}
  {{- end}}
{{- end}}`

// OnChange is called on every keypress.
func (m *MultiSelect) OnChange(key rune, config *PromptConfig) {
	options := m.filterOptions(config)
	oldFilter := m.filter

	if key == terminal.KeyArrowUp || (m.VimMode && key == 'k') {
		// if we are at the top of the list
		if m.selectedIndex == 0 {
			// go to the bottom
			m.selectedIndex = len(options) - 1
		} else {
			// decrement the selected index
			m.selectedIndex--
		}
	} else if key == terminal.KeyTab || key == terminal.KeyArrowDown || (m.VimMode && key == 'j') {
		// if we are at the bottom of the list
		if m.selectedIndex == len(options)-1 {
			// start at the top
			m.selectedIndex = 0
		} else {
			// increment the selected index
			m.selectedIndex++
		}
		// if the user pressed down and there is room to move
	} else if key == terminal.KeySpace {
		// the option they have selected
		if m.selectedIndex < len(options) {
			selectedOpt := options[m.selectedIndex]

			// if we haven't seen this index before
			if old, ok := m.checked[selectedOpt.Index]; !ok {
				// set the value to true
				m.checked[selectedOpt.Index] = true
			} else {
				// otherwise just invert the current value
				m.checked[selectedOpt.Index] = !old
			}
			if !config.KeepFilter {
				m.filter = ""
			}
		}
		// only show the help message if we have one to show
	} else if string(key) == config.HelpInput && m.Help != "" {
		m.showingHelp = true
	} else if key == terminal.KeyEscape {
		m.VimMode = !m.VimMode
	} else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
		m.filter = ""
	} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
		if m.filter != "" {
			m.filter = m.filter[0 : len(m.filter)-1]
		}
	} else if key >= terminal.KeySpace {
		m.filter += string(key)
		m.VimMode = false
	} else if key == terminal.KeyArrowRight {
		for _, v := range options {
			m.checked[v.Index] = true
		}
		if !config.KeepFilter {
			m.filter = ""
		}
	} else if key == terminal.KeyArrowLeft {
		for _, v := range options {
			m.checked[v.Index] = false
		}
		if !config.KeepFilter {
			m.filter = ""
		}
	}

	m.FilterMessage = ""
	if m.filter != "" {
		m.FilterMessage = " " + m.filter
	}
	if oldFilter != m.filter {
		// filter changed
		options = m.filterOptions(config)
		if len(options) > 0 && len(options) <= m.selectedIndex {
			m.selectedIndex = len(options) - 1
		}
	}
	// paginate the options
	// figure out the page size
	pageSize := m.PageSize
	// if we dont have a specific one
	if pageSize == 0 {
		// grab the global value
		pageSize = config.PageSize
	}

	// TODO if we have started filtering and were looking at the end of a list
	// and we have modified the filter then we should move the page back!
	opts, idx := paginate(pageSize, options, m.selectedIndex)

	// render the options
	m.Render(
		MultiSelectQuestionTemplate,
		MultiSelectTemplateData{
			MultiSelect:   *m,
			SelectedIndex: idx,
			Checked:       m.checked,
			ShowHelp:      m.showingHelp,
			PageEntries:   opts,
			Config:        config,
		},
	)
}

func (m *MultiSelect) filterOptions(config *PromptConfig) []core.OptionAnswer {
	// the filtered list
	answers := []core.OptionAnswer{}

	// if there is no filter applied
	if m.filter == "" {
		// return all of the options
		return core.OptionAnswerList(m.Options)
	}

	// the filter to apply
	filter := m.Filter
	if filter == nil {
		filter = config.Filter
	}

	// apply the filter to each option
	for i, opt := range m.Options {
		// i the filter says to include the option
		if filter(m.filter, opt, i) {
			answers = append(answers, core.OptionAnswer{
				Index: i,
				Value: opt,
			})
		}
	}

	// we're done here
	return answers
}

func (m *MultiSelect) Prompt(config *PromptConfig) (interface{}, error) {
	// compute the default state
	m.checked = make(map[int]bool)
	// if there is a default
	if m.Default != nil {
		// if the default is string values
		if defaultValues, ok := m.Default.([]string); ok {
			for _, dflt := range defaultValues {
				for i, opt := range m.Options {
					// if the option corresponds to the default
					if opt == dflt {
						// we found our initial value
						m.checked[i] = true
						// stop looking
						break
					}
				}
			}
			// if the default value is index values
		} else if defaultIndices, ok := m.Default.([]int); ok {
			// go over every index we need to enable by default
			for _, idx := range defaultIndices {
				// and enable it
				m.checked[idx] = true
			}
		}
	}

	// if there are no options to render
	if len(m.Options) == 0 {
		// we failed
		return "", errors.New("please provide options to select from")
	}

	// figure out the page size
	pageSize := m.PageSize
	// if we dont have a specific one
	if pageSize == 0 {
		// grab the global value
		pageSize = config.PageSize
	}
	// paginate the options
	// build up a list of option answers
	opts, idx := paginate(pageSize, core.OptionAnswerList(m.Options), m.selectedIndex)

	cursor := m.NewCursor()
	cursor.Hide()       // hide the cursor
	defer cursor.Show() // show the cursor when we're done

	// ask the question
	err := m.Render(
		MultiSelectQuestionTemplate,
		MultiSelectTemplateData{
			MultiSelect:   *m,
			SelectedIndex: idx,
			Checked:       m.checked,
			PageEntries:   opts,
			Config:        config,
		},
	)
	if err != nil {
		return "", err
	}

	rr := m.NewRuneReader()
	rr.SetTermMode()
	defer rr.RestoreTermMode()

	// start waiting for input
	for {
		r, _, _ := rr.ReadRune()
		if r == '\r' || r == '\n' {
			break
		}
		if r == terminal.KeyInterrupt {
			return "", terminal.InterruptErr
		}
		if r == terminal.KeyEndTransmission {
			break
		}
		m.OnChange(r, config)
	}
	m.filter = ""
	m.FilterMessage = ""

	answers := []core.OptionAnswer{}
	for i, option := range m.Options {
		if val, ok := m.checked[i]; ok && val {
			answers = append(answers, core.OptionAnswer{Value: option, Index: i})
		}
	}

	return answers, nil
}

// Cleanup removes the options section, and renders the ask like a normal question.
func (m *MultiSelect) Cleanup(config *PromptConfig, val interface{}) error {
	// the answer to show
	answer := ""
	for _, ans := range val.([]core.OptionAnswer) {
		answer = fmt.Sprintf("%s, %s", answer, ans.Value)
	}

	// if we answered anything
	if len(answer) > 2 {
		// remove the precending commas
		answer = answer[2:]
	}

	// execute the output summary template with the answer
	return m.Render(
		MultiSelectQuestionTemplate,
		MultiSelectTemplateData{
			MultiSelect:   *m,
			SelectedIndex: m.selectedIndex,
			Checked:       m.checked,
			Answer:        answer,
			ShowAnswer:    true,
			Config:        config,
		},
	)
}
<commit_msg>Error handling for inputs (#316)<commit_after>package survey

import (
	"errors"
	"fmt"

	"github.com/AlecAivazis/survey/v2/core"
	"github.com/AlecAivazis/survey/v2/terminal"
)

/*
MultiSelect is a prompt that presents a list of various options to the user
for them to select using the arrow keys and enter. Response type is a slice of strings.

	days := []string{}
	prompt := &survey.MultiSelect{
		Message: "What days do you prefer:",
		Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
	}
	survey.AskOne(prompt, &days)
*/
type MultiSelect struct {
	Renderer
	Message       string
	Options       []string
	Default       interface{}
	Help          string
	PageSize      int
	VimMode       bool
	FilterMessage string
	Filter        func(filter string, value string, index int) bool
	filter        string
	selectedIndex int
	checked       map[int]bool
	showingHelp   bool
}

// data available to the templates when processing
type MultiSelectTemplateData struct {
	MultiSelect
	Answer        string
	ShowAnswer    bool
	Checked       map[int]bool
	SelectedIndex int
	ShowHelp      bool
	PageEntries   []core.OptionAnswer
	Config        *PromptConfig
}

var MultiSelectQuestionTemplate = `
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
{{- else }}
	{{- "  "}}{{- color "cyan"}}[Use arrows to move, space to select, <right> to all, <left> to none, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
  {{- "\n"}}
  {{- range $ix, $option := .PageEntries}}
    {{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
    {{- if index $.Checked $option.Index }}{{color $.Config.Icons.MarkedOption.Format }} {{ $.Config.Icons.MarkedOption.Text }} {{else}}{{color $.Config.Icons.UnmarkedOption.Format }} {{ $.Config.Icons.UnmarkedOption.Text }} {{end}}
    {{- color "reset"}}
    {{- " "}}{{$option.Value}}{{"\n"}}
  {{- end}}
{{- end}}`

// OnChange is called on every keypress.
func (m *MultiSelect) OnChange(key rune, config *PromptConfig) {
	options := m.filterOptions(config)
	oldFilter := m.filter

	if key == terminal.KeyArrowUp || (m.VimMode && key == 'k') {
		// if we are at the top of the list
		if m.selectedIndex == 0 {
			// go to the bottom
			m.selectedIndex = len(options) - 1
		} else {
			// decrement the selected index
			m.selectedIndex--
		}
	} else if key == terminal.KeyTab || key == terminal.KeyArrowDown || (m.VimMode && key == 'j') {
		// if we are at the bottom of the list
		if m.selectedIndex == len(options)-1 {
			// start at the top
			m.selectedIndex = 0
		} else {
			// increment the selected index
			m.selectedIndex++
		}
		// if the user pressed down and there is room to move
	} else if key == terminal.KeySpace {
		// the option they have selected
		if m.selectedIndex < len(options) {
			selectedOpt := options[m.selectedIndex]

			// if we haven't seen this index before
			if old, ok := m.checked[selectedOpt.Index]; !ok {
				// set the value to true
				m.checked[selectedOpt.Index] = true
			} else {
				// otherwise just invert the current value
				m.checked[selectedOpt.Index] = !old
			}
			if !config.KeepFilter {
				m.filter = ""
			}
		}
		// only show the help message if we have one to show
	} else if string(key) == config.HelpInput && m.Help != "" {
		m.showingHelp = true
	} else if key == terminal.KeyEscape {
		m.VimMode = !m.VimMode
	} else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
		m.filter = ""
	} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
		if m.filter != "" {
			m.filter = m.filter[0 : len(m.filter)-1]
		}
	} else if key >= terminal.KeySpace {
		m.filter += string(key)
		m.VimMode = false
	} else if key == terminal.KeyArrowRight {
		for _, v := range options {
			m.checked[v.Index] = true
		}
		if !config.KeepFilter {
			m.filter = ""
		}
	} else if key == terminal.KeyArrowLeft {
		for _, v := range options {
			m.checked[v.Index] = false
		}
		if !config.KeepFilter {
			m.filter = ""
		}
	}

	m.FilterMessage = ""
	if m.filter != "" {
		m.FilterMessage = " " + m.filter
	}
	if oldFilter != m.filter {
		// filter changed
		options = m.filterOptions(config)
		if len(options) > 0 && len(options) <= m.selectedIndex {
			m.selectedIndex = len(options) - 1
		}
	}
	// paginate the options
	// figure out the page size
	pageSize := m.PageSize
	// if we dont have a specific one
	if pageSize == 0 {
		// grab the global value
		pageSize = config.PageSize
	}

	// TODO if we have started filtering and were looking at the end of a list
	// and we have modified the filter then we should move the page back!
	opts, idx := paginate(pageSize, options, m.selectedIndex)

	// render the options
	m.Render(
		MultiSelectQuestionTemplate,
		MultiSelectTemplateData{
			MultiSelect:   *m,
			SelectedIndex: idx,
			Checked:       m.checked,
			ShowHelp:      m.showingHelp,
			PageEntries:   opts,
			Config:        config,
		},
	)
}

func (m *MultiSelect) filterOptions(config *PromptConfig) []core.OptionAnswer {
	// the filtered list
	answers := []core.OptionAnswer{}

	// if there is no filter applied
	if m.filter == "" {
		// return all of the options
		return core.OptionAnswerList(m.Options)
	}

	// the filter to apply
	filter := m.Filter
	if filter == nil {
		filter = config.Filter
	}

	// apply the filter to each option
	for i, opt := range m.Options {
		// i the filter says to include the option
		if filter(m.filter, opt, i) {
			answers = append(answers, core.OptionAnswer{
				Index: i,
				Value: opt,
			})
		}
	}

	// we're done here
	return answers
}

func (m *MultiSelect) Prompt(config *PromptConfig) (interface{}, error) {
	// compute the default state
	m.checked = make(map[int]bool)
	// if there is a default
	if m.Default != nil {
		// if the default is string values
		if defaultValues, ok := m.Default.([]string); ok {
			for _, dflt := range defaultValues {
				for i, opt := range m.Options {
					// if the option corresponds to the default
					if opt == dflt {
						// we found our initial value
						m.checked[i] = true
						// stop looking
						break
					}
				}
			}
			// if the default value is index values
		} else if defaultIndices, ok := m.Default.([]int); ok {
			// go over every index we need to enable by default
			for _, idx := range defaultIndices {
				// and enable it
				m.checked[idx] = true
			}
		}
	}

	// if there are no options to render
	if len(m.Options) == 0 {
		// we failed
		return "", errors.New("please provide options to select from")
	}

	// figure out the page size
	pageSize := m.PageSize
	// if we dont have a specific one
	if pageSize == 0 {
		// grab the global value
		pageSize = config.PageSize
	}
	// paginate the options
	// build up a list of option answers
	opts, idx := paginate(pageSize, core.OptionAnswerList(m.Options), m.selectedIndex)

	cursor := m.NewCursor()
	cursor.Hide()       // hide the cursor
	defer cursor.Show() // show the cursor when we're done

	// ask the question
	err := m.Render(
		MultiSelectQuestionTemplate,
		MultiSelectTemplateData{
			MultiSelect:   *m,
			SelectedIndex: idx,
			Checked:       m.checked,
			PageEntries:   opts,
			Config:        config,
		},
	)
	if err != nil {
		return "", err
	}

	rr := m.NewRuneReader()
	rr.SetTermMode()
	defer rr.RestoreTermMode()

	// start waiting for input
	for {
		r, _, err := rr.ReadRune()
		if err != nil {
			return "", err
		}
		if r == '\r' || r == '\n' {
			break
		}
		if r == terminal.KeyInterrupt {
			return "", terminal.InterruptErr
		}
		if r == terminal.KeyEndTransmission {
			break
		}
		m.OnChange(r, config)
	}
	m.filter = ""
	m.FilterMessage = ""

	answers := []core.OptionAnswer{}
	for i, option := range m.Options {
		if val, ok := m.checked[i]; ok && val {
			answers = append(answers, core.OptionAnswer{Value: option, Index: i})
		}
	}

	return answers, nil
}

// Cleanup removes the options section, and renders the ask like a normal question.
func (m *MultiSelect) Cleanup(config *PromptConfig, val interface{}) error {
	// the answer to show
	answer := ""
	for _, ans := range val.([]core.OptionAnswer) {
		answer = fmt.Sprintf("%s, %s", answer, ans.Value)
	}

	// if we answered anything
	if len(answer) > 2 {
		// remove the precending commas
		answer = answer[2:]
	}

	// execute the output summary template with the answer
	return m.Render(
		MultiSelectQuestionTemplate,
		MultiSelectTemplateData{
			MultiSelect:   *m,
			SelectedIndex: m.selectedIndex,
			Checked:       m.checked,
			Answer:        answer,
			ShowAnswer:    true,
			Config:        config,
		},
	)
}
<|endoftext|>