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

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"sync"

	"github.com/ceph/ceph-csi/internal/util"

	"k8s.io/klog"
)

const (
	volumeMounterFuse   = "fuse"
	volumeMounterKernel = "kernel"
	netDev              = "_netdev"
)

var (
	availableMounters []string

	// maps a mountpoint to PID of its FUSE daemon
	fusePidMap    = make(map[string]int)
	fusePidMapMtx sync.Mutex

	fusePidRx = regexp.MustCompile(`(?m)^ceph-fuse\[(.+)\]: starting fuse$`)
)

// Version checking the running kernel and comparing it to known versions that
// have support for quota. Distributors of enterprise Linux have backported
// quota support to previous versions. This function checks if the running
// kernel is one of the versions that have the feature/fixes backported.
//
// `uname -r` (or Uname().Utsname.Release has a format like 1.2.3-rc.vendor
// This can be slit up in the following components:
// - version (1)
// - patchlevel (2)
// - sublevel (3) - optional, defaults to 0
// - extraversion (rc) - optional, matching integers only
// - distribution (.vendor) - optional, match against whole `uname -r` string
//
// For matching multiple versions, the kernelSupport type contains a backport
// bool, which will cause matching
// version+patchlevel+sublevel+(>=extraversion)+(~distribution)
//
// In case the backport bool is false, a simple check for higher versions than
// version+patchlevel+sublevel is done.
func kernelSupportsQuota(release string) bool {
	type kernelSupport struct {
		version      int
		patchlevel   int
		sublevel     int
		extraversion int    // prefix of the part after the first "-"
		distribution string // component of full extraversion
		backport     bool   // backports have a fixed version/patchlevel/sublevel
	}

	quotaSupport := []kernelSupport{
		{4, 17, 0, 0, "", false},       // standard 4.17+ versions
		{3, 10, 0, 1062, ".el7", true}, // RHEL-7.7
	}

	vers := strings.Split(strings.SplitN(release, "-", 2)[0], ".")
	version, err := strconv.Atoi(vers[0])
	if err != nil {
		klog.Errorf("failed to parse version from %s: %v", release, err)
		return false
	}
	patchlevel, err := strconv.Atoi(vers[1])
	if err != nil {
		klog.Errorf("failed to parse patchlevel from %s: %v", release, err)
		return false
	}
	sublevel := 0
	if len(vers) >= 3 {
		sublevel, err = strconv.Atoi(vers[2])
		if err != nil {
			klog.Errorf("failed to parse sublevel from %s: %v", release, err)
			return false
		}
	}
	extra := strings.SplitN(release, "-", 2)
	extraversion := 0
	if len(extra) == 2 {
		// ignore errors, 1st component of extraversion does not need to be an int
		extraversion, err = strconv.Atoi(strings.Split(extra[1], ".")[0])
		if err != nil {
			// "go lint" wants err to be checked...
			extraversion = 0
		}
	}

	// compare running kernel against known versions
	for _, kernel := range quotaSupport {
		if !kernel.backport {
			// deal with the default case(s), find >= match for version, patchlevel, sublevel
			if version > kernel.version || (version == kernel.version && patchlevel > kernel.patchlevel) ||
				(version == kernel.version && patchlevel == kernel.patchlevel && sublevel >= kernel.sublevel) {
				return true
			}
		} else {
			// specific backport, match distribution initially
			if !strings.Contains(release, kernel.distribution) {
				continue
			}

			// strict match version, patchlevel, sublevel, and >= match extraversion
			if version == kernel.version && patchlevel == kernel.patchlevel &&
				sublevel == kernel.sublevel && extraversion >= kernel.extraversion {
				return true
			}
		}
	}
	klog.Errorf("kernel %s does not support quota", release)
	return false
}

// Load available ceph mounters installed on system into availableMounters
// Called from driver.go's Run()
func loadAvailableMounters(conf *util.Config) error {
	// #nosec
	fuseMounterProbe := exec.Command("ceph-fuse", "--version")
	// #nosec
	kernelMounterProbe := exec.Command("mount.ceph")

	err := kernelMounterProbe.Run()
	if err != nil {
		klog.Errorf("failed to run mount.ceph %v", err)
	} else {
		// fetch the current running kernel info
		release, kvErr := util.KernelVersion()
		if kvErr != nil {
			return kvErr
		}

		if conf.ForceKernelCephFS || kernelSupportsQuota(release) {
			klog.V(1).Infof("loaded mounter: %s", volumeMounterKernel)
			availableMounters = append(availableMounters, volumeMounterKernel)
		} else {
			klog.V(1).Infof("kernel version < 4.17 might not support quota feature, hence not loading kernel client")
		}
	}

	err = fuseMounterProbe.Run()
	if err != nil {
		klog.Errorf("failed to run ceph-fuse %v", err)
	} else {
		klog.V(1).Infof("loaded mounter: %s", volumeMounterFuse)
		availableMounters = append(availableMounters, volumeMounterFuse)
	}

	if len(availableMounters) == 0 {
		return errors.New("no ceph mounters found on system")
	}

	return nil
}

type volumeMounter interface {
	mount(ctx context.Context, mountPoint string, cr *util.Credentials, volOptions *volumeOptions) error
	name() string
}

func newMounter(volOptions *volumeOptions) (volumeMounter, error) {
	// Get the mounter from the configuration

	wantMounter := volOptions.Mounter

	// Verify that it's available

	var chosenMounter string

	for _, availMounter := range availableMounters {
		if availMounter == wantMounter {
			chosenMounter = wantMounter
			break
		}
	}

	if chosenMounter == "" {
		// Otherwise pick whatever is left
		chosenMounter = availableMounters[0]
		klog.V(4).Infof("requested mounter: %s, chosen mounter: %s", wantMounter, chosenMounter)
	}

	// Create the mounter

	switch chosenMounter {
	case volumeMounterFuse:
		return &fuseMounter{}, nil
	case volumeMounterKernel:
		return &kernelMounter{}, nil
	}

	return nil, fmt.Errorf("unknown mounter '%s'", chosenMounter)
}

type fuseMounter struct{}

func mountFuse(ctx context.Context, mountPoint string, cr *util.Credentials, volOptions *volumeOptions) error {
	args := []string{
		mountPoint,
		"-m", volOptions.Monitors,
		"-c", util.CephConfigPath,
		"-n", cephEntityClientPrefix + cr.ID, "--keyfile=" + cr.KeyFile,
		"-r", volOptions.RootPath,
		"-o", "nonempty",
	}

	if volOptions.FuseMountOptions != "" {
		args = append(args, ","+volOptions.FuseMountOptions)
	}

	if volOptions.FsName != "" {
		args = append(args, "--client_mds_namespace="+volOptions.FsName)
	}

	_, stderr, err := execCommand(ctx, "ceph-fuse", args[:]...)
	if err != nil {
		return err
	}

	// Parse the output:
	// We need "starting fuse" meaning the mount is ok
	// and PID of the ceph-fuse daemon for unmount

	match := fusePidRx.FindSubmatch(stderr)
	if len(match) != 2 {
		return fmt.Errorf("ceph-fuse failed: %s", stderr)
	}

	pid, err := strconv.Atoi(string(match[1]))
	if err != nil {
		return fmt.Errorf("failed to parse FUSE daemon PID: %v", err)
	}

	fusePidMapMtx.Lock()
	fusePidMap[mountPoint] = pid
	fusePidMapMtx.Unlock()

	return nil
}

func (m *fuseMounter) mount(ctx context.Context, mountPoint string, cr *util.Credentials, volOptions *volumeOptions) error {
	if err := util.CreateMountPoint(mountPoint); err != nil {
		return err
	}

	return mountFuse(ctx, mountPoint, cr, volOptions)
}

func (m *fuseMounter) name() string { return "Ceph FUSE driver" }

type kernelMounter struct{}

func mountKernel(ctx context.Context, mountPoint string, cr *util.Credentials, volOptions *volumeOptions) error {
	if err := execCommandErr(ctx, "modprobe", "ceph"); err != nil {
		return err
	}

	args := []string{
		"-t", "ceph",
		fmt.Sprintf("%s:%s", volOptions.Monitors, volOptions.RootPath),
		mountPoint,
	}
	optionsStr := fmt.Sprintf("name=%s,secretfile=%s", cr.ID, cr.KeyFile)
	if volOptions.FsName != "" {
		optionsStr += fmt.Sprintf(",mds_namespace=%s", volOptions.FsName)
	}
	if volOptions.KernelMountOptions != "" {
		optionsStr += fmt.Sprintf(",%s", volOptions.KernelMountOptions)
	}

	if !strings.Contains(volOptions.KernelMountOptions, netDev) {
		optionsStr += fmt.Sprintf(",%s", netDev)
	}

	args = append(args, "-o", optionsStr)

	return execCommandErr(ctx, "mount", args[:]...)
}

func (m *kernelMounter) mount(ctx context.Context, mountPoint string, cr *util.Credentials, volOptions *volumeOptions) error {
	if err := util.CreateMountPoint(mountPoint); err != nil {
		return err
	}

	return mountKernel(ctx, mountPoint, cr, volOptions)
}

func (m *kernelMounter) name() string { return "Ceph kernel client" }

func bindMount(ctx context.Context, from, to string, readOnly bool, mntOptions []string) error {
	mntOptionSli := strings.Join(mntOptions, ",")
	if err := execCommandErr(ctx, "mount", "-o", mntOptionSli, from, to); err != nil {
		return fmt.Errorf("failed to bind-mount %s to %s: %v", from, to, err)
	}

	if readOnly {
		mntOptionSli += ",remount"
		if err := execCommandErr(ctx, "mount", "-o", mntOptionSli, to); err != nil {
			return fmt.Errorf("failed read-only remount of %s: %v", to, err)
		}
	}

	return nil
}

func unmountVolume(ctx context.Context, mountPoint string) error {
	if err := execCommandErr(ctx, "umount", mountPoint); err != nil {
		if strings.Contains(err.Error(), fmt.Sprintf("exit status 32: umount: %s: not mounted", mountPoint)) ||
			strings.Contains(err.Error(), "No such file or directory") {
			return nil
		}
		return err
	}

	fusePidMapMtx.Lock()
	pid, ok := fusePidMap[mountPoint]
	if ok {
		delete(fusePidMap, mountPoint)
	}
	fusePidMapMtx.Unlock()

	if ok {
		p, err := os.FindProcess(pid)
		if err != nil {
			klog.Warningf(util.Log(ctx, "failed to find process %d: %v"), pid, err)
		} else {
			if _, err = p.Wait(); err != nil {
				klog.Warningf(util.Log(ctx, "%d is not a child process: %v"), pid, err)
			}
		}
	}

	return nil
}
<commit_msg>cleanup: use util.MountOptionsAdd() in CephFS volumemounter<commit_after>/*
Copyright 2018 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 cephfs

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"sync"

	"github.com/ceph/ceph-csi/internal/util"

	"k8s.io/klog"
)

const (
	volumeMounterFuse   = "fuse"
	volumeMounterKernel = "kernel"
	netDev              = "_netdev"
)

var (
	availableMounters []string

	// maps a mountpoint to PID of its FUSE daemon
	fusePidMap    = make(map[string]int)
	fusePidMapMtx sync.Mutex

	fusePidRx = regexp.MustCompile(`(?m)^ceph-fuse\[(.+)\]: starting fuse$`)
)

// Version checking the running kernel and comparing it to known versions that
// have support for quota. Distributors of enterprise Linux have backported
// quota support to previous versions. This function checks if the running
// kernel is one of the versions that have the feature/fixes backported.
//
// `uname -r` (or Uname().Utsname.Release has a format like 1.2.3-rc.vendor
// This can be slit up in the following components:
// - version (1)
// - patchlevel (2)
// - sublevel (3) - optional, defaults to 0
// - extraversion (rc) - optional, matching integers only
// - distribution (.vendor) - optional, match against whole `uname -r` string
//
// For matching multiple versions, the kernelSupport type contains a backport
// bool, which will cause matching
// version+patchlevel+sublevel+(>=extraversion)+(~distribution)
//
// In case the backport bool is false, a simple check for higher versions than
// version+patchlevel+sublevel is done.
func kernelSupportsQuota(release string) bool {
	type kernelSupport struct {
		version      int
		patchlevel   int
		sublevel     int
		extraversion int    // prefix of the part after the first "-"
		distribution string // component of full extraversion
		backport     bool   // backports have a fixed version/patchlevel/sublevel
	}

	quotaSupport := []kernelSupport{
		{4, 17, 0, 0, "", false},       // standard 4.17+ versions
		{3, 10, 0, 1062, ".el7", true}, // RHEL-7.7
	}

	vers := strings.Split(strings.SplitN(release, "-", 2)[0], ".")
	version, err := strconv.Atoi(vers[0])
	if err != nil {
		klog.Errorf("failed to parse version from %s: %v", release, err)
		return false
	}
	patchlevel, err := strconv.Atoi(vers[1])
	if err != nil {
		klog.Errorf("failed to parse patchlevel from %s: %v", release, err)
		return false
	}
	sublevel := 0
	if len(vers) >= 3 {
		sublevel, err = strconv.Atoi(vers[2])
		if err != nil {
			klog.Errorf("failed to parse sublevel from %s: %v", release, err)
			return false
		}
	}
	extra := strings.SplitN(release, "-", 2)
	extraversion := 0
	if len(extra) == 2 {
		// ignore errors, 1st component of extraversion does not need to be an int
		extraversion, err = strconv.Atoi(strings.Split(extra[1], ".")[0])
		if err != nil {
			// "go lint" wants err to be checked...
			extraversion = 0
		}
	}

	// compare running kernel against known versions
	for _, kernel := range quotaSupport {
		if !kernel.backport {
			// deal with the default case(s), find >= match for version, patchlevel, sublevel
			if version > kernel.version || (version == kernel.version && patchlevel > kernel.patchlevel) ||
				(version == kernel.version && patchlevel == kernel.patchlevel && sublevel >= kernel.sublevel) {
				return true
			}
		} else {
			// specific backport, match distribution initially
			if !strings.Contains(release, kernel.distribution) {
				continue
			}

			// strict match version, patchlevel, sublevel, and >= match extraversion
			if version == kernel.version && patchlevel == kernel.patchlevel &&
				sublevel == kernel.sublevel && extraversion >= kernel.extraversion {
				return true
			}
		}
	}
	klog.Errorf("kernel %s does not support quota", release)
	return false
}

// Load available ceph mounters installed on system into availableMounters
// Called from driver.go's Run()
func loadAvailableMounters(conf *util.Config) error {
	// #nosec
	fuseMounterProbe := exec.Command("ceph-fuse", "--version")
	// #nosec
	kernelMounterProbe := exec.Command("mount.ceph")

	err := kernelMounterProbe.Run()
	if err != nil {
		klog.Errorf("failed to run mount.ceph %v", err)
	} else {
		// fetch the current running kernel info
		release, kvErr := util.KernelVersion()
		if kvErr != nil {
			return kvErr
		}

		if conf.ForceKernelCephFS || kernelSupportsQuota(release) {
			klog.V(1).Infof("loaded mounter: %s", volumeMounterKernel)
			availableMounters = append(availableMounters, volumeMounterKernel)
		} else {
			klog.V(1).Infof("kernel version < 4.17 might not support quota feature, hence not loading kernel client")
		}
	}

	err = fuseMounterProbe.Run()
	if err != nil {
		klog.Errorf("failed to run ceph-fuse %v", err)
	} else {
		klog.V(1).Infof("loaded mounter: %s", volumeMounterFuse)
		availableMounters = append(availableMounters, volumeMounterFuse)
	}

	if len(availableMounters) == 0 {
		return errors.New("no ceph mounters found on system")
	}

	return nil
}

type volumeMounter interface {
	mount(ctx context.Context, mountPoint string, cr *util.Credentials, volOptions *volumeOptions) error
	name() string
}

func newMounter(volOptions *volumeOptions) (volumeMounter, error) {
	// Get the mounter from the configuration

	wantMounter := volOptions.Mounter

	// Verify that it's available

	var chosenMounter string

	for _, availMounter := range availableMounters {
		if availMounter == wantMounter {
			chosenMounter = wantMounter
			break
		}
	}

	if chosenMounter == "" {
		// Otherwise pick whatever is left
		chosenMounter = availableMounters[0]
		klog.V(4).Infof("requested mounter: %s, chosen mounter: %s", wantMounter, chosenMounter)
	}

	// Create the mounter

	switch chosenMounter {
	case volumeMounterFuse:
		return &fuseMounter{}, nil
	case volumeMounterKernel:
		return &kernelMounter{}, nil
	}

	return nil, fmt.Errorf("unknown mounter '%s'", chosenMounter)
}

type fuseMounter struct{}

func mountFuse(ctx context.Context, mountPoint string, cr *util.Credentials, volOptions *volumeOptions) error {
	args := []string{
		mountPoint,
		"-m", volOptions.Monitors,
		"-c", util.CephConfigPath,
		"-n", cephEntityClientPrefix + cr.ID, "--keyfile=" + cr.KeyFile,
		"-r", volOptions.RootPath,
		"-o", "nonempty",
	}

	if volOptions.FuseMountOptions != "" {
		args = append(args, ","+volOptions.FuseMountOptions)
	}

	if volOptions.FsName != "" {
		args = append(args, "--client_mds_namespace="+volOptions.FsName)
	}

	_, stderr, err := execCommand(ctx, "ceph-fuse", args[:]...)
	if err != nil {
		return err
	}

	// Parse the output:
	// We need "starting fuse" meaning the mount is ok
	// and PID of the ceph-fuse daemon for unmount

	match := fusePidRx.FindSubmatch(stderr)
	if len(match) != 2 {
		return fmt.Errorf("ceph-fuse failed: %s", stderr)
	}

	pid, err := strconv.Atoi(string(match[1]))
	if err != nil {
		return fmt.Errorf("failed to parse FUSE daemon PID: %v", err)
	}

	fusePidMapMtx.Lock()
	fusePidMap[mountPoint] = pid
	fusePidMapMtx.Unlock()

	return nil
}

func (m *fuseMounter) mount(ctx context.Context, mountPoint string, cr *util.Credentials, volOptions *volumeOptions) error {
	if err := util.CreateMountPoint(mountPoint); err != nil {
		return err
	}

	return mountFuse(ctx, mountPoint, cr, volOptions)
}

func (m *fuseMounter) name() string { return "Ceph FUSE driver" }

type kernelMounter struct{}

func mountKernel(ctx context.Context, mountPoint string, cr *util.Credentials, volOptions *volumeOptions) error {
	if err := execCommandErr(ctx, "modprobe", "ceph"); err != nil {
		return err
	}

	args := []string{
		"-t", "ceph",
		fmt.Sprintf("%s:%s", volOptions.Monitors, volOptions.RootPath),
		mountPoint,
	}

	optionsStr := fmt.Sprintf("name=%s,secretfile=%s", cr.ID, cr.KeyFile)
	mdsNamespace := ""
	if volOptions.FsName != "" {
		mdsNamespace = fmt.Sprintf("mds_namespace=%s", volOptions.FsName)
	}
	optionsStr = util.MountOptionsAdd(optionsStr, mdsNamespace, volOptions.KernelMountOptions, netDev)

	args = append(args, "-o", optionsStr)

	return execCommandErr(ctx, "mount", args[:]...)
}

func (m *kernelMounter) mount(ctx context.Context, mountPoint string, cr *util.Credentials, volOptions *volumeOptions) error {
	if err := util.CreateMountPoint(mountPoint); err != nil {
		return err
	}

	return mountKernel(ctx, mountPoint, cr, volOptions)
}

func (m *kernelMounter) name() string { return "Ceph kernel client" }

func bindMount(ctx context.Context, from, to string, readOnly bool, mntOptions []string) error {
	mntOptionSli := strings.Join(mntOptions, ",")
	if err := execCommandErr(ctx, "mount", "-o", mntOptionSli, from, to); err != nil {
		return fmt.Errorf("failed to bind-mount %s to %s: %v", from, to, err)
	}

	if readOnly {
		mntOptionSli = util.MountOptionsAdd(mntOptionSli, "remount")
		if err := execCommandErr(ctx, "mount", "-o", mntOptionSli, to); err != nil {
			return fmt.Errorf("failed read-only remount of %s: %v", to, err)
		}
	}

	return nil
}

func unmountVolume(ctx context.Context, mountPoint string) error {
	if err := execCommandErr(ctx, "umount", mountPoint); err != nil {
		if strings.Contains(err.Error(), fmt.Sprintf("exit status 32: umount: %s: not mounted", mountPoint)) ||
			strings.Contains(err.Error(), "No such file or directory") {
			return nil
		}
		return err
	}

	fusePidMapMtx.Lock()
	pid, ok := fusePidMap[mountPoint]
	if ok {
		delete(fusePidMap, mountPoint)
	}
	fusePidMapMtx.Unlock()

	if ok {
		p, err := os.FindProcess(pid)
		if err != nil {
			klog.Warningf(util.Log(ctx, "failed to find process %d: %v"), pid, err)
		} else {
			if _, err = p.Wait(); err != nil {
				klog.Warningf(util.Log(ctx, "%d is not a child process: %v"), pid, err)
			}
		}
	}

	return nil
}
<|endoftext|>


<commit_before>package plugin

import (
	"bytes"
	"fmt"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/terraform/config/hcl2shim"
	"github.com/hashicorp/terraform/providers"
	"github.com/hashicorp/terraform/tfdiags"
	"github.com/zclconf/go-cty/cty"

	proto "github.com/hashicorp/terraform/internal/tfplugin5"
	mockproto "github.com/hashicorp/terraform/plugin/mock_proto"
)

var _ providers.Interface = (*GRPCProvider)(nil)

func mockProviderClient(t *testing.T) *mockproto.MockProviderClient {
	ctrl := gomock.NewController(t)
	client := mockproto.NewMockProviderClient(ctrl)

	// we always need a GetSchema method
	client.EXPECT().GetSchema(
		gomock.Any(),
		gomock.Any(),
	).Return(providerProtoSchema(), nil)

	return client
}

func checkDiags(t *testing.T, d tfdiags.Diagnostics) {
	t.Helper()
	if d.HasErrors() {
		t.Fatal(d.Err())
	}
}

func providerProtoSchema() *proto.GetProviderSchema_Response {
	return &proto.GetProviderSchema_Response{
		Provider: &proto.Schema{
			Block: &proto.Schema_Block{
				Attributes: []*proto.Schema_Attribute{
					{
						Name:     "attr",
						Type:     []byte(`"string"`),
						Required: true,
					},
				},
			},
		},
		ResourceSchemas: map[string]*proto.Schema{
			"resource": &proto.Schema{
				Version: 1,
				Block: &proto.Schema_Block{
					Attributes: []*proto.Schema_Attribute{
						{
							Name:     "attr",
							Type:     []byte(`"string"`),
							Required: true,
						},
					},
				},
			},
		},
		DataSourceSchemas: map[string]*proto.Schema{
			"data": &proto.Schema{
				Version: 1,
				Block: &proto.Schema_Block{
					Attributes: []*proto.Schema_Attribute{
						{
							Name:     "attr",
							Type:     []byte(`"string"`),
							Required: true,
						},
					},
				},
			},
		},
	}
}

func TestGRPCProvider_GetSchema(t *testing.T) {
	p := &GRPCProvider{
		client: mockProviderClient(t),
	}

	resp := p.GetSchema()
	checkDiags(t, resp.Diagnostics)
}

func TestGRPCProvider_PrepareProviderConfig(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().PrepareProviderConfig(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.PrepareProviderConfig_Response{}, nil)

	cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
	resp := p.PrepareProviderConfig(providers.PrepareProviderConfigRequest{Config: cfg})
	checkDiags(t, resp.Diagnostics)
}

func TestGRPCProvider_ValidateResourceTypeConfig(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().ValidateResourceTypeConfig(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ValidateResourceTypeConfig_Response{}, nil)

	cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
	resp := p.ValidateResourceTypeConfig(providers.ValidateResourceTypeConfigRequest{
		TypeName: "resource",
		Config:   cfg,
	})
	checkDiags(t, resp.Diagnostics)
}

func TestGRPCProvider_ValidateDataSourceConfig(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().ValidateDataSourceConfig(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ValidateDataSourceConfig_Response{}, nil)

	cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
	resp := p.ValidateDataSourceConfig(providers.ValidateDataSourceConfigRequest{
		TypeName: "data",
		Config:   cfg,
	})
	checkDiags(t, resp.Diagnostics)
}

func TestGRPCProvider_UpgradeResourceState(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().UpgradeResourceState(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.UpgradeResourceState_Response{
		UpgradedState: &proto.DynamicValue{
			Msgpack: []byte("\x81\xa4attr\xa3bar"),
		},
	}, nil)

	resp := p.UpgradeResourceState(providers.UpgradeResourceStateRequest{
		TypeName:     "resource",
		Version:      0,
		RawStateJSON: []byte(`{"old_attr":"bar"}`),
	})
	checkDiags(t, resp.Diagnostics)

	expected := cty.ObjectVal(map[string]cty.Value{
		"attr": cty.StringVal("bar"),
	})

	if !cmp.Equal(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty))
	}
}

func TestGRPCProvider_Configure(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().Configure(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.Configure_Response{}, nil)

	resp := p.Configure(providers.ConfigureRequest{
		Config: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("foo"),
		}),
	})
	checkDiags(t, resp.Diagnostics)
}

func TestGRPCProvider_Stop(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().Stop(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.Stop_Response{}, nil)

	err := p.Stop()
	if err != nil {
		t.Fatal(err)
	}
}

func TestGRPCProvider_ReadResource(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().ReadResource(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ReadResource_Response{
		NewState: &proto.DynamicValue{
			Msgpack: []byte("\x81\xa4attr\xa3bar"),
		},
	}, nil)

	resp := p.ReadResource(providers.ReadResourceRequest{
		TypeName: "resource",
		PriorState: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("foo"),
		}),
	})

	checkDiags(t, resp.Diagnostics)

	expected := cty.ObjectVal(map[string]cty.Value{
		"attr": cty.StringVal("bar"),
	})

	if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty))
	}
}

func TestGRPCProvider_PlanResourceChange(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	expectedPrivate := []byte(`{"meta": "data"}`)

	client.EXPECT().PlanResourceChange(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.PlanResourceChange_Response{
		PlannedState: &proto.DynamicValue{
			Msgpack: []byte("\x81\xa4attr\xa3bar"),
		},
		RequiresReplace: []*proto.AttributePath{
			{
				Steps: []*proto.AttributePath_Step{
					{
						Selector: &proto.AttributePath_Step_AttributeName{
							AttributeName: "attr",
						},
					},
				},
			},
		},
		PlannedPrivate: expectedPrivate,
	}, nil)

	resp := p.PlanResourceChange(providers.PlanResourceChangeRequest{
		TypeName: "resource",
		PriorState: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("foo"),
		}),
		ProposedNewState: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("bar"),
		}),
		Config: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("bar"),
		}),
	})

	checkDiags(t, resp.Diagnostics)

	expectedState := cty.ObjectVal(map[string]cty.Value{
		"attr": cty.StringVal("bar"),
	})

	if !cmp.Equal(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty))
	}

	expectedReplace := `[]cty.Path{cty.Path{cty.GetAttrStep{Name:"attr"}}}`
	replace := fmt.Sprintf("%#v", resp.RequiresReplace)
	if expectedReplace != replace {
		t.Fatalf("expected %q, got %q", expectedReplace, replace)
	}

	if !bytes.Equal(expectedPrivate, resp.PlannedPrivate) {
		t.Fatalf("expected %q, got %q", expectedPrivate, resp.PlannedPrivate)
	}
}

func TestGRPCProvider_ApplyResourceChange(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	expectedPrivate := []byte(`{"meta": "data"}`)

	client.EXPECT().ApplyResourceChange(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ApplyResourceChange_Response{
		NewState: &proto.DynamicValue{
			Msgpack: []byte("\x81\xa4attr\xa3bar"),
		},
		Private: expectedPrivate,
	}, nil)

	resp := p.ApplyResourceChange(providers.ApplyResourceChangeRequest{
		TypeName: "resource",
		PriorState: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("foo"),
		}),
		PlannedState: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("bar"),
		}),
		Config: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("bar"),
		}),
		PlannedPrivate: expectedPrivate,
	})

	checkDiags(t, resp.Diagnostics)

	expectedState := cty.ObjectVal(map[string]cty.Value{
		"attr": cty.StringVal("bar"),
	})

	if !cmp.Equal(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty))
	}

	if !bytes.Equal(expectedPrivate, resp.Private) {
		t.Fatalf("expected %q, got %q", expectedPrivate, resp.Private)
	}
}

func TestGRPCProvider_ImportResourceState(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	expectedPrivate := []byte(`{"meta": "data"}`)

	client.EXPECT().ImportResourceState(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ImportResourceState_Response{
		ImportedResources: []*proto.ImportResourceState_ImportedResource{
			{
				TypeName: "resource",
				State: &proto.DynamicValue{
					Msgpack: []byte("\x81\xa4attr\xa3bar"),
				},
				Private: expectedPrivate,
			},
		},
	}, nil)

	resp := p.ImportResourceState(providers.ImportResourceStateRequest{
		TypeName: "resource",
		ID:       "foo",
	})

	checkDiags(t, resp.Diagnostics)

	expectedResource := providers.ImportedResource{
		TypeName: "resource",
		State: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("bar"),
		}),
		Private: expectedPrivate,
	}

	imported := resp.ImportedResources[0]
	if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty))
	}
}

func TestGRPCProvider_ReadDataSource(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().ReadDataSource(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ReadDataSource_Response{
		State: &proto.DynamicValue{
			Msgpack: []byte("\x81\xa4attr\xa3bar"),
		},
	}, nil)

	resp := p.ReadDataSource(providers.ReadDataSourceRequest{
		TypeName: "data",
		Config: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("foo"),
		}),
	})

	checkDiags(t, resp.Diagnostics)

	expected := cty.ObjectVal(map[string]cty.Value{
		"attr": cty.StringVal("bar"),
	})

	if !cmp.Equal(expected, resp.State, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expected, resp.State, typeComparer, valueComparer, equateEmpty))
	}
}
<commit_msg>add 3rd param to mock call<commit_after>package plugin

import (
	"bytes"
	"fmt"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/terraform/config/hcl2shim"
	"github.com/hashicorp/terraform/providers"
	"github.com/hashicorp/terraform/tfdiags"
	"github.com/zclconf/go-cty/cty"

	proto "github.com/hashicorp/terraform/internal/tfplugin5"
	mockproto "github.com/hashicorp/terraform/plugin/mock_proto"
)

var _ providers.Interface = (*GRPCProvider)(nil)

func mockProviderClient(t *testing.T) *mockproto.MockProviderClient {
	ctrl := gomock.NewController(t)
	client := mockproto.NewMockProviderClient(ctrl)

	// we always need a GetSchema method
	client.EXPECT().GetSchema(
		gomock.Any(),
		gomock.Any(),
		gomock.Any(),
	).Return(providerProtoSchema(), nil)

	return client
}

func checkDiags(t *testing.T, d tfdiags.Diagnostics) {
	t.Helper()
	if d.HasErrors() {
		t.Fatal(d.Err())
	}
}

func providerProtoSchema() *proto.GetProviderSchema_Response {
	return &proto.GetProviderSchema_Response{
		Provider: &proto.Schema{
			Block: &proto.Schema_Block{
				Attributes: []*proto.Schema_Attribute{
					{
						Name:     "attr",
						Type:     []byte(`"string"`),
						Required: true,
					},
				},
			},
		},
		ResourceSchemas: map[string]*proto.Schema{
			"resource": &proto.Schema{
				Version: 1,
				Block: &proto.Schema_Block{
					Attributes: []*proto.Schema_Attribute{
						{
							Name:     "attr",
							Type:     []byte(`"string"`),
							Required: true,
						},
					},
				},
			},
		},
		DataSourceSchemas: map[string]*proto.Schema{
			"data": &proto.Schema{
				Version: 1,
				Block: &proto.Schema_Block{
					Attributes: []*proto.Schema_Attribute{
						{
							Name:     "attr",
							Type:     []byte(`"string"`),
							Required: true,
						},
					},
				},
			},
		},
	}
}

func TestGRPCProvider_GetSchema(t *testing.T) {
	p := &GRPCProvider{
		client: mockProviderClient(t),
	}

	resp := p.GetSchema()
	checkDiags(t, resp.Diagnostics)
}

func TestGRPCProvider_PrepareProviderConfig(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().PrepareProviderConfig(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.PrepareProviderConfig_Response{}, nil)

	cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
	resp := p.PrepareProviderConfig(providers.PrepareProviderConfigRequest{Config: cfg})
	checkDiags(t, resp.Diagnostics)
}

func TestGRPCProvider_ValidateResourceTypeConfig(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().ValidateResourceTypeConfig(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ValidateResourceTypeConfig_Response{}, nil)

	cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
	resp := p.ValidateResourceTypeConfig(providers.ValidateResourceTypeConfigRequest{
		TypeName: "resource",
		Config:   cfg,
	})
	checkDiags(t, resp.Diagnostics)
}

func TestGRPCProvider_ValidateDataSourceConfig(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().ValidateDataSourceConfig(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ValidateDataSourceConfig_Response{}, nil)

	cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"attr": "value"})
	resp := p.ValidateDataSourceConfig(providers.ValidateDataSourceConfigRequest{
		TypeName: "data",
		Config:   cfg,
	})
	checkDiags(t, resp.Diagnostics)
}

func TestGRPCProvider_UpgradeResourceState(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().UpgradeResourceState(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.UpgradeResourceState_Response{
		UpgradedState: &proto.DynamicValue{
			Msgpack: []byte("\x81\xa4attr\xa3bar"),
		},
	}, nil)

	resp := p.UpgradeResourceState(providers.UpgradeResourceStateRequest{
		TypeName:     "resource",
		Version:      0,
		RawStateJSON: []byte(`{"old_attr":"bar"}`),
	})
	checkDiags(t, resp.Diagnostics)

	expected := cty.ObjectVal(map[string]cty.Value{
		"attr": cty.StringVal("bar"),
	})

	if !cmp.Equal(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expected, resp.UpgradedState, typeComparer, valueComparer, equateEmpty))
	}
}

func TestGRPCProvider_Configure(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().Configure(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.Configure_Response{}, nil)

	resp := p.Configure(providers.ConfigureRequest{
		Config: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("foo"),
		}),
	})
	checkDiags(t, resp.Diagnostics)
}

func TestGRPCProvider_Stop(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().Stop(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.Stop_Response{}, nil)

	err := p.Stop()
	if err != nil {
		t.Fatal(err)
	}
}

func TestGRPCProvider_ReadResource(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().ReadResource(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ReadResource_Response{
		NewState: &proto.DynamicValue{
			Msgpack: []byte("\x81\xa4attr\xa3bar"),
		},
	}, nil)

	resp := p.ReadResource(providers.ReadResourceRequest{
		TypeName: "resource",
		PriorState: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("foo"),
		}),
	})

	checkDiags(t, resp.Diagnostics)

	expected := cty.ObjectVal(map[string]cty.Value{
		"attr": cty.StringVal("bar"),
	})

	if !cmp.Equal(expected, resp.NewState, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expected, resp.NewState, typeComparer, valueComparer, equateEmpty))
	}
}

func TestGRPCProvider_PlanResourceChange(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	expectedPrivate := []byte(`{"meta": "data"}`)

	client.EXPECT().PlanResourceChange(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.PlanResourceChange_Response{
		PlannedState: &proto.DynamicValue{
			Msgpack: []byte("\x81\xa4attr\xa3bar"),
		},
		RequiresReplace: []*proto.AttributePath{
			{
				Steps: []*proto.AttributePath_Step{
					{
						Selector: &proto.AttributePath_Step_AttributeName{
							AttributeName: "attr",
						},
					},
				},
			},
		},
		PlannedPrivate: expectedPrivate,
	}, nil)

	resp := p.PlanResourceChange(providers.PlanResourceChangeRequest{
		TypeName: "resource",
		PriorState: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("foo"),
		}),
		ProposedNewState: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("bar"),
		}),
		Config: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("bar"),
		}),
	})

	checkDiags(t, resp.Diagnostics)

	expectedState := cty.ObjectVal(map[string]cty.Value{
		"attr": cty.StringVal("bar"),
	})

	if !cmp.Equal(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expectedState, resp.PlannedState, typeComparer, valueComparer, equateEmpty))
	}

	expectedReplace := `[]cty.Path{cty.Path{cty.GetAttrStep{Name:"attr"}}}`
	replace := fmt.Sprintf("%#v", resp.RequiresReplace)
	if expectedReplace != replace {
		t.Fatalf("expected %q, got %q", expectedReplace, replace)
	}

	if !bytes.Equal(expectedPrivate, resp.PlannedPrivate) {
		t.Fatalf("expected %q, got %q", expectedPrivate, resp.PlannedPrivate)
	}
}

func TestGRPCProvider_ApplyResourceChange(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	expectedPrivate := []byte(`{"meta": "data"}`)

	client.EXPECT().ApplyResourceChange(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ApplyResourceChange_Response{
		NewState: &proto.DynamicValue{
			Msgpack: []byte("\x81\xa4attr\xa3bar"),
		},
		Private: expectedPrivate,
	}, nil)

	resp := p.ApplyResourceChange(providers.ApplyResourceChangeRequest{
		TypeName: "resource",
		PriorState: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("foo"),
		}),
		PlannedState: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("bar"),
		}),
		Config: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("bar"),
		}),
		PlannedPrivate: expectedPrivate,
	})

	checkDiags(t, resp.Diagnostics)

	expectedState := cty.ObjectVal(map[string]cty.Value{
		"attr": cty.StringVal("bar"),
	})

	if !cmp.Equal(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expectedState, resp.NewState, typeComparer, valueComparer, equateEmpty))
	}

	if !bytes.Equal(expectedPrivate, resp.Private) {
		t.Fatalf("expected %q, got %q", expectedPrivate, resp.Private)
	}
}

func TestGRPCProvider_ImportResourceState(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	expectedPrivate := []byte(`{"meta": "data"}`)

	client.EXPECT().ImportResourceState(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ImportResourceState_Response{
		ImportedResources: []*proto.ImportResourceState_ImportedResource{
			{
				TypeName: "resource",
				State: &proto.DynamicValue{
					Msgpack: []byte("\x81\xa4attr\xa3bar"),
				},
				Private: expectedPrivate,
			},
		},
	}, nil)

	resp := p.ImportResourceState(providers.ImportResourceStateRequest{
		TypeName: "resource",
		ID:       "foo",
	})

	checkDiags(t, resp.Diagnostics)

	expectedResource := providers.ImportedResource{
		TypeName: "resource",
		State: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("bar"),
		}),
		Private: expectedPrivate,
	}

	imported := resp.ImportedResources[0]
	if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty))
	}
}

func TestGRPCProvider_ReadDataSource(t *testing.T) {
	client := mockProviderClient(t)
	p := &GRPCProvider{
		client: client,
	}

	client.EXPECT().ReadDataSource(
		gomock.Any(),
		gomock.Any(),
	).Return(&proto.ReadDataSource_Response{
		State: &proto.DynamicValue{
			Msgpack: []byte("\x81\xa4attr\xa3bar"),
		},
	}, nil)

	resp := p.ReadDataSource(providers.ReadDataSourceRequest{
		TypeName: "data",
		Config: cty.ObjectVal(map[string]cty.Value{
			"attr": cty.StringVal("foo"),
		}),
	})

	checkDiags(t, resp.Diagnostics)

	expected := cty.ObjectVal(map[string]cty.Value{
		"attr": cty.StringVal("bar"),
	})

	if !cmp.Equal(expected, resp.State, typeComparer, valueComparer, equateEmpty) {
		t.Fatal(cmp.Diff(expected, resp.State, typeComparer, valueComparer, equateEmpty))
	}
}
<|endoftext|>


<commit_before>// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package target

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"github.com/minio/minio/internal/event"
	"github.com/minio/minio/internal/logger"
	"github.com/minio/pkg/certs"
	xnet "github.com/minio/pkg/net"
)

// Webhook constants
const (
	WebhookEndpoint   = "endpoint"
	WebhookAuthToken  = "auth_token"
	WebhookQueueDir   = "queue_dir"
	WebhookQueueLimit = "queue_limit"
	WebhookClientCert = "client_cert"
	WebhookClientKey  = "client_key"

	EnvWebhookEnable     = "MINIO_NOTIFY_WEBHOOK_ENABLE"
	EnvWebhookEndpoint   = "MINIO_NOTIFY_WEBHOOK_ENDPOINT"
	EnvWebhookAuthToken  = "MINIO_NOTIFY_WEBHOOK_AUTH_TOKEN"
	EnvWebhookQueueDir   = "MINIO_NOTIFY_WEBHOOK_QUEUE_DIR"
	EnvWebhookQueueLimit = "MINIO_NOTIFY_WEBHOOK_QUEUE_LIMIT"
	EnvWebhookClientCert = "MINIO_NOTIFY_WEBHOOK_CLIENT_CERT"
	EnvWebhookClientKey  = "MINIO_NOTIFY_WEBHOOK_CLIENT_KEY"
)

// WebhookArgs - Webhook target arguments.
type WebhookArgs struct {
	Enable     bool            `json:"enable"`
	Endpoint   xnet.URL        `json:"endpoint"`
	AuthToken  string          `json:"authToken"`
	Transport  *http.Transport `json:"-"`
	QueueDir   string          `json:"queueDir"`
	QueueLimit uint64          `json:"queueLimit"`
	ClientCert string          `json:"clientCert"`
	ClientKey  string          `json:"clientKey"`
}

// Validate WebhookArgs fields
func (w WebhookArgs) Validate() error {
	if !w.Enable {
		return nil
	}
	if w.Endpoint.IsEmpty() {
		return errors.New("endpoint empty")
	}
	if w.QueueDir != "" {
		if !filepath.IsAbs(w.QueueDir) {
			return errors.New("queueDir path should be absolute")
		}
	}
	if w.ClientCert != "" && w.ClientKey == "" || w.ClientCert == "" && w.ClientKey != "" {
		return errors.New("cert and key must be specified as a pair")
	}
	return nil
}

// WebhookTarget - Webhook target.
type WebhookTarget struct {
	lazyInit lazyInit

	id         event.TargetID
	args       WebhookArgs
	transport  *http.Transport
	httpClient *http.Client
	store      Store
	loggerOnce logger.LogOnce
	quitCh     chan struct{}
}

// ID - returns target ID.
func (target *WebhookTarget) ID() event.TargetID {
	return target.id
}

// IsActive - Return true if target is up and active
func (target *WebhookTarget) IsActive() (bool, error) {
	if err := target.init(); err != nil {
		return false, err
	}
	return target.isActive()
}

func (target *WebhookTarget) isActive() (bool, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodHead, target.args.Endpoint.String(), nil)
	if err != nil {
		if xnet.IsNetworkOrHostDown(err, false) {
			return false, errNotConnected
		}
		return false, err
	}
	tokens := strings.Fields(target.args.AuthToken)
	switch len(tokens) {
	case 2:
		req.Header.Set("Authorization", target.args.AuthToken)
	case 1:
		req.Header.Set("Authorization", "Bearer "+target.args.AuthToken)
	}

	resp, err := target.httpClient.Do(req)
	if err != nil {
		if xnet.IsNetworkOrHostDown(err, true) {
			return false, errNotConnected
		}
		return false, err
	}
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
	// No network failure i.e response from the target means its up
	return true, nil
}

// Save - saves the events to the store if queuestore is configured,
// which will be replayed when the webhook connection is active.
func (target *WebhookTarget) Save(eventData event.Event) error {
	if err := target.init(); err != nil {
		return err
	}

	if target.store != nil {
		return target.store.Put(eventData)
	}
	err := target.send(eventData)
	if err != nil {
		if xnet.IsNetworkOrHostDown(err, false) {
			return errNotConnected
		}
	}
	return err
}

// send - sends an event to the webhook.
func (target *WebhookTarget) send(eventData event.Event) error {
	objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
	if err != nil {
		return err
	}
	key := eventData.S3.Bucket.Name + "/" + objectName

	data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}})
	if err != nil {
		return err
	}

	req, err := http.NewRequest("POST", target.args.Endpoint.String(), bytes.NewReader(data))
	if err != nil {
		return err
	}

	// Verify if the authToken already contains
	// <Key> <Token> like format, if this is
	// already present we can blindly use the
	// authToken as is instead of adding 'Bearer'
	tokens := strings.Fields(target.args.AuthToken)
	switch len(tokens) {
	case 2:
		req.Header.Set("Authorization", target.args.AuthToken)
	case 1:
		req.Header.Set("Authorization", "Bearer "+target.args.AuthToken)
	}

	req.Header.Set("Content-Type", "application/json")

	resp, err := target.httpClient.Do(req)
	if err != nil {
		target.Close()
		return err
	}
	defer resp.Body.Close()
	io.Copy(io.Discard, resp.Body)

	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		target.Close()
		return fmt.Errorf("sending event failed with %v", resp.Status)
	}

	return nil
}

// Send - reads an event from store and sends it to webhook.
func (target *WebhookTarget) Send(eventKey string) error {
	if err := target.init(); err != nil {
		return err
	}

	eventData, eErr := target.store.Get(eventKey)
	if eErr != nil {
		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
		// Such events will not exist and would've been already been sent successfully.
		if os.IsNotExist(eErr) {
			return nil
		}
		return eErr
	}

	if err := target.send(eventData); err != nil {
		if xnet.IsNetworkOrHostDown(err, false) {
			return errNotConnected
		}
		return err
	}

	// Delete the event from store.
	return target.store.Del(eventKey)
}

// Close - does nothing and available for interface compatibility.
func (target *WebhookTarget) Close() error {
	close(target.quitCh)
	return nil
}

func (target *WebhookTarget) init() error {
	return target.lazyInit.Do(target.initWebhook)
}

// Only called from init()
func (target *WebhookTarget) initWebhook() error {
	args := target.args
	transport := target.transport

	if args.ClientCert != "" && args.ClientKey != "" {
		manager, err := certs.NewManager(context.Background(), args.ClientCert, args.ClientKey, tls.LoadX509KeyPair)
		if err != nil {
			return err
		}
		manager.ReloadOnSignal(syscall.SIGHUP) // allow reloads upon SIGHUP
		transport.TLSClientConfig.GetClientCertificate = manager.GetClientCertificate
	}
	target.httpClient = &http.Client{Transport: transport}

	yes, err := target.isActive()
	if err != nil {
		return err
	}
	if !yes {
		return errNotConnected
	}

	if target.store != nil {
		streamEventsFromStore(target.store, target, target.quitCh, target.loggerOnce)
	}
	return nil
}

// NewWebhookTarget - creates new Webhook target.
func NewWebhookTarget(ctx context.Context, id string, args WebhookArgs, loggerOnce logger.LogOnce, transport *http.Transport) (*WebhookTarget, error) {
	var store Store
	if args.QueueDir != "" {
		queueDir := filepath.Join(args.QueueDir, storePrefix+"-webhook-"+id)
		store = NewQueueStore(queueDir, args.QueueLimit)
		if err := store.Open(); err != nil {
			return nil, fmt.Errorf("unable to initialize the queue store of Webhook `%s`: %w", id, err)
		}
	}

	return &WebhookTarget{
		id:         event.TargetID{ID: id, Name: "webhook"},
		args:       args,
		loggerOnce: loggerOnce,
		transport:  transport,
		store:      store,
		quitCh:     make(chan struct{}),
	}, nil
}
<commit_msg>do not panic if webhook returns an error (#15970)<commit_after>// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package target

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"github.com/minio/minio/internal/event"
	"github.com/minio/minio/internal/logger"
	"github.com/minio/pkg/certs"
	xnet "github.com/minio/pkg/net"
)

// Webhook constants
const (
	WebhookEndpoint   = "endpoint"
	WebhookAuthToken  = "auth_token"
	WebhookQueueDir   = "queue_dir"
	WebhookQueueLimit = "queue_limit"
	WebhookClientCert = "client_cert"
	WebhookClientKey  = "client_key"

	EnvWebhookEnable     = "MINIO_NOTIFY_WEBHOOK_ENABLE"
	EnvWebhookEndpoint   = "MINIO_NOTIFY_WEBHOOK_ENDPOINT"
	EnvWebhookAuthToken  = "MINIO_NOTIFY_WEBHOOK_AUTH_TOKEN"
	EnvWebhookQueueDir   = "MINIO_NOTIFY_WEBHOOK_QUEUE_DIR"
	EnvWebhookQueueLimit = "MINIO_NOTIFY_WEBHOOK_QUEUE_LIMIT"
	EnvWebhookClientCert = "MINIO_NOTIFY_WEBHOOK_CLIENT_CERT"
	EnvWebhookClientKey  = "MINIO_NOTIFY_WEBHOOK_CLIENT_KEY"
)

// WebhookArgs - Webhook target arguments.
type WebhookArgs struct {
	Enable     bool            `json:"enable"`
	Endpoint   xnet.URL        `json:"endpoint"`
	AuthToken  string          `json:"authToken"`
	Transport  *http.Transport `json:"-"`
	QueueDir   string          `json:"queueDir"`
	QueueLimit uint64          `json:"queueLimit"`
	ClientCert string          `json:"clientCert"`
	ClientKey  string          `json:"clientKey"`
}

// Validate WebhookArgs fields
func (w WebhookArgs) Validate() error {
	if !w.Enable {
		return nil
	}
	if w.Endpoint.IsEmpty() {
		return errors.New("endpoint empty")
	}
	if w.QueueDir != "" {
		if !filepath.IsAbs(w.QueueDir) {
			return errors.New("queueDir path should be absolute")
		}
	}
	if w.ClientCert != "" && w.ClientKey == "" || w.ClientCert == "" && w.ClientKey != "" {
		return errors.New("cert and key must be specified as a pair")
	}
	return nil
}

// WebhookTarget - Webhook target.
type WebhookTarget struct {
	lazyInit lazyInit

	id         event.TargetID
	args       WebhookArgs
	transport  *http.Transport
	httpClient *http.Client
	store      Store
	loggerOnce logger.LogOnce
	cancel     context.CancelFunc
	cancelCh   <-chan struct{}
}

// ID - returns target ID.
func (target *WebhookTarget) ID() event.TargetID {
	return target.id
}

// IsActive - Return true if target is up and active
func (target *WebhookTarget) IsActive() (bool, error) {
	if err := target.init(); err != nil {
		return false, err
	}
	return target.isActive()
}

func (target *WebhookTarget) isActive() (bool, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodHead, target.args.Endpoint.String(), nil)
	if err != nil {
		if xnet.IsNetworkOrHostDown(err, false) {
			return false, errNotConnected
		}
		return false, err
	}
	tokens := strings.Fields(target.args.AuthToken)
	switch len(tokens) {
	case 2:
		req.Header.Set("Authorization", target.args.AuthToken)
	case 1:
		req.Header.Set("Authorization", "Bearer "+target.args.AuthToken)
	}

	resp, err := target.httpClient.Do(req)
	if err != nil {
		if xnet.IsNetworkOrHostDown(err, true) {
			return false, errNotConnected
		}
		return false, err
	}
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
	// No network failure i.e response from the target means its up
	return true, nil
}

// Save - saves the events to the store if queuestore is configured,
// which will be replayed when the webhook connection is active.
func (target *WebhookTarget) Save(eventData event.Event) error {
	if err := target.init(); err != nil {
		return err
	}

	if target.store != nil {
		return target.store.Put(eventData)
	}
	err := target.send(eventData)
	if err != nil {
		if xnet.IsNetworkOrHostDown(err, false) {
			return errNotConnected
		}
	}
	return err
}

// send - sends an event to the webhook.
func (target *WebhookTarget) send(eventData event.Event) error {
	objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
	if err != nil {
		return err
	}
	key := eventData.S3.Bucket.Name + "/" + objectName

	data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}})
	if err != nil {
		return err
	}

	req, err := http.NewRequest("POST", target.args.Endpoint.String(), bytes.NewReader(data))
	if err != nil {
		return err
	}

	// Verify if the authToken already contains
	// <Key> <Token> like format, if this is
	// already present we can blindly use the
	// authToken as is instead of adding 'Bearer'
	tokens := strings.Fields(target.args.AuthToken)
	switch len(tokens) {
	case 2:
		req.Header.Set("Authorization", target.args.AuthToken)
	case 1:
		req.Header.Set("Authorization", "Bearer "+target.args.AuthToken)
	}

	req.Header.Set("Content-Type", "application/json")

	resp, err := target.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	io.Copy(io.Discard, resp.Body)

	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return fmt.Errorf("sending event failed with %v", resp.Status)
	}

	return nil
}

// Send - reads an event from store and sends it to webhook.
func (target *WebhookTarget) Send(eventKey string) error {
	if err := target.init(); err != nil {
		return err
	}

	eventData, eErr := target.store.Get(eventKey)
	if eErr != nil {
		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
		// Such events will not exist and would've been already been sent successfully.
		if os.IsNotExist(eErr) {
			return nil
		}
		return eErr
	}

	if err := target.send(eventData); err != nil {
		if xnet.IsNetworkOrHostDown(err, false) {
			return errNotConnected
		}
		return err
	}

	// Delete the event from store.
	return target.store.Del(eventKey)
}

// Close - does nothing and available for interface compatibility.
func (target *WebhookTarget) Close() error {
	target.cancel()
	return nil
}

func (target *WebhookTarget) init() error {
	return target.lazyInit.Do(target.initWebhook)
}

// Only called from init()
func (target *WebhookTarget) initWebhook() error {
	args := target.args
	transport := target.transport

	if args.ClientCert != "" && args.ClientKey != "" {
		manager, err := certs.NewManager(context.Background(), args.ClientCert, args.ClientKey, tls.LoadX509KeyPair)
		if err != nil {
			return err
		}
		manager.ReloadOnSignal(syscall.SIGHUP) // allow reloads upon SIGHUP
		transport.TLSClientConfig.GetClientCertificate = manager.GetClientCertificate
	}
	target.httpClient = &http.Client{Transport: transport}

	yes, err := target.isActive()
	if err != nil {
		return err
	}
	if !yes {
		return errNotConnected
	}

	if target.store != nil {
		streamEventsFromStore(target.store, target, target.cancelCh, target.loggerOnce)
	}
	return nil
}

// NewWebhookTarget - creates new Webhook target.
func NewWebhookTarget(ctx context.Context, id string, args WebhookArgs, loggerOnce logger.LogOnce, transport *http.Transport) (*WebhookTarget, error) {
	ctx, cancel := context.WithCancel(ctx)

	var store Store
	if args.QueueDir != "" {
		queueDir := filepath.Join(args.QueueDir, storePrefix+"-webhook-"+id)
		store = NewQueueStore(queueDir, args.QueueLimit)
		if err := store.Open(); err != nil {
			cancel()
			return nil, fmt.Errorf("unable to initialize the queue store of Webhook `%s`: %w", id, err)
		}
	}

	return &WebhookTarget{
		id:         event.TargetID{ID: id, Name: "webhook"},
		args:       args,
		loggerOnce: loggerOnce,
		transport:  transport,
		store:      store,
		cancel:     cancel,
		cancelCh:   ctx.Done(),
	}, nil
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"os"
	"os/signal"
	"path"
	"syscall"
	"time"

	"github.com/fatih/color"
)

var jobs = map[string]time.Duration{
	"popular-anime": 1 * time.Second,
}

func main() {
	// Start all jobs defined in the map above
	startJobs()

	// Wait for program termination
	wait()
}

func startJobs() {
	exe, err := os.Executable()

	if err != nil {
		panic(err)
	}

	root := path.Dir(exe)

	for job, interval := range jobs {
		jobInterval := interval
		executable := path.Join(root, job, job)

		fmt.Printf("Registered job %s for execution every %v\n", color.YellowString(job), interval)

		go func() {
			ticker := time.NewTicker(jobInterval)
			defer ticker.Stop()

			for {
				fmt.Println(executable)
				<-ticker.C
			}
		}()
	}
}

func wait() {
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
	<-stop
}
<commit_msg>Implemented job scheduler<commit_after>package main

import (
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"path"
	"syscall"
	"time"

	"github.com/aerogo/log"
	"github.com/fatih/color"
)

var colorPool = []*color.Color{
	color.New(color.FgCyan),
	color.New(color.FgYellow),
	color.New(color.FgGreen),
	color.New(color.FgBlue),
	color.New(color.FgMagenta),
}

var jobs = map[string]time.Duration{
	"popular-anime": 5 * time.Second,
	"search-index":  15 * time.Second,
}

func main() {
	// Start all jobs defined in the map above
	startJobs()

	// Wait for program termination
	wait()
}

func startJobs() {
	// Get the directory the executable is in
	exe, err := os.Executable()

	if err != nil {
		panic(err)
	}

	root := path.Dir(exe)

	// Log paths
	logsPath := path.Join(root, "../", "logs")
	jobLogsPath := path.Join(root, "../", "logs", "jobs")
	os.Mkdir(jobLogsPath, 0777)

	// Scheduler log
	mainLog := log.New()
	mainLog.AddOutput(os.Stdout)
	mainLog.AddOutput(log.File(path.Join(logsPath, "scheduler.log")))
	schedulerLog := mainLog

	// Color index
	colorIndex := 0

	// Start each job
	for job, interval := range jobs {
		jobName := job
		jobInterval := interval
		executable := path.Join(root, jobName, jobName)
		jobColor := colorPool[colorIndex].SprintFunc()

		jobLog := log.New()
		jobLog.AddOutput(log.File(path.Join(jobLogsPath, jobName+".log")))

		fmt.Printf("Registered job %s for execution every %v\n", jobColor(jobName), interval)

		go func() {
			ticker := time.NewTicker(jobInterval)
			defer ticker.Stop()

			var err error

			for {
				schedulerLog.Info("Starting " + jobColor(jobName))

				cmd := exec.Command(executable)
				cmd.Stdout = jobLog
				cmd.Stderr = jobLog

				err = cmd.Start()

				if err != nil {
					color.Red(err.Error())
				}

				err = cmd.Wait()

				if err != nil {
					color.Red(err.Error())
				}

				schedulerLog.Info("Finished " + jobColor(jobName))
				jobLog.Info("--------------------------------------------------------------------------------")

				<-ticker.C
			}
		}()

		colorIndex = (colorIndex + 1) % len(colorPool)
	}

	// Finished job registration
	println("--------------------------------------------------------------------------------")
}

func wait() {
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
	<-stop
}
<|endoftext|>


<commit_before>package validator

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"strings"
	"unicode"

	"github.com/ghodss/yaml"
	"github.com/golang/protobuf/jsonpb"
	"github.com/googleapis/gapic-config-validator/internal/config"
	"github.com/jhump/protoreflect/desc"
	"google.golang.org/genproto/googleapis/api/annotations"
	"google.golang.org/genproto/googleapis/longrunning"
)

var (
	wellKnownPatterns = map[string]bool{
		"projects/{project}":                      true,
		"organizations/{organization}":            true,
		"folders/{folder}":                        true,
		"projects/{project}/locations/{location}": true,
		"billingAccounts/{billing_account_id}":    true,
	}

	wellKnownNames = map[string]bool{
		"project":      true,
		"organization": true,
		"folder":       true,
		"location":     true,
	}
)

func (v *validator) compare() {
	// compare interfaces
	v.compareServices()

	// compare resource references
	v.compareResourceRefs()
}

func (v *validator) compareServices() {
	for _, inter := range v.gapic.GetInterfaces() {
		serv := v.resolveServiceByName(inter.GetName())
		if serv == nil {
			v.addError("Interface %q does not exist", inter.GetName())
			continue
		}

		// compare resources
		v.compareResources(inter)

		// compare methods
		for _, method := range inter.GetMethods() {
			methodDesc := serv.FindMethodByName(method.GetName())
			if methodDesc == nil {
				v.addError("Method %q does not exist", inter.GetName()+"."+method.GetName())
				continue
			}

			v.compareMethod(methodDesc, method)
		}
	}
}

func (v *validator) compareMethod(methodDesc *desc.MethodDescriptor, method *config.MethodConfigProto) {
	fqn := methodDesc.GetFullyQualifiedName()
	mOpts := methodDesc.GetMethodOptions()

	// compare method_signatures & flattening groups
	if flattenings := method.GetFlattening(); flattenings != nil {
		eSigs, err := ext(mOpts, annotations.E_MethodSignature)
		if err != nil {
			v.addError("Method %q missing method_signature(s) for flattening(s)", fqn)
			goto LRO
		}
		sigs := eSigs.([]string)

		for _, flat := range flattenings.GetGroups() {
			joined := strings.Join(flat.GetParameters(), ",")
			if !containStr(sigs, joined) {
				v.addError("Method %q missing method_signature for flattening %q", fqn, joined)
			}
		}
	}

LRO:
	// compare operation_info & longrunning config
	if lro := method.GetLongRunning(); lro != nil {
		eLRO, err := ext(mOpts, longrunning.E_OperationInfo)
		if err != nil {
			v.addError("Method %q missing longrunning.operation_info", fqn)
			goto Behavior
		}
		info := eLRO.(*longrunning.OperationInfo)

		// trim to local message name
		protoRes := info.GetResponseType()
		if strings.Contains(protoRes, ".") {
			protoRes = protoRes[strings.LastIndex(protoRes, ".")+1:]
		}
		gapicRes := lro.GetReturnType()
		if strings.Contains(gapicRes, ".") {
			gapicRes = gapicRes[strings.LastIndex(gapicRes, ".")+1:]
		}

		if protoRes != gapicRes {
			v.addError("Method %q operation_info.response_type %q does not match %q",
				fqn,
				protoRes,
				gapicRes)
		}

		// trim to local message name
		protoMeta := info.GetMetadataType()
		if strings.Contains(protoMeta, ".") {
			protoMeta = protoMeta[strings.LastIndex(protoMeta, ".")+1:]
		}
		gapicMeta := lro.GetMetadataType()
		if strings.Contains(gapicMeta, ".") {
			gapicMeta = gapicMeta[strings.LastIndex(gapicMeta, ".")+1:]
		}

		if protoMeta != gapicMeta {
			v.addError("Method %q operation_info.metadata_type %q does not match %q",
				fqn,
				protoMeta,
				gapicMeta)
		}
	}

Behavior:
	// compare input message field_behaviors & required_fields
	if reqs := method.GetRequiredFields(); len(reqs) > 0 {
		input := methodDesc.GetInputType()

		for _, name := range reqs {
			field := input.FindFieldByName(name)
			if field == nil {
				v.addError("Field %q in method %q required_fields does not exist in %q",
					name,
					fqn,
					input.GetFullyQualifiedName())
				continue
			}

			eBehv, err := ext(field.GetFieldOptions(), annotations.E_FieldBehavior)
			if err != nil {
				v.addError("Field %q is missing field_behavior = REQUIRED per required_fields config", field.GetFullyQualifiedName())
				continue
			}
			behavior := eBehv.([]annotations.FieldBehavior)

			if !containBehavior(behavior, annotations.FieldBehavior_REQUIRED) {
				v.addError("Field %q is not annotated as REQUIRED per required_fields config", field.GetFullyQualifiedName())
			}
		}
	}
}

func (v *validator) compareResources(inter *config.InterfaceConfigProto) {
	for _, res := range inter.GetCollections() {
		if wellKnownPatterns[res.GetNamePattern()] || wellKnownNames[res.GetEntityName()] {
			continue
		}

		for _, f := range v.files {
			for _, m := range f.GetMessageTypes() {
				eRes, err := ext(m.GetMessageOptions(), annotations.E_Resource)
				if err != nil {
					continue
				}
				resDesc := eRes.(*annotations.ResourceDescriptor)

				typ := resDesc.GetType()
				typ = typ[strings.Index(typ, "/")+1:]

				entName := snakeToCamel(res.GetEntityName())

				// the pattern is defined in a resource named differently than the
				// name_pattern value, which is OK.
				if containStr(resDesc.GetPattern(), res.GetNamePattern()) {
					goto Next
				}

				if typ == entName {
					if !containStr(resDesc.GetPattern(), res.GetNamePattern()) {
						v.addError("resource definition for %q in %q does not have pattern %q",
							resDesc.GetType(),
							m.GetFullyQualifiedName(),
							res.GetNamePattern())
					}

					goto Next
				}
			}
		}

		v.addError("No corresponding resource definition for %q: %q", res.GetEntityName(), res.GetNamePattern())

	Next:
	}
}

func (v *validator) compareResourceRefs() {
	for _, ref := range v.gapic.GetResourceNameGeneration() {
		msgDesc := v.resolveMsgByLocalName(ref.GetMessageName())
		if msgDesc == nil {
			v.addError("Message %q in resource_name_generation item does not exist", ref.GetMessageName())
			continue
		}

		for fname, ref := range ref.GetFieldEntityMap() {
			// skip nested fields, presumably they are
			// being validated in the origial msg
			if strings.Contains(fname, ".") {
				continue
			}

			field := msgDesc.FindFieldByName(fname)
			if field == nil {
				v.addError("Field %q does not exist on message %q per resource_name_generation item", fname, msgDesc.GetFullyQualifiedName())
				continue
			}

			var typ string
			if eResRef, err := ext(field.GetFieldOptions(), annotations.E_ResourceReference); err == nil {
				resRef := eResRef.(*annotations.ResourceReference)

				typ = resRef.GetType()
				if typ == "" {
					typ = resRef.GetChildType()
				}
			} else if eRes, err := ext(msgDesc.GetMessageOptions(), annotations.E_Resource); err == nil {
				res := eRes.(*annotations.ResourceDescriptor)
				typ = res.GetType()
			} else {
				v.addError("Field %q missing resource_reference to %q", field.GetFullyQualifiedName(), ref)
				continue
			}

			// compare using upper camel case names
			t := typ[strings.Index(typ, "/")+1:]
			if !wellKnownTypes[typ] && t != snakeToCamel(ref) {
				v.addError("Field %q resource_type_kind %q doesn't match %q in config", field.GetFullyQualifiedName(), typ, ref)
			}
		}
	}
}

func containBehavior(arr []annotations.FieldBehavior, behv annotations.FieldBehavior) bool {
	for _, b := range arr {
		if b == behv {
			return true
		}
	}

	return false
}

func containStr(arr []string, str string) bool {
	for _, s := range arr {
		if s == str {
			return true
		}
	}

	return false
}

func (v *validator) resolveServiceByName(name string) *desc.ServiceDescriptor {
	for _, f := range v.files {
		if s := f.FindService(name); s != nil {
			return s
		}
	}

	return nil
}

func (v *validator) resolveMsgByLocalName(name string) *desc.MessageDescriptor {
	for _, f := range v.files {
		fqn := f.GetPackage() + "." + name

		if m := f.FindMessage(fqn); m != nil {
			return m
		}
	}

	return nil
}

func (v *validator) parseParameters(p string) error {
	for _, s := range strings.Split(p, ",") {
		if e := strings.IndexByte(s, '='); e > 0 {
			switch s[:e] {
			case "gapic-yaml":

				f, err := ioutil.ReadFile(s[e+1:])
				if err != nil {
					return fmt.Errorf("error reading gapic config: %v", err)
				}

				// throw away the first line containing
				// "type: com.google.api.codegen.ConfigProto" because
				// that's not in the proto, causing an unmarshal error
				data := bytes.NewBuffer(f)
				data.ReadString('\n')

				j, err := yaml.YAMLToJSON(data.Bytes())
				if err != nil {
					return fmt.Errorf("error decoding gapic config: %v", err)
				}

				v.gapic = &config.ConfigProto{}
				err = jsonpb.Unmarshal(bytes.NewBuffer(j), v.gapic)
				if err != nil {
					return fmt.Errorf("error decoding gapic config: %v", err)
				}
			}
		}
	}

	return nil
}

// converts snake_case and SNAKE_CASE to CamelCase.
//
// copied from github.com/googleapis/gapic-generator-go
func snakeToCamel(s string) string {
	var sb strings.Builder
	up := true
	for _, r := range s {
		if r == '_' {
			up = true
		} else if up {
			sb.WriteRune(unicode.ToUpper(r))
			up = false
		} else {
			sb.WriteRune(unicode.ToLower(r))
		}
	}
	return sb.String()
}
<commit_msg>comparison: fix use of child_type in reference comparison (#54)<commit_after>package validator

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"strings"
	"unicode"

	"github.com/ghodss/yaml"
	"github.com/golang/protobuf/jsonpb"
	"github.com/googleapis/gapic-config-validator/internal/config"
	"github.com/jhump/protoreflect/desc"
	"google.golang.org/genproto/googleapis/api/annotations"
	"google.golang.org/genproto/googleapis/longrunning"
)

var (
	wellKnownPatterns = map[string]bool{
		"projects/{project}":                      true,
		"organizations/{organization}":            true,
		"folders/{folder}":                        true,
		"projects/{project}/locations/{location}": true,
		"billingAccounts/{billing_account_id}":    true,
	}

	wellKnownNames = map[string]bool{
		"project":      true,
		"organization": true,
		"folder":       true,
		"location":     true,
	}

	isIamMethod = map[string]bool{
		"GetIamPolicy":       true,
		"SetIamPolicy":       true,
		"TestIamPermissions": true,
	}
)

func (v *validator) compare() {
	// compare interfaces
	v.compareServices()

	// compare resource references
	v.compareResourceRefs()
}

func (v *validator) compareServices() {
	for _, inter := range v.gapic.GetInterfaces() {
		serv := v.resolveServiceByName(inter.GetName())
		if serv == nil {
			v.addError("Interface %q does not exist", inter.GetName())
			continue
		}

		// compare resources
		v.compareResources(inter)

		// compare methods
		for _, method := range inter.GetMethods() {
			methodDesc := serv.FindMethodByName(method.GetName())
			if methodDesc == nil {
				v.addError("Method %q does not exist", inter.GetName()+"."+method.GetName())
				continue
			}

			v.compareMethod(methodDesc, method)
		}
	}
}

func (v *validator) compareMethod(methodDesc *desc.MethodDescriptor, method *config.MethodConfigProto) {
	fqn := methodDesc.GetFullyQualifiedName()
	mOpts := methodDesc.GetMethodOptions()

	// ignore IAM methods
	if isIamMethod[method.GetName()] {
		return
	}

	// compare method_signatures & flattening groups
	if flattenings := method.GetFlattening(); flattenings != nil {
		eSigs, err := ext(mOpts, annotations.E_MethodSignature)
		if err != nil {
			v.addError("Method %q missing method_signature(s) for flattening(s)", fqn)
			goto LRO
		}
		sigs := eSigs.([]string)

		for _, flat := range flattenings.GetGroups() {
			joined := strings.Join(flat.GetParameters(), ",")
			if !containStr(sigs, joined) {
				v.addError("Method %q missing method_signature for flattening %q", fqn, joined)
			}
		}
	}

LRO:
	// compare operation_info & longrunning config
	if lro := method.GetLongRunning(); lro != nil {
		eLRO, err := ext(mOpts, longrunning.E_OperationInfo)
		if err != nil {
			v.addError("Method %q missing longrunning.operation_info", fqn)
			goto Behavior
		}
		info := eLRO.(*longrunning.OperationInfo)

		// trim to local message name
		protoRes := info.GetResponseType()
		if strings.Contains(protoRes, ".") {
			protoRes = protoRes[strings.LastIndex(protoRes, ".")+1:]
		}
		gapicRes := lro.GetReturnType()
		if strings.Contains(gapicRes, ".") {
			gapicRes = gapicRes[strings.LastIndex(gapicRes, ".")+1:]
		}

		if protoRes != gapicRes {
			v.addError("Method %q operation_info.response_type %q does not match %q",
				fqn,
				protoRes,
				gapicRes)
		}

		// trim to local message name
		protoMeta := info.GetMetadataType()
		if strings.Contains(protoMeta, ".") {
			protoMeta = protoMeta[strings.LastIndex(protoMeta, ".")+1:]
		}
		gapicMeta := lro.GetMetadataType()
		if strings.Contains(gapicMeta, ".") {
			gapicMeta = gapicMeta[strings.LastIndex(gapicMeta, ".")+1:]
		}

		if protoMeta != gapicMeta {
			v.addError("Method %q operation_info.metadata_type %q does not match %q",
				fqn,
				protoMeta,
				gapicMeta)
		}
	}

Behavior:
	// compare input message field_behaviors & required_fields
	if reqs := method.GetRequiredFields(); len(reqs) > 0 {
		input := methodDesc.GetInputType()

		for _, name := range reqs {
			field := input.FindFieldByName(name)
			if field == nil {
				v.addError("Field %q in method %q required_fields does not exist in %q",
					name,
					fqn,
					input.GetFullyQualifiedName())
				continue
			}

			eBehv, err := ext(field.GetFieldOptions(), annotations.E_FieldBehavior)
			if err != nil {
				v.addError("Field %q is missing field_behavior = REQUIRED per required_fields config", field.GetFullyQualifiedName())
				continue
			}
			behavior := eBehv.([]annotations.FieldBehavior)

			if !containBehavior(behavior, annotations.FieldBehavior_REQUIRED) {
				v.addError("Field %q is not annotated as REQUIRED per required_fields config", field.GetFullyQualifiedName())
			}
		}
	}
}

func (v *validator) compareResources(inter *config.InterfaceConfigProto) {
	for _, res := range inter.GetCollections() {
		if wellKnownPatterns[res.GetNamePattern()] || wellKnownNames[res.GetEntityName()] {
			continue
		}

		for _, f := range v.files {
			for _, m := range f.GetMessageTypes() {
				eRes, err := ext(m.GetMessageOptions(), annotations.E_Resource)
				if err != nil {
					continue
				}
				resDesc := eRes.(*annotations.ResourceDescriptor)

				typ := resDesc.GetType()
				typ = typ[strings.Index(typ, "/")+1:]

				entName := snakeToCamel(res.GetEntityName())

				// the pattern is defined in a resource named differently than the
				// name_pattern value, which is OK.
				if containStr(resDesc.GetPattern(), res.GetNamePattern()) {
					goto Next
				}

				if typ == entName {
					if !containStr(resDesc.GetPattern(), res.GetNamePattern()) {
						v.addError("resource definition for %q in %q does not have pattern %q",
							resDesc.GetType(),
							m.GetFullyQualifiedName(),
							res.GetNamePattern())
					}

					goto Next
				}
			}
		}

		v.addError("No corresponding resource definition for %q: %q", res.GetEntityName(), res.GetNamePattern())

	Next:
	}
}

func (v *validator) compareResourceRefs() {
	for _, ref := range v.gapic.GetResourceNameGeneration() {
		// skip IAM messages
		if strings.HasPrefix(ref.GetMessageName(), "google.iam.v1") {
			continue
		}

		msgDesc := v.resolveMsgByLocalName(ref.GetMessageName())
		if msgDesc == nil {
			v.addError("Message %q in resource_name_generation item does not exist", ref.GetMessageName())
			continue
		}

		for fname, ref := range ref.GetFieldEntityMap() {
			// skip nested fields, presumably they are
			// being validated in the origial msg
			if strings.Contains(fname, ".") {
				continue
			}

			field := msgDesc.FindFieldByName(fname)
			if field == nil {
				v.addError("Field %q does not exist on message %q per resource_name_generation item", fname, msgDesc.GetFullyQualifiedName())
				continue
			}

			var typ, child string
			if eResRef, err := ext(field.GetFieldOptions(), annotations.E_ResourceReference); err == nil {
				resRef := eResRef.(*annotations.ResourceReference)

				typ = resRef.GetType()
				child = resRef.GetChildType()
			} else if eRes, err := ext(msgDesc.GetMessageOptions(), annotations.E_Resource); err == nil {
				res := eRes.(*annotations.ResourceDescriptor)
				typ = res.GetType()
			} else {
				v.addError("Field %q missing resource_reference to %q", field.GetFullyQualifiedName(), ref)
				continue
			}

			// use child_type instead
			if typ == "" {
				childMsg := v.resolveResRefMessage(child, msgDesc.GetFile())
				if childMsg == nil {
					v.addError("child_type %q on %q is not a defined resource", child, field.GetFullyQualifiedName())
					continue
				}

				refItem := v.resolveRefFromCollections(ref)
				if refItem == nil {
					v.addError("entity_name %q is not a defined in any available collection", ref)
				}

				if eResDef, err := ext(childMsg.GetMessageOptions(), annotations.E_Resource); err == nil {
					var found bool
					resDef := eResDef.(*annotations.ResourceDescriptor)
					for _, pattern := range resDef.GetPattern() {
						if strings.HasPrefix(pattern, refItem.GetNamePattern()) {
							found = true
							break
						}
					}

					if !found {
						v.addError("Field %q child_type %q isn't a proper child of %q in GAPIC config", field.GetFullyQualifiedName(), child, ref)
					}
				}

				continue
			}

			// compare using upper camel case names
			t := typ[strings.Index(typ, "/")+1:]
			if !wellKnownTypes[typ] && t != snakeToCamel(ref) {
				v.addError("Field %q resource_type_kind %q doesn't match %q in config", field.GetFullyQualifiedName(), typ, ref)
			}
		}
	}
}

func containBehavior(arr []annotations.FieldBehavior, behv annotations.FieldBehavior) bool {
	for _, b := range arr {
		if b == behv {
			return true
		}
	}

	return false
}

func containStr(arr []string, str string) bool {
	for _, s := range arr {
		if s == str {
			return true
		}
	}

	return false
}

func (v *validator) resolveServiceByName(name string) *desc.ServiceDescriptor {
	for _, f := range v.files {
		if s := f.FindService(name); s != nil {
			return s
		}
	}

	return nil
}

func (v *validator) resolveMsgByLocalName(name string) *desc.MessageDescriptor {
	for _, f := range v.files {
		fqn := f.GetPackage() + "." + name

		if m := f.FindMessage(fqn); m != nil {
			return m
		}
	}

	return nil
}

func (v *validator) resolveRefFromCollections(name string) *config.CollectionConfigProto {
	for _, item := range v.gapic.GetCollections() {
		if item.GetEntityName() == name {
			return item
		}
	}

	for _, inter := range v.gapic.GetInterfaces() {
		for _, item := range inter.GetCollections() {
			if item.GetEntityName() == name {
				return item
			}
		}
	}

	return nil
}

func (v *validator) parseParameters(p string) error {
	for _, s := range strings.Split(p, ",") {
		if e := strings.IndexByte(s, '='); e > 0 {
			switch s[:e] {
			case "gapic-yaml":

				f, err := ioutil.ReadFile(s[e+1:])
				if err != nil {
					return fmt.Errorf("error reading gapic config: %v", err)
				}

				// throw away the first line containing
				// "type: com.google.api.codegen.ConfigProto" because
				// that's not in the proto, causing an unmarshal error
				data := bytes.NewBuffer(f)
				data.ReadString('\n')

				j, err := yaml.YAMLToJSON(data.Bytes())
				if err != nil {
					return fmt.Errorf("error decoding gapic config: %v", err)
				}

				v.gapic = &config.ConfigProto{}
				err = jsonpb.Unmarshal(bytes.NewBuffer(j), v.gapic)
				if err != nil {
					return fmt.Errorf("error decoding gapic config: %v", err)
				}
			}
		}
	}

	return nil
}

// converts snake_case and SNAKE_CASE to CamelCase.
//
// copied from github.com/googleapis/gapic-generator-go
func snakeToCamel(s string) string {
	var sb strings.Builder
	up := true
	for _, r := range s {
		if r == '_' {
			up = true
		} else if up {
			sb.WriteRune(unicode.ToUpper(r))
			up = false
		} else {
			sb.WriteRune(unicode.ToLower(r))
		}
	}
	return sb.String()
}
<|endoftext|>


<commit_before>/*
	Script to handle an email response to the reminder.
	Steps:
		1. Get Sender - is it one of the registered senders in queue and are they the hosting group?
		2. Check if they should be responding or if someone is being snarky.
		3. Look for Yes/No/Skip
			3a. Yes - update the order in the group
			3b. No - send an email to the next in line, update group
			3c. Skip - respond with the current turn order for next week
	Notes: Super procedural right now.  I need to clean up the code once I have it working!
	and wow! look at the use of 3 different logging!

*/
package hostqueue

import (
	"appengine"
	"io/ioutil"
	"log"
	"net/mail"
	"net/http"
    "regexp"
    "strings"
)

func init() {

}

func IncomingMail(w http.ResponseWriter, r *http.Request) {
        //Sample from https://cloud.google.com/appengine/docs/go/mail/

        ctx := appengine.NewContext(r)
        defer r.Body.Close()

        //Get Sender - is it one of the registered senders in queue and are they the hosting group?
        m, err := mail.ReadMessage(r.Body)
		if err != nil {
			log.Fatal(err)
		}

		from := m.Header.Get("From")
		from = strings.Split(from, "<")[1]
		from = strings.Split(from, ">")[0]
		ctx.Infof("Email replied from: %s", from)

		//***** Check if they should be responding or if someone is being snarky. ************
		groups, err := GetGroups(ctx)
		if err != nil {
			log.Fatal(err)
		}

		var g Group
		for _, group := range groups {
			for _, host := range group.Hosts {
				if strings.Contains(host.Emails, from) {
					g = group
					break;
				}
				if g.GroupName != "" {  // cannot use (Group{}) because of []Hosts for some reason
					break;
				}
				continue

			}
			if g.GroupName != "" {
				break;
			}
			continue
		}

		if !strings.Contains(g.Next.Emails, from) {
			log.Fatal("Sent from the wrong person!\n  Sent from: %s, but expected: %s", from, g.Hosts[0].Emails)
		}

        //Look for Yes/No/Skip
        yes, err := regexp.Compile(`yes`)
        no, err := regexp.Compile(`no`)
        skip, err := regexp.Compile(`skip`)

        body, err := ioutil.ReadAll(m.Body)
        ctx.Infof("email body: %s", body)

        if err != nil{
			log.Fatal(err)
		}

        s := string(body)
        bodyString := strings.ToLower(s)

		//TODO:  Buggy Logic, test when working!
        if yes.MatchString(bodyString) == true {
        	// Update the order in the group
        	hosts := g.Hosts
        	currentHost := hosts[0]
        	hosts = hosts[1:]
        	hosts = append(hosts, currentHost)  //Think slices are by reference??
        	g.Next = hosts[0]
        	g.save(ctx)
        	ctx.Infof("Match Yes")
	    } else if skip.MatchString(bodyString) == true {
	    	//Respond with the current turn order for next week
	    	sendSkipMessage(g, r)
	        ctx.Infof("Match Skip")
	    } else if no.MatchString(bodyString) == true {
	    	//Send an email to the next in line
	    	hosts := g.Hosts
	    	currentIndex := SliceIndex(len(hosts), func(i int) bool { return strings.Contains(hosts[i].Emails, from) })
	    	if(currentIndex < (len(hosts) - 1)) {
	    		g.Next = hosts[currentIndex + 1]
	    	} else {
	    		g.Next = hosts[0]
	    	}

	    	g.save(ctx)
	    	sendReminder(g, r)
	    	ctx.Infof("Match No")
	    } else {
	    	ctx.Infof("Could not find yes/no/skip")
	    }
}

//http://stackoverflow.com/questions/10485743/contains-method-for-a-slice
func contains(slice []string, item string) bool {
    set := make(map[string]struct{}, len(slice))
    for _, s := range slice {
        set[s] = struct{}{}
    }

    _, ok := set[item]
    return ok
}

//http://stackoverflow.com/questions/8307478/go-how-to-find-out-element-position-in-slice
func SliceIndex(limit int, predicate func(i int) bool) int {
    for i := 0; i < limit; i++ {
        if predicate(i) {
            return i
        }
    }
    return -1
}<commit_msg>do not parse quoted reply text for yes/no/skip.<commit_after>/*
	Script to handle an email response to the reminder.
	Steps:
		1. Get Sender - is it one of the registered senders in queue and are they the hosting group?
		2. Check if they should be responding or if someone is being snarky.
		3. Look for Yes/No/Skip
			3a. Yes - update the order in the group
			3b. No - send an email to the next in line, update group
			3c. Skip - respond with the current turn order for next week
	Notes: Super procedural right now.  I need to clean up the code once I have it working!
	and wow! look at the use of 3 different logging!

*/
package hostqueue

import (
	"appengine"
	"io/ioutil"
	"log"
	"net/mail"
	"net/http"
    "regexp"
    "strings"
)

func init() {

}

func IncomingMail(w http.ResponseWriter, r *http.Request) {
        //Sample from https://cloud.google.com/appengine/docs/go/mail/

        ctx := appengine.NewContext(r)
        defer r.Body.Close()

        //Get Sender - is it one of the registered senders in queue and are they the hosting group?
        m, err := mail.ReadMessage(r.Body)
		if err != nil {
			log.Fatal(err)
		}

		from := m.Header.Get("From")
		from = strings.Split(from, "<")[1]
		from = strings.Split(from, ">")[0]
		ctx.Infof("Email replied from: %s", from)

		//***** Check if they should be responding or if someone is being snarky. ************
		groups, err := GetGroups(ctx)
		if err != nil {
			log.Fatal(err)
		}

		var g Group
		for _, group := range groups {
			for _, host := range group.Hosts {
				if strings.Contains(host.Emails, from) {
					g = group
					break;
				}
				if g.GroupName != "" {  // cannot use (Group{}) because of []Hosts for some reason
					break;
				}
				continue

			}
			if g.GroupName != "" {
				break;
			}
			continue
		}

		if !strings.Contains(g.Next.Emails, from) {
			log.Fatal("Sent from the wrong person!\n  Sent from: %s, but expected: %s", from, g.Hosts[0].Emails)
		}

        //Look for Yes/No/Skip
        yes, err := regexp.Compile(`yes`)
        no, err := regexp.Compile(`no`)
        skip, err := regexp.Compile(`skip`)

        body, err := ioutil.ReadAll(m.Body)
        ctx.Infof("email body: %s", body)

        if err != nil{
			log.Fatal(err)
		}

        s := string(body)
        bodyString := strings.ToLower(s)
        bodyString = strings.Split(bodyString, "it is your turn to host!")[0]

		//TODO:  Buggy Logic, test when working!
        if yes.MatchString(bodyString) == true {
        	// Update the order in the group
        	hosts := g.Hosts
        	currentHost := hosts[0]
        	hosts = hosts[1:]
        	hosts = append(hosts, currentHost)  //Think slices are by reference??
        	g.Next = hosts[0]
        	g.save(ctx)
        	ctx.Infof("Match Yes")
	    } else if skip.MatchString(bodyString) == true {
	    	//Respond with the current turn order for next week
	    	sendSkipMessage(g, r)
	        ctx.Infof("Match Skip")
	    } else if no.MatchString(bodyString) == true {
	    	//Send an email to the next in line
	    	hosts := g.Hosts
	    	currentIndex := SliceIndex(len(hosts), func(i int) bool { return strings.Contains(hosts[i].Emails, from) })
	    	if(currentIndex < (len(hosts) - 1)) {
	    		g.Next = hosts[currentIndex + 1]
	    	} else {
	    		g.Next = hosts[0]
	    	}

	    	g.save(ctx)
	    	sendReminder(g, r)
	    	ctx.Infof("Match No")
	    } else {
	    	ctx.Infof("Could not find yes/no/skip")
	    }
}

//http://stackoverflow.com/questions/10485743/contains-method-for-a-slice
func contains(slice []string, item string) bool {
    set := make(map[string]struct{}, len(slice))
    for _, s := range slice {
        set[s] = struct{}{}
    }

    _, ok := set[item]
    return ok
}

//http://stackoverflow.com/questions/8307478/go-how-to-find-out-element-position-in-slice
func SliceIndex(limit int, predicate func(i int) bool) int {
    for i := 0; i < limit; i++ {
        if predicate(i) {
            return i
        }
    }
    return -1
}<|endoftext|>