<commit_before>// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package os_test

import (
	"fmt"
	"io/ioutil"
	. "os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
)

func TestRemoveAll(t *testing.T) {
	tmpDir, err := ioutil.TempDir("", "TestRemoveAll-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tmpDir)

	if err := RemoveAll(""); err != nil {
		t.Errorf("RemoveAll(\"\"): %v; want nil", err)
	}

	file := filepath.Join(tmpDir, "file")
	path := filepath.Join(tmpDir, "_TestRemoveAll_")
	fpath := filepath.Join(path, "file")
	dpath := filepath.Join(path, "dir")

	// Make a regular file and remove
	fd, err := Create(file)
	if err != nil {
		t.Fatalf("create %q: %s", file, err)
	}
	fd.Close()
	if err = RemoveAll(file); err != nil {
		t.Fatalf("RemoveAll %q (first): %s", file, err)
	}
	if _, err = Lstat(file); err == nil {
		t.Fatalf("Lstat %q succeeded after RemoveAll (first)", file)
	}

	// Make directory with 1 file and remove.
	if err := MkdirAll(path, 0777); err != nil {
		t.Fatalf("MkdirAll %q: %s", path, err)
	}
	fd, err = Create(fpath)
	if err != nil {
		t.Fatalf("create %q: %s", fpath, err)
	}
	fd.Close()
	if err = RemoveAll(path); err != nil {
		t.Fatalf("RemoveAll %q (second): %s", path, err)
	}
	if _, err = Lstat(path); err == nil {
		t.Fatalf("Lstat %q succeeded after RemoveAll (second)", path)
	}

	// Make directory with file and subdirectory and remove.
	if err = MkdirAll(dpath, 0777); err != nil {
		t.Fatalf("MkdirAll %q: %s", dpath, err)
	}
	fd, err = Create(fpath)
	if err != nil {
		t.Fatalf("create %q: %s", fpath, err)
	}
	fd.Close()
	fd, err = Create(dpath + "/file")
	if err != nil {
		t.Fatalf("create %q: %s", fpath, err)
	}
	fd.Close()
	if err = RemoveAll(path); err != nil {
		t.Fatalf("RemoveAll %q (third): %s", path, err)
	}
	if _, err := Lstat(path); err == nil {
		t.Fatalf("Lstat %q succeeded after RemoveAll (third)", path)
	}

	// Determine if we should run the following test.
	testit := true
	if runtime.GOOS == "windows" {
		// Chmod is not supported under windows.
		testit = false
	} else {
		// Test fails as root.
		testit = Getuid() != 0
	}
	if testit {
		// Make directory with file and subdirectory and trigger error.
		if err = MkdirAll(dpath, 0777); err != nil {
			t.Fatalf("MkdirAll %q: %s", dpath, err)
		}

		for _, s := range []string{fpath, dpath + "/file1", path + "/zzz"} {
			fd, err = Create(s)
			if err != nil {
				t.Fatalf("create %q: %s", s, err)
			}
			fd.Close()
		}
		if err = Chmod(dpath, 0); err != nil {
			t.Fatalf("Chmod %q 0: %s", dpath, err)
		}

		// No error checking here: either RemoveAll
		// will or won't be able to remove dpath;
		// either way we want to see if it removes fpath
		// and path/zzz. Reasons why RemoveAll might
		// succeed in removing dpath as well include:
		//	* running as root
		//	* running on a file system without permissions (FAT)
		RemoveAll(path)
		Chmod(dpath, 0777)

		for _, s := range []string{fpath, path + "/zzz"} {
			if _, err = Lstat(s); err == nil {
				t.Fatalf("Lstat %q succeeded after partial RemoveAll", s)
			}
		}
	}
	if err = RemoveAll(path); err != nil {
		t.Fatalf("RemoveAll %q after partial RemoveAll: %s", path, err)
	}
	if _, err = Lstat(path); err == nil {
		t.Fatalf("Lstat %q succeeded after RemoveAll (final)", path)
	}
}

// Test RemoveAll on a large directory.
func TestRemoveAllLarge(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}

	tmpDir, err := ioutil.TempDir("", "TestRemoveAll-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tmpDir)

	path := filepath.Join(tmpDir, "_TestRemoveAllLarge_")

	// Make directory with 1000 files and remove.
	if err := MkdirAll(path, 0777); err != nil {
		t.Fatalf("MkdirAll %q: %s", path, err)
	}
	for i := 0; i < 1000; i++ {
		fpath := fmt.Sprintf("%s/file%d", path, i)
		fd, err := Create(fpath)
		if err != nil {
			t.Fatalf("create %q: %s", fpath, err)
		}
		fd.Close()
	}
	if err := RemoveAll(path); err != nil {
		t.Fatalf("RemoveAll %q: %s", path, err)
	}
	if _, err := Lstat(path); err == nil {
		t.Fatalf("Lstat %q succeeded after RemoveAll", path)
	}
}

func TestRemoveAllLongPath(t *testing.T) {
	switch runtime.GOOS {
	case "aix", "darwin", "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris":
		break
	default:
		t.Skip("skipping for not implemented platforms")
	}

	prevDir, err := Getwd()
	if err != nil {
		t.Fatalf("Could not get wd: %s", err)
	}

	startPath, err := ioutil.TempDir("", "TestRemoveAllLongPath-")
	if err != nil {
		t.Fatalf("Could not create TempDir: %s", err)
	}
	defer RemoveAll(startPath)

	err = Chdir(startPath)
	if err != nil {
		t.Fatalf("Could not chdir %s: %s", startPath, err)
	}

	// Removing paths with over 4096 chars commonly fails
	for i := 0; i < 41; i++ {
		name := strings.Repeat("a", 100)

		err = Mkdir(name, 0755)
		if err != nil {
			t.Fatalf("Could not mkdir %s: %s", name, err)
		}

		err = Chdir(name)
		if err != nil {
			t.Fatalf("Could not chdir %s: %s", name, err)
		}
	}

	err = Chdir(prevDir)
	if err != nil {
		t.Fatalf("Could not chdir %s: %s", prevDir, err)
	}

	err = RemoveAll(startPath)
	if err != nil {
		t.Errorf("RemoveAll could not remove long file path %s: %s", startPath, err)
	}
}

func TestRemoveAllDot(t *testing.T) {
	prevDir, err := Getwd()
	if err != nil {
		t.Fatalf("Could not get wd: %s", err)
	}
	tempDir, err := ioutil.TempDir("", "TestRemoveAllDot-")
	if err != nil {
		t.Fatalf("Could not create TempDir: %s", err)
	}
	defer RemoveAll(tempDir)

	err = Chdir(tempDir)
	if err != nil {
		t.Fatalf("Could not chdir to tempdir: %s", err)
	}

	err = RemoveAll(".")
	if err == nil {
		t.Errorf("RemoveAll succeed to remove .")
	}

	err = Chdir(prevDir)
	if err != nil {
		t.Fatalf("Could not chdir %s: %s", prevDir, err)
	}
}

func TestRemoveAllDotDot(t *testing.T) {
	t.Parallel()

	tempDir, err := ioutil.TempDir("", "TestRemoveAllDotDot-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tempDir)

	subdir := filepath.Join(tempDir, "x")
	subsubdir := filepath.Join(subdir, "y")
	if err := MkdirAll(subsubdir, 0777); err != nil {
		t.Fatal(err)
	}
	if err := RemoveAll(filepath.Join(subsubdir, "..")); err != nil {
		t.Error(err)
	}
	for _, dir := range []string{subsubdir, subdir} {
		if _, err := Stat(dir); err == nil {
			t.Errorf("%s: exists after RemoveAll", dir)
		}
	}
}

// Issue #29178.
func TestRemoveReadOnlyDir(t *testing.T) {
	t.Parallel()

	tempDir, err := ioutil.TempDir("", "TestRemoveReadOnlyDir-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tempDir)

	subdir := filepath.Join(tempDir, "x")
	if err := Mkdir(subdir, 0); err != nil {
		t.Fatal(err)
	}

	// If an error occurs make it more likely that removing the
	// temporary directory will succeed.
	defer Chmod(subdir, 0777)

	if err := RemoveAll(subdir); err != nil {
		t.Fatal(err)
	}

	if _, err := Stat(subdir); err == nil {
		t.Error("subdirectory was not removed")
	}
}

// Issue #29983.
func TestRemoveAllButReadOnly(t *testing.T) {
	switch runtime.GOOS {
	case "nacl", "js", "windows":
		t.Skipf("skipping test on %s", runtime.GOOS)
	}

	if Getuid() == 0 {
		t.Skip("skipping test when running as root")
	}

	t.Parallel()

	tempDir, err := ioutil.TempDir("", "TestRemoveAllButReadOnly-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tempDir)

	dirs := []string{
		"a",
		"a/x",
		"a/x/1",
		"b",
		"b/y",
		"b/y/2",
		"c",
		"c/z",
		"c/z/3",
	}
	readonly := []string{
		"b",
	}
	inReadonly := func(d string) bool {
		for _, ro := range readonly {
			if d == ro {
				return true
			}
			dd, _ := filepath.Split(d)
			if filepath.Clean(dd) == ro {
				return true
			}
		}
		return false
	}

	for _, dir := range dirs {
		if err := Mkdir(filepath.Join(tempDir, dir), 0777); err != nil {
			t.Fatal(err)
		}
	}
	for _, dir := range readonly {
		d := filepath.Join(tempDir, dir)
		if err := Chmod(d, 0555); err != nil {
			t.Fatal(err)
		}

		// Defer changing the mode back so that the deferred
		// RemoveAll(tempDir) can succeed.
		defer Chmod(d, 0777)
	}

	if err := RemoveAll(tempDir); err == nil {
		t.Fatal("RemoveAll succeeded unexpectedly")
	}

	for _, dir := range dirs {
		_, err := Stat(filepath.Join(tempDir, dir))
		if inReadonly(dir) {
			if err != nil {
				t.Errorf("file %q was deleted but should still exist", dir)
			}
		} else {
			if err == nil {
				t.Errorf("file %q still exists but should have been deleted", dir)
			}
		}
	}
}

func TestRemoveUnreadableDir(t *testing.T) {
	switch runtime.GOOS {
	case "nacl", "js", "windows":
		t.Skipf("skipping test on %s", runtime.GOOS)
	}

	if Getuid() == 0 {
		t.Skip("skipping test when running as root")
	}

	t.Parallel()

	tempDir, err := ioutil.TempDir("", "TestRemoveAllButReadOnly-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tempDir)

	target := filepath.Join(tempDir, "d0", "d1", "d2")
	if err := MkdirAll(target, 0755); err != nil {
		t.Fatal(err)
	}
	if err := Chmod(target, 0300); err != nil {
		t.Fatal(err)
	}
	if err := RemoveAll(filepath.Join(tempDir, "d0")); err != nil {
		t.Fatal(err)
	}
}
<commit_msg>os: simplify check whether to run subtest of TestRemoveAll<commit_after>// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package os_test

import (
	"fmt"
	"io/ioutil"
	. "os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
)

func TestRemoveAll(t *testing.T) {
	tmpDir, err := ioutil.TempDir("", "TestRemoveAll-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tmpDir)

	if err := RemoveAll(""); err != nil {
		t.Errorf("RemoveAll(\"\"): %v; want nil", err)
	}

	file := filepath.Join(tmpDir, "file")
	path := filepath.Join(tmpDir, "_TestRemoveAll_")
	fpath := filepath.Join(path, "file")
	dpath := filepath.Join(path, "dir")

	// Make a regular file and remove
	fd, err := Create(file)
	if err != nil {
		t.Fatalf("create %q: %s", file, err)
	}
	fd.Close()
	if err = RemoveAll(file); err != nil {
		t.Fatalf("RemoveAll %q (first): %s", file, err)
	}
	if _, err = Lstat(file); err == nil {
		t.Fatalf("Lstat %q succeeded after RemoveAll (first)", file)
	}

	// Make directory with 1 file and remove.
	if err := MkdirAll(path, 0777); err != nil {
		t.Fatalf("MkdirAll %q: %s", path, err)
	}
	fd, err = Create(fpath)
	if err != nil {
		t.Fatalf("create %q: %s", fpath, err)
	}
	fd.Close()
	if err = RemoveAll(path); err != nil {
		t.Fatalf("RemoveAll %q (second): %s", path, err)
	}
	if _, err = Lstat(path); err == nil {
		t.Fatalf("Lstat %q succeeded after RemoveAll (second)", path)
	}

	// Make directory with file and subdirectory and remove.
	if err = MkdirAll(dpath, 0777); err != nil {
		t.Fatalf("MkdirAll %q: %s", dpath, err)
	}
	fd, err = Create(fpath)
	if err != nil {
		t.Fatalf("create %q: %s", fpath, err)
	}
	fd.Close()
	fd, err = Create(dpath + "/file")
	if err != nil {
		t.Fatalf("create %q: %s", fpath, err)
	}
	fd.Close()
	if err = RemoveAll(path); err != nil {
		t.Fatalf("RemoveAll %q (third): %s", path, err)
	}
	if _, err := Lstat(path); err == nil {
		t.Fatalf("Lstat %q succeeded after RemoveAll (third)", path)
	}

	// Chmod is not supported under Windows and test fails as root.
	if runtime.GOOS != "windows" && Getuid() != 0 {
		// Make directory with file and subdirectory and trigger error.
		if err = MkdirAll(dpath, 0777); err != nil {
			t.Fatalf("MkdirAll %q: %s", dpath, err)
		}

		for _, s := range []string{fpath, dpath + "/file1", path + "/zzz"} {
			fd, err = Create(s)
			if err != nil {
				t.Fatalf("create %q: %s", s, err)
			}
			fd.Close()
		}
		if err = Chmod(dpath, 0); err != nil {
			t.Fatalf("Chmod %q 0: %s", dpath, err)
		}

		// No error checking here: either RemoveAll
		// will or won't be able to remove dpath;
		// either way we want to see if it removes fpath
		// and path/zzz. Reasons why RemoveAll might
		// succeed in removing dpath as well include:
		//	* running as root
		//	* running on a file system without permissions (FAT)
		RemoveAll(path)
		Chmod(dpath, 0777)

		for _, s := range []string{fpath, path + "/zzz"} {
			if _, err = Lstat(s); err == nil {
				t.Fatalf("Lstat %q succeeded after partial RemoveAll", s)
			}
		}
	}
	if err = RemoveAll(path); err != nil {
		t.Fatalf("RemoveAll %q after partial RemoveAll: %s", path, err)
	}
	if _, err = Lstat(path); err == nil {
		t.Fatalf("Lstat %q succeeded after RemoveAll (final)", path)
	}
}

// Test RemoveAll on a large directory.
func TestRemoveAllLarge(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping in short mode")
	}

	tmpDir, err := ioutil.TempDir("", "TestRemoveAll-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tmpDir)

	path := filepath.Join(tmpDir, "_TestRemoveAllLarge_")

	// Make directory with 1000 files and remove.
	if err := MkdirAll(path, 0777); err != nil {
		t.Fatalf("MkdirAll %q: %s", path, err)
	}
	for i := 0; i < 1000; i++ {
		fpath := fmt.Sprintf("%s/file%d", path, i)
		fd, err := Create(fpath)
		if err != nil {
			t.Fatalf("create %q: %s", fpath, err)
		}
		fd.Close()
	}
	if err := RemoveAll(path); err != nil {
		t.Fatalf("RemoveAll %q: %s", path, err)
	}
	if _, err := Lstat(path); err == nil {
		t.Fatalf("Lstat %q succeeded after RemoveAll", path)
	}
}

func TestRemoveAllLongPath(t *testing.T) {
	switch runtime.GOOS {
	case "aix", "darwin", "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris":
		break
	default:
		t.Skip("skipping for not implemented platforms")
	}

	prevDir, err := Getwd()
	if err != nil {
		t.Fatalf("Could not get wd: %s", err)
	}

	startPath, err := ioutil.TempDir("", "TestRemoveAllLongPath-")
	if err != nil {
		t.Fatalf("Could not create TempDir: %s", err)
	}
	defer RemoveAll(startPath)

	err = Chdir(startPath)
	if err != nil {
		t.Fatalf("Could not chdir %s: %s", startPath, err)
	}

	// Removing paths with over 4096 chars commonly fails
	for i := 0; i < 41; i++ {
		name := strings.Repeat("a", 100)

		err = Mkdir(name, 0755)
		if err != nil {
			t.Fatalf("Could not mkdir %s: %s", name, err)
		}

		err = Chdir(name)
		if err != nil {
			t.Fatalf("Could not chdir %s: %s", name, err)
		}
	}

	err = Chdir(prevDir)
	if err != nil {
		t.Fatalf("Could not chdir %s: %s", prevDir, err)
	}

	err = RemoveAll(startPath)
	if err != nil {
		t.Errorf("RemoveAll could not remove long file path %s: %s", startPath, err)
	}
}

func TestRemoveAllDot(t *testing.T) {
	prevDir, err := Getwd()
	if err != nil {
		t.Fatalf("Could not get wd: %s", err)
	}
	tempDir, err := ioutil.TempDir("", "TestRemoveAllDot-")
	if err != nil {
		t.Fatalf("Could not create TempDir: %s", err)
	}
	defer RemoveAll(tempDir)

	err = Chdir(tempDir)
	if err != nil {
		t.Fatalf("Could not chdir to tempdir: %s", err)
	}

	err = RemoveAll(".")
	if err == nil {
		t.Errorf("RemoveAll succeed to remove .")
	}

	err = Chdir(prevDir)
	if err != nil {
		t.Fatalf("Could not chdir %s: %s", prevDir, err)
	}
}

func TestRemoveAllDotDot(t *testing.T) {
	t.Parallel()

	tempDir, err := ioutil.TempDir("", "TestRemoveAllDotDot-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tempDir)

	subdir := filepath.Join(tempDir, "x")
	subsubdir := filepath.Join(subdir, "y")
	if err := MkdirAll(subsubdir, 0777); err != nil {
		t.Fatal(err)
	}
	if err := RemoveAll(filepath.Join(subsubdir, "..")); err != nil {
		t.Error(err)
	}
	for _, dir := range []string{subsubdir, subdir} {
		if _, err := Stat(dir); err == nil {
			t.Errorf("%s: exists after RemoveAll", dir)
		}
	}
}

// Issue #29178.
func TestRemoveReadOnlyDir(t *testing.T) {
	t.Parallel()

	tempDir, err := ioutil.TempDir("", "TestRemoveReadOnlyDir-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tempDir)

	subdir := filepath.Join(tempDir, "x")
	if err := Mkdir(subdir, 0); err != nil {
		t.Fatal(err)
	}

	// If an error occurs make it more likely that removing the
	// temporary directory will succeed.
	defer Chmod(subdir, 0777)

	if err := RemoveAll(subdir); err != nil {
		t.Fatal(err)
	}

	if _, err := Stat(subdir); err == nil {
		t.Error("subdirectory was not removed")
	}
}

// Issue #29983.
func TestRemoveAllButReadOnly(t *testing.T) {
	switch runtime.GOOS {
	case "nacl", "js", "windows":
		t.Skipf("skipping test on %s", runtime.GOOS)
	}

	if Getuid() == 0 {
		t.Skip("skipping test when running as root")
	}

	t.Parallel()

	tempDir, err := ioutil.TempDir("", "TestRemoveAllButReadOnly-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tempDir)

	dirs := []string{
		"a",
		"a/x",
		"a/x/1",
		"b",
		"b/y",
		"b/y/2",
		"c",
		"c/z",
		"c/z/3",
	}
	readonly := []string{
		"b",
	}
	inReadonly := func(d string) bool {
		for _, ro := range readonly {
			if d == ro {
				return true
			}
			dd, _ := filepath.Split(d)
			if filepath.Clean(dd) == ro {
				return true
			}
		}
		return false
	}

	for _, dir := range dirs {
		if err := Mkdir(filepath.Join(tempDir, dir), 0777); err != nil {
			t.Fatal(err)
		}
	}
	for _, dir := range readonly {
		d := filepath.Join(tempDir, dir)
		if err := Chmod(d, 0555); err != nil {
			t.Fatal(err)
		}

		// Defer changing the mode back so that the deferred
		// RemoveAll(tempDir) can succeed.
		defer Chmod(d, 0777)
	}

	if err := RemoveAll(tempDir); err == nil {
		t.Fatal("RemoveAll succeeded unexpectedly")
	}

	for _, dir := range dirs {
		_, err := Stat(filepath.Join(tempDir, dir))
		if inReadonly(dir) {
			if err != nil {
				t.Errorf("file %q was deleted but should still exist", dir)
			}
		} else {
			if err == nil {
				t.Errorf("file %q still exists but should have been deleted", dir)
			}
		}
	}
}

func TestRemoveUnreadableDir(t *testing.T) {
	switch runtime.GOOS {
	case "nacl", "js", "windows":
		t.Skipf("skipping test on %s", runtime.GOOS)
	}

	if Getuid() == 0 {
		t.Skip("skipping test when running as root")
	}

	t.Parallel()

	tempDir, err := ioutil.TempDir("", "TestRemoveAllButReadOnly-")
	if err != nil {
		t.Fatal(err)
	}
	defer RemoveAll(tempDir)

	target := filepath.Join(tempDir, "d0", "d1", "d2")
	if err := MkdirAll(target, 0755); err != nil {
		t.Fatal(err)
	}
	if err := Chmod(target, 0300); err != nil {
		t.Fatal(err)
	}
	if err := RemoveAll(filepath.Join(tempDir, "d0")); err != nil {
		t.Fatal(err)
	}
}
<|endoftext|>


<commit_before>package bulklrpstatus

import (
	"encoding/json"
	"net/http"
	"regexp"
	"strings"
	"sync"

	"code.cloudfoundry.org/bbs"
	"code.cloudfoundry.org/bbs/models"
	"code.cloudfoundry.org/clock"
	"code.cloudfoundry.org/lager"
	"code.cloudfoundry.org/runtimeschema/cc_messages"
	"code.cloudfoundry.org/tps/handler/lrpstatus"
	"github.com/cloudfoundry/gunk/workpool"
)

var processGuidPattern = regexp.MustCompile(`^([a-zA-Z0-9_-]+,)*[a-zA-Z0-9_-]+$`)

type handler struct {
	bbsClient                 bbs.Client
	clock                     clock.Clock
	logger                    lager.Logger
	bulkLRPStatusWorkPoolSize int
}

func NewHandler(bbsClient bbs.Client, clk clock.Clock, bulkLRPStatusWorkPoolSize int, logger lager.Logger) http.Handler {
	return &handler{
		bbsClient: bbsClient,
		clock:     clk,
		bulkLRPStatusWorkPoolSize: bulkLRPStatusWorkPoolSize,
		logger: logger,
	}
}

func (handler *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	logger := handler.logger.Session("bulk-lrp-status")

	guidParameter := r.FormValue("guids")
	if !processGuidPattern.Match([]byte(guidParameter)) {
		logger.Error("failed-parsing-guids", nil, lager.Data{"guid-parameter": guidParameter})
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	guids := strings.Split(guidParameter, ",")
	works := []func(){}

	statusBundle := make(map[string][]cc_messages.LRPInstance)
	statusLock := sync.Mutex{}

	for _, processGuid := range guids {
		works = append(works, handler.getStatusForLRPWorkFunction(logger, processGuid, &statusLock, statusBundle))
	}

	throttler, err := workpool.NewThrottler(handler.bulkLRPStatusWorkPoolSize, works)
	if err != nil {
		logger.Error("failed-constructing-throttler", err, lager.Data{"max-workers": handler.bulkLRPStatusWorkPoolSize, "num-works": len(works)})
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	throttler.Work()

	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-Type", "application/json")

	err = json.NewEncoder(w).Encode(statusBundle)
	if err != nil {
		logger.Error("stream-response-failed", err, nil)
	}
}

func (handler *handler) getStatusForLRPWorkFunction(logger lager.Logger, processGuid string, statusLock *sync.Mutex, statusBundle map[string][]cc_messages.LRPInstance) func() {
	return func() {
		logger = logger.Session("fetching-actual-lrps-info", lager.Data{"process-guid": processGuid})
		logger.Info("start")
		defer logger.Info("complete")
		actualLRPGroups, err := handler.bbsClient.ActualLRPGroupsByProcessGuid(logger, processGuid)
		if err != nil {
			logger.Error("fetching-actual-lrps-info-failed", err)
			return
		}

		instances := lrpstatus.LRPInstances(actualLRPGroups,
			func(instance *cc_messages.LRPInstance, actual *models.ActualLRP) {
				instance.Details = actual.PlacementError
			},
			handler.clock,
		)

		statusLock.Lock()
		statusBundle[processGuid] = instances
		statusLock.Unlock()
	}
}
<commit_msg>import workpool from code.cloudfoundry.org<commit_after>package bulklrpstatus

import (
	"encoding/json"
	"net/http"
	"regexp"
	"strings"
	"sync"

	"code.cloudfoundry.org/bbs"
	"code.cloudfoundry.org/bbs/models"
	"code.cloudfoundry.org/clock"
	"code.cloudfoundry.org/lager"
	"code.cloudfoundry.org/runtimeschema/cc_messages"
	"code.cloudfoundry.org/tps/handler/lrpstatus"
	"code.cloudfoundry.org/workpool"
)

var processGuidPattern = regexp.MustCompile(`^([a-zA-Z0-9_-]+,)*[a-zA-Z0-9_-]+$`)

type handler struct {
	bbsClient                 bbs.Client
	clock                     clock.Clock
	logger                    lager.Logger
	bulkLRPStatusWorkPoolSize int
}

func NewHandler(bbsClient bbs.Client, clk clock.Clock, bulkLRPStatusWorkPoolSize int, logger lager.Logger) http.Handler {
	return &handler{
		bbsClient: bbsClient,
		clock:     clk,
		bulkLRPStatusWorkPoolSize: bulkLRPStatusWorkPoolSize,
		logger: logger,
	}
}

func (handler *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	logger := handler.logger.Session("bulk-lrp-status")

	guidParameter := r.FormValue("guids")
	if !processGuidPattern.Match([]byte(guidParameter)) {
		logger.Error("failed-parsing-guids", nil, lager.Data{"guid-parameter": guidParameter})
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	guids := strings.Split(guidParameter, ",")
	works := []func(){}

	statusBundle := make(map[string][]cc_messages.LRPInstance)
	statusLock := sync.Mutex{}

	for _, processGuid := range guids {
		works = append(works, handler.getStatusForLRPWorkFunction(logger, processGuid, &statusLock, statusBundle))
	}

	throttler, err := workpool.NewThrottler(handler.bulkLRPStatusWorkPoolSize, works)
	if err != nil {
		logger.Error("failed-constructing-throttler", err, lager.Data{"max-workers": handler.bulkLRPStatusWorkPoolSize, "num-works": len(works)})
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	throttler.Work()

	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-Type", "application/json")

	err = json.NewEncoder(w).Encode(statusBundle)
	if err != nil {
		logger.Error("stream-response-failed", err, nil)
	}
}

func (handler *handler) getStatusForLRPWorkFunction(logger lager.Logger, processGuid string, statusLock *sync.Mutex, statusBundle map[string][]cc_messages.LRPInstance) func() {
	return func() {
		logger = logger.Session("fetching-actual-lrps-info", lager.Data{"process-guid": processGuid})
		logger.Info("start")
		defer logger.Info("complete")
		actualLRPGroups, err := handler.bbsClient.ActualLRPGroupsByProcessGuid(logger, processGuid)
		if err != nil {
			logger.Error("fetching-actual-lrps-info-failed", err)
			return
		}

		instances := lrpstatus.LRPInstances(actualLRPGroups,
			func(instance *cc_messages.LRPInstance, actual *models.ActualLRP) {
				instance.Details = actual.PlacementError
			},
			handler.clock,
		)

		statusLock.Lock()
		statusBundle[processGuid] = instances
		statusLock.Unlock()
	}
}
<|endoftext|>


<commit_before>// Copyright (c) 2017 Cisco and/or its affiliates.
//
// 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 probe

import (
	"net/http"

	"github.com/ligato/cn-infra/health/statuscheck/model/status"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/unrolled/render"
)

const (
	defaultPluginName string = "PROMETHEUS"

	// DefaultMetricsPath default Prometheus metrics URL
	DefaultMetricsPath string = "/metrics"
	// DefaultHealthPath default Prometheus health metrics URL
	DefaultHealthPath string = "/health"

	// Namespace namespace to use for Prometheus health metrics
	Namespace string = ""
	// Subsystem subsystem to use for Prometheus health metrics
	Subsystem string = ""
	// ServiceLabel label for service field
	ServiceLabel string = "service"
	// DependencyLabel label for dependency field
	DependencyLabel string = "dependency"
	// BuildVersionLabel label for build version field
	BuildVersionLabel string = "build_version"
	// BuildDateLabel label for build date field
	BuildDateLabel string = "build_date"

	// ServiceHealthName name of service health metric
	ServiceHealthName string = "service_health"

	// ServiceHealthHelp help text for service health metric
	// Adapt Ligato status code for now.
	// TODO: Consolidate with that from the "Common Container Telemetry" proposal.
	// ServiceHealthHelp    string = "The health of the ServiceLabel 0 = INIT, 1 = UP, 2 = DOWN, 3 = OUTAGE"
	ServiceHealthHelp string = "The health of the ServiceLabel 0 = INIT, 1 = OK, 2 = ERROR"

	// DependencyHealthName name of dependency health metric
	DependencyHealthName string = "service_dependency_health"

	// DependencyHealthHelp help text for dependency health metric
	// Adapt Ligato status code for now.
	// TODO: Consolidate with that from the "Common Container Telemetry" proposal.
	// DependencyHealthHelp string = "The health of the DependencyLabel 0 = INIT, 1 = UP, 2 = DOWN, 3 = OUTAGE"
	DependencyHealthHelp string = "The health of the DependencyLabel 0 = INIT, 1 = OK, 2 = ERROR"

	// ServiceInfoName name of service info metric
	ServiceInfoName string = "service_info"
	// ServiceInfoHelp help text for service info metric
	ServiceInfoHelp string = "Build info for the service.  Value is always 1, build info is in the tags."
)

// PrometheusPlugin struct holds all plugin-related data.
type PrometheusPlugin struct {
	Deps
	healthRegistry *prometheus.Registry
}

// Init may create a new (custom) instance of HTTP if the injected instance uses
// different HTTP port than requested.
func (p *PrometheusPlugin) Init() (err error) {

	p.healthRegistry = prometheus.NewRegistry()

	p.registerGauge(
		Namespace,
		Subsystem,
		ServiceHealthName,
		ServiceHealthHelp,
		prometheus.Labels{ServiceLabel: p.getServiceLabel()},
		p.getServiceHealth,
	)

	agentStatus := p.StatusCheck.GetAgentStatus()
	p.registerGauge(
		Namespace,
		Subsystem,
		ServiceInfoName,
		ServiceInfoHelp,
		prometheus.Labels{
			ServiceLabel:      p.getServiceLabel(),
			BuildVersionLabel: agentStatus.BuildVersion,
			BuildDateLabel:    agentStatus.BuildDate},
		func() float64 { return 1 },
	)

	return nil
}

// AfterInit registers HTTP handlers.
func (p *PrometheusPlugin) AfterInit() error {
	if p.HTTP != nil {
		if p.StatusCheck != nil {
			p.Log.Info("Starting Prometheus metrics handlers")
			p.HTTP.RegisterHTTPHandler(DefaultMetricsPath, p.metricsHandler, "GET")
			p.HTTP.RegisterHTTPHandler(DefaultHealthPath, p.healthMetricsHandler, "GET")
			p.Log.Infof("Serving %s on port %d", DefaultMetricsPath, p.HTTP.GetPort())
			p.Log.Infof("Serving %s on port %d", DefaultHealthPath, p.HTTP.GetPort())
		} else {
			p.Log.Info("Unable to register Prometheus metrics handlers, StatusCheck is nil")
		}
	} else {
		p.Log.Info("Unable to register Prometheus metrics handlers, HTTP is nil")
	}

	//TODO: Need improvement - instead of the exposing the map directly need to use in-memory mapping
	if p.PluginStatusCheck != nil {
		allPluginStatusMap := p.PluginStatusCheck.GetAllPluginStatus()
		for k, v := range allPluginStatusMap {
			p.Log.Infof("k=%v, v=%v, state=%v", k, v, v.State)
			p.registerGauge(
				Namespace,
				Subsystem,
				DependencyHealthName,
				DependencyHealthHelp,
				prometheus.Labels{
					ServiceLabel:    p.getServiceLabel(),
					DependencyLabel: k,
				},
				p.getDependencyHealth(k, v),
			)
		}
	} else {
		p.Log.Error("PluginStatusCheck is nil")
	}

	/*if p.PluginStatusCheck != nil {
		if p.PluginStatusCheck.GetPluginStatusMap() != nil {
			pluginStatusIdx := p.PluginStatusCheck.GetPluginStatusMap()
			allPluginNames := pluginStatusIdx.GetMapping().ListAllNames()
			for _, v := range allPluginNames {
				p.registerGauge(
					Namespace,
					Subsystem,
					DependencyHealthName,
					DependencyHealthHelp,
					prometheus.Labels{
						ServiceLabel:    agentName,
						DependencyLabel: v,
					},
					func() float64 {
						p.Log.Infof("DependencyHealth for Plugin %v", v)
						pluginStatus, ok := pluginStatusIdx.GetValue(v)
						if ok {
							p.Log.Infof("DependencyHealth: %v", float64(pluginStatus.State))
							return float64(pluginStatus.State)
						} else {
							p.Log.Info("DependencyHealth not found")
							return float64(-1)
						}
					},
				)
			}
		} else {
			p.Log.Error("Plugin map is nil")
		}
	} else {
		p.Log.Error("PluginStatusCheck is nil")
	}*/

	return nil
}

// Close shutdowns HTTP if a custom instance was created in Init().
func (p *PrometheusPlugin) Close() error {
	return nil
}

// metricsHandler handles Prometheus metrics collection.
func (p *PrometheusPlugin) metricsHandler(formatter *render.Render) http.HandlerFunc {
	return promhttp.Handler().ServeHTTP
}

// healthMetricsHandler handles custom health metrics for Prometheus.
func (p *PrometheusPlugin) healthMetricsHandler(formatter *render.Render) http.HandlerFunc {
	return promhttp.HandlerFor(p.healthRegistry, promhttp.HandlerOpts{}).ServeHTTP
}

// getServiceHealth returns agent health status
func (p *PrometheusPlugin) getServiceHealth() float64 {
	agentStatus := p.StatusCheck.GetAgentStatus()
	// Adapt Ligato status code for now.
	// TODO: Consolidate with that from the "Common Container Telemetry" proposal.
	health := float64(agentStatus.State)
	p.Log.Infof("ServiceHealth: %v", health)
	return health
}

// getDependencyHealth returns plugin health status
func (p *PrometheusPlugin) getDependencyHealth(pluginName string, pluginStatus *status.PluginStatus) func() float64 {
	p.Log.Infof("DependencyHealth for plugin %v: %v", pluginName, float64(pluginStatus.State))

	return func() float64 {
		health := float64(pluginStatus.State)
		depName := pluginName
		p.Log.Infof("Dependency Health %v: %v", depName, health)
		return health
	}
}

// registerGauge registers custom gauge with specific valueFunc to report status when invoked.
func (p *PrometheusPlugin) registerGauge(namespace string, subsystem string, name string, help string,
	labels prometheus.Labels, valueFunc func() float64) {
	gaugeName := name
	if subsystem != "" {
		gaugeName = subsystem + "_" + gaugeName
	}
	if namespace != "" {
		gaugeName = namespace + "_" + gaugeName
	}
	if err := p.healthRegistry.Register(prometheus.NewGaugeFunc(
		prometheus.GaugeOpts{
			// Namespace, Subsystem, and Name are components of the fully-qualified
			// name of the Metric (created by joining these components with
			// "_"). Only Name is mandatory, the others merely help structuring the
			// name. Note that the fully-qualified name of the metric must be a
			// valid Prometheus metric name.
			Namespace: namespace,
			Subsystem: subsystem,
			Name:      name,

			// Help provides information about this metric. Mandatory!
			//
			// Metrics with the same fully-qualified name must have the same Help
			// string.
			Help: help,

			// ConstLabels are used to attach fixed labels to this metric. Metrics
			// with the same fully-qualified name must have the same label names in
			// their ConstLabels.
			//
			// Note that in most cases, labels have a value that varies during the
			// lifetime of a process. Those labels are usually managed with a metric
			// vector collector (like CounterVec, GaugeVec, UntypedVec). ConstLabels
			// serve only special purposes. One is for the special case where the
			// value of a label does not change during the lifetime of a process,
			// e.g. if the revision of the running binary is put into a
			// label. Another, more advanced purpose is if more than one Collector
			// needs to collect Metrics with the same fully-qualified name. In that
			// case, those Metrics must differ in the values of their
			// ConstLabels. See the Collector examples.
			//
			// If the value of a label never changes (not even between binaries),
			// that label most likely should not be a label at all (but part of the
			// metric name).
			ConstLabels: labels,
		},
		valueFunc,
	)); err == nil {
		p.Log.Infof("GaugeFunc('%s') registered.", gaugeName)
	} else {
		p.Log.Errorf("GaugeFunc('%s') registration failed: %s", gaugeName, err)
	}
}

// String returns plugin name if it was injected, defaultPluginName otherwise.
func (p *PrometheusPlugin) String() string {
	if len(string(p.PluginName)) > 0 {
		return string(p.PluginName)
	}
	return defaultPluginName
}

func (p *PrometheusPlugin) getServiceLabel() string {
	serviceLabel := p.String()
	if p.Deps.ServiceLabel != nil {
		serviceLabel = p.Deps.ServiceLabel.GetAgentLabel()
	}
	return serviceLabel
}
<commit_msg>SPOPT-1621 - fixing merge conflict removed unit tests for OR predicate since not valid for Cassandra<commit_after>// Copyright (c) 2017 Cisco and/or its affiliates.
//
// 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 probe

import (
	"net/http"

	"github.com/ligato/cn-infra/health/statuscheck/model/status"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/unrolled/render"
)

const (
	defaultPluginName string = "PROMETHEUS"

	// DefaultMetricsPath default Prometheus metrics URL
	DefaultMetricsPath string = "/metrics"
	// DefaultHealthPath default Prometheus health metrics URL
	DefaultHealthPath string = "/health"

	// Namespace namespace to use for Prometheus health metrics
	Namespace string = ""
	// Subsystem subsystem to use for Prometheus health metrics
	Subsystem string = ""
	// ServiceLabel label for service field
	ServiceLabel string = "service"
	// DependencyLabel label for dependency field
	DependencyLabel string = "dependency"
	// BuildVersionLabel label for build version field
	BuildVersionLabel string = "build_version"
	// BuildDateLabel label for build date field
	BuildDateLabel string = "build_date"

	// ServiceHealthName name of service health metric
	ServiceHealthName string = "service_health"

	// ServiceHealthHelp help text for service health metric
	// Adapt Ligato status code for now.
	// TODO: Consolidate with that from the "Common Container Telemetry" proposal.
	// ServiceHealthHelp    string = "The health of the ServiceLabel 0 = INIT, 1 = UP, 2 = DOWN, 3 = OUTAGE"
	ServiceHealthHelp string = "The health of the ServiceLabel 0 = INIT, 1 = OK, 2 = ERROR"

	// DependencyHealthName name of dependency health metric
	DependencyHealthName string = "service_dependency_health"

	// DependencyHealthHelp help text for dependency health metric
	// Adapt Ligato status code for now.
	// TODO: Consolidate with that from the "Common Container Telemetry" proposal.
	// DependencyHealthHelp string = "The health of the DependencyLabel 0 = INIT, 1 = UP, 2 = DOWN, 3 = OUTAGE"
	DependencyHealthHelp string = "The health of the DependencyLabel 0 = INIT, 1 = OK, 2 = ERROR"

	// ServiceInfoName name of service info metric
	ServiceInfoName string = "service_info"
	// ServiceInfoHelp help text for service info metric
	ServiceInfoHelp string = "Build info for the service.  Value is always 1, build info is in the tags."
)

// PrometheusPlugin struct holds all plugin-related data.
type PrometheusPlugin struct {
	Deps
	healthRegistry *prometheus.Registry
}

// Init may create a new (custom) instance of HTTP if the injected instance uses
// different HTTP port than requested.
func (p *PrometheusPlugin) Init() (err error) {
	serviceLabel := p.String()
	if p.Deps.ServiceLabel != nil {
		serviceLabel = p.Deps.ServiceLabel.GetAgentLabel()
	}

	p.healthRegistry = prometheus.NewRegistry()

	p.registerGauge(
		Namespace,
		Subsystem,
		ServiceHealthName,
		ServiceHealthHelp,
		prometheus.Labels{ServiceLabel: serviceLabel},
		p.getServiceHealth,
	)

	agentStatus := p.StatusCheck.GetAgentStatus()
	p.registerGauge(
		Namespace,
		Subsystem,
		ServiceInfoName,
		ServiceInfoHelp,
		prometheus.Labels{
			ServiceLabel:      serviceLabel,
			BuildVersionLabel: agentStatus.BuildVersion,
			BuildDateLabel:    agentStatus.BuildDate},
		func() float64 { return 1 },
	)

	return nil
}

// AfterInit registers HTTP handlers.
func (p *PrometheusPlugin) AfterInit() error {
	if p.HTTP != nil {
		if p.StatusCheck != nil {
			p.Log.Info("Starting Prometheus metrics handlers")
			p.HTTP.RegisterHTTPHandler(DefaultMetricsPath, p.metricsHandler, "GET")
			p.HTTP.RegisterHTTPHandler(DefaultHealthPath, p.healthMetricsHandler, "GET")
			p.Log.Infof("Serving %s on port %d", DefaultMetricsPath, p.HTTP.GetPort())
			p.Log.Infof("Serving %s on port %d", DefaultHealthPath, p.HTTP.GetPort())
		} else {
			p.Log.Info("Unable to register Prometheus metrics handlers, StatusCheck is nil")
		}
	} else {
		p.Log.Info("Unable to register Prometheus metrics handlers, HTTP is nil")
	}

	//TODO: Need improvement - instead of the exposing the map directly need to use in-memory mapping
	if p.PluginStatusCheck != nil {
		allPluginStatusMap := p.PluginStatusCheck.GetAllPluginStatus()
		for k, v := range allPluginStatusMap {
			p.Log.Infof("k=%v, v=%v, state=%v", k, v, v.State)
			p.registerGauge(
				Namespace,
				Subsystem,
				DependencyHealthName,
				DependencyHealthHelp,
				prometheus.Labels{
					ServiceLabel:    p.getServiceLabel(),
					DependencyLabel: k,
				},
				p.getDependencyHealth(k, v),
			)
		}
	} else {
		p.Log.Error("PluginStatusCheck is nil")
	}

	/*if p.PluginStatusCheck != nil {
		if p.PluginStatusCheck.GetPluginStatusMap() != nil {
			pluginStatusIdx := p.PluginStatusCheck.GetPluginStatusMap()
			allPluginNames := pluginStatusIdx.GetMapping().ListAllNames()
			for _, v := range allPluginNames {
				p.registerGauge(
					Namespace,
					Subsystem,
					DependencyHealthName,
					DependencyHealthHelp,
					prometheus.Labels{
						ServiceLabel:    agentName,
						DependencyLabel: v,
					},
					func() float64 {
						p.Log.Infof("DependencyHealth for Plugin %v", v)
						pluginStatus, ok := pluginStatusIdx.GetValue(v)
						if ok {
							p.Log.Infof("DependencyHealth: %v", float64(pluginStatus.State))
							return float64(pluginStatus.State)
						} else {
							p.Log.Info("DependencyHealth not found")
							return float64(-1)
						}
					},
				)
			}
		} else {
			p.Log.Error("Plugin map is nil")
		}
	} else {
		p.Log.Error("PluginStatusCheck is nil")
	}*/

	return nil
}

// Close shutdowns HTTP if a custom instance was created in Init().
func (p *PrometheusPlugin) Close() error {
	return nil
}

// metricsHandler handles Prometheus metrics collection.
func (p *PrometheusPlugin) metricsHandler(formatter *render.Render) http.HandlerFunc {
	return promhttp.Handler().ServeHTTP
}

// healthMetricsHandler handles custom health metrics for Prometheus.
func (p *PrometheusPlugin) healthMetricsHandler(formatter *render.Render) http.HandlerFunc {
	return promhttp.HandlerFor(p.healthRegistry, promhttp.HandlerOpts{}).ServeHTTP
}

// getServiceHealth returns agent health status
func (p *PrometheusPlugin) getServiceHealth() float64 {
	agentStatus := p.StatusCheck.GetAgentStatus()
	// Adapt Ligato status code for now.
	// TODO: Consolidate with that from the "Common Container Telemetry" proposal.
	health := float64(agentStatus.State)
	p.Log.Infof("ServiceHealth: %v", health)
	return health
}

// getDependencyHealth returns plugin health status
func (p *PrometheusPlugin) getDependencyHealth(pluginName string, pluginStatus *status.PluginStatus) func() float64 {
	p.Log.Infof("DependencyHealth for plugin %v: %v", pluginName, float64(pluginStatus.State))

	return func() float64 {
		health := float64(pluginStatus.State)
		depName := pluginName
		p.Log.Infof("Dependency Health %v: %v", depName, health)
		return health
	}
}

// registerGauge registers custom gauge with specific valueFunc to report status when invoked.
func (p *PrometheusPlugin) registerGauge(namespace string, subsystem string, name string, help string,
	labels prometheus.Labels, valueFunc func() float64) {
	gaugeName := name
	if subsystem != "" {
		gaugeName = subsystem + "_" + gaugeName
	}
	if namespace != "" {
		gaugeName = namespace + "_" + gaugeName
	}
	if err := p.healthRegistry.Register(prometheus.NewGaugeFunc(
		prometheus.GaugeOpts{
			// Namespace, Subsystem, and Name are components of the fully-qualified
			// name of the Metric (created by joining these components with
			// "_"). Only Name is mandatory, the others merely help structuring the
			// name. Note that the fully-qualified name of the metric must be a
			// valid Prometheus metric name.
			Namespace: namespace,
			Subsystem: subsystem,
			Name:      name,

			// Help provides information about this metric. Mandatory!
			//
			// Metrics with the same fully-qualified name must have the same Help
			// string.
			Help: help,

			// ConstLabels are used to attach fixed labels to this metric. Metrics
			// with the same fully-qualified name must have the same label names in
			// their ConstLabels.
			//
			// Note that in most cases, labels have a value that varies during the
			// lifetime of a process. Those labels are usually managed with a metric
			// vector collector (like CounterVec, GaugeVec, UntypedVec). ConstLabels
			// serve only special purposes. One is for the special case where the
			// value of a label does not change during the lifetime of a process,
			// e.g. if the revision of the running binary is put into a
			// label. Another, more advanced purpose is if more than one Collector
			// needs to collect Metrics with the same fully-qualified name. In that
			// case, those Metrics must differ in the values of their
			// ConstLabels. See the Collector examples.
			//
			// If the value of a label never changes (not even between binaries),
			// that label most likely should not be a label at all (but part of the
			// metric name).
			ConstLabels: labels,
		},
		valueFunc,
	)); err == nil {
		p.Log.Infof("GaugeFunc('%s') registered.", gaugeName)
	} else {
		p.Log.Errorf("GaugeFunc('%s') registration failed: %s", gaugeName, err)
	}
}

// String returns plugin name if it was injected, defaultPluginName otherwise.
func (p *PrometheusPlugin) String() string {
	if len(string(p.PluginName)) > 0 {
		return string(p.PluginName)
	}
	return defaultPluginName
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"time"

	"github.com/lowstz/slackhookgo"
)

type alertMessage struct {
	url   string
	text  string
	color string
}

type connection struct {
	protocol string
	host     string
	port     string
	address  string
}

func checkError(err error) (int) {
	if err == nil {
		return 0
	}
	log.Printf("error: %s", err)
	return 1
}

func (c *connection) conn() (net.Conn, error) {
	conn, err := net.DialTimeout(c.protocol, c.address, 3*time.Second)
	checkError(err)
	return conn, err
}

func (a *alertMessage) sentSlack() {
	err := slackhookgo.Send(
		a.url,
		slackhookgo.NewSlackMessage(
			"username",
			"backup",
		).AddAttachment(
			slackhookgo.MessageAttachment{
				Color: a.color,
				Text:  a.text,
				Title: "<!channel>",
			},
		),
	)
	checkError(err)
}

func main() {
	var (
		k 	string
		t	string
		lastE	int = -1
		protocol = flag.String("protocol", "tcp", "protocol tcp/udp")
		host     = flag.String("host", "ya.ru", "destination host")
		port     = flag.String("port", "80", "destination port")
		interval = flag.Uint("interval", 5, "interval check seconds")
		url      = flag.String("url", "", "hook url")
	)

	flag.Parse()

	for {
		c := connection{
			protocol: *protocol,
			address:  fmt.Sprintf("%s:%s", *host, *port),
		}
		conn, err := c.conn()
		e := checkError(err)
                if e != lastE {
                        if e == 0 {		// normal
                                k = "good"
                                t = "reachable"
                                conn.Close()
                        } else {		// not normal
                                k = "danger"
                                t = "unreachable"
                        }
                        lastE = e		// key of success
                        am := alertMessage{
                                color:  k,
				text:   fmt.Sprintf("Destination host %s:%s %s\n", *host, *port, t),
                                url:    *url,
                        }
                        am.sentSlack()
                }
		time.Sleep(time.Duration(*interval) * time.Second)
	}
}
<commit_msg>fix conn()<commit_after>package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"time"

	"github.com/lowstz/slackhookgo"
)

type alertMessage struct {
	url   string
	text  string
	color string
}

type connection struct {
	protocol string
	host     string
	port     int
	address  string
}

func checkError(err error) int {
	if err == nil {
		return 0
	}
	log.Printf("error: %s", err)
	return 1
}

func (c *connection) conn() (net.Conn, int) {
	conn, err := net.DialTimeout(c.protocol, c.address, 3*time.Second)
	errInt := checkError(err)
	return conn, errInt
}

func (a *alertMessage) sentSlack() {
	err := slackhookgo.Send(
		a.url,
		slackhookgo.NewSlackMessage(
			"username",
			"backup",
		).AddAttachment(
			slackhookgo.MessageAttachment{
				Color: a.color,
				Text:  a.text,
				Title: "<!channel>",
			},
		),
	)
	checkError(err)
}

func main() {
	var (
		k        string
		t        string
		lastE    int
		protocol = flag.String("protocol", "tcp", "protocol tcp/udp")
		host     = flag.String("host", "ya.ru", "destination host")
		port     = flag.Int("port", 80, "destination port")
		interval = flag.Uint("interval", 5, "interval check seconds")
		url      = flag.String("url", "", "hook url")
	)

	flag.Parse()

	for {
		c := connection{
			protocol: *protocol,
			address:  fmt.Sprintf("%s:%v", *host, *port),
		}
		conn, err := c.conn()
		if err != lastE {
			if err == 0 { // normal
				k = "good"
				t = "reachable"
				conn.Close()
			} else { // not normal
				k = "danger"
				t = "unreachable"
			}
			lastE = err // key of success
			am := alertMessage{
				color: k,
				text:  fmt.Sprintf("Destination host %s:%v %s\n", *host, *port, t),
				url:   *url,
			}
			am.sentSlack()
		}
		time.Sleep(time.Duration(*interval) * time.Second)
	}
}
<|endoftext|>


<commit_before>package cmd

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"

	"github.com/codegangsta/cli"
	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/log"
	m "github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/sqlstore"
	"github.com/grafana/grafana/pkg/setting"
)

var ImportJson = cli.Command{
	Name:        "dashboard:import",
	Usage:       "imports dashboards in JSON from a directory",
	Description: "Starts Grafana import process",
	Action:      runImport,
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "dir",
			Usage: "path to folder containing json dashboards",
		},
		cli.StringFlag{
			Name:  "account",
			Usage: "Account name to save dashboards under",
		},
	},
}

func runImport(c *cli.Context) {
	dir := c.String("dir")
	if len(dir) == 0 {
		log.Error(3, "Missing command flag --dir")
		return
	}

	file, err := os.Stat(dir)
	if os.IsNotExist(err) {
		log.Error(3, "Directory does not exist: %v", dir)
		return
	}

	if !file.IsDir() {
		log.Error(3, "%v is not a directory", dir)
		return
	}

	accountName := c.String("account")
	if len(accountName) == 0 {
		log.Error(3, "Missing command flag --account")
		return
	}

	setting.NewConfigContext()
	sqlstore.NewEngine()
	sqlstore.EnsureAdminUser()

	accountQuery := m.GetAccountByNameQuery{Name: accountName}
	if err := bus.Dispatch(&accountQuery); err != nil {
		log.Error(3, "Failed to find account", err)
		return
	}

	accountId := accountQuery.Result.Id

	visitor := func(path string, f os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if f.IsDir() {
			return nil
		}
		if strings.HasSuffix(f.Name(), ".json") {
			if err := importDashboard(path, accountId); err != nil {
				log.Error(3, "Failed to import dashboard file: %v,  err: %v", path, err)
			}
		}
		return nil
	}

	if err := filepath.Walk(dir, visitor); err != nil {
		log.Error(3, "failed to scan dir for json files: %v", err)
	}
}

func importDashboard(path string, accountId int64) error {
	log.Info("Importing %v", path)

	reader, err := os.Open(path)
	if err != nil {
		return err
	}

	dash := m.NewDashboard("temp")
	jsonParser := json.NewDecoder(reader)

	if err := jsonParser.Decode(&dash.Data); err != nil {
		return err
	}
	dash.Data["id"] = nil

	cmd := m.SaveDashboardCommand{
		AccountId: accountId,
		Dashboard: dash.Data,
	}

	if err := bus.Dispatch(&cmd); err != nil {
		return err
	}

	return nil
}
<commit_msg>CLI: Conver account flag to required arg<commit_after>package cmd

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"

	"github.com/codegangsta/cli"
	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/log"
	m "github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/sqlstore"
	"github.com/grafana/grafana/pkg/setting"
)

var ImportJson = cli.Command{
	Name:        "dashboard:import",
	Usage:       "imports dashboards in JSON from a directory",
	Description: "Starts Grafana import process",
	Action:      runImport,
	Flags: []cli.Flag{
		cli.StringFlag{
			Name:  "dir",
			Usage: "path to folder containing json dashboards",
		},
	},
}

func runImport(c *cli.Context) {
	dir := c.String("dir")
	if len(dir) == 0 {
		log.Error(3, "Missing command flag --dir")
		return
	}

	file, err := os.Stat(dir)
	if os.IsNotExist(err) {
		log.Error(3, "Directory does not exist: %v", dir)
		return
	}

	if !file.IsDir() {
		log.Error(3, "%v is not a directory", dir)
		return
	}

	if !c.Args().Present() {
		log.ConsoleFatal("Account name arg is required")
	}

	accountName := c.Args().First()

	setting.NewConfigContext()
	sqlstore.NewEngine()
	sqlstore.EnsureAdminUser()

	accountQuery := m.GetAccountByNameQuery{Name: accountName}
	if err := bus.Dispatch(&accountQuery); err != nil {
		log.Error(3, "Failed to find account", err)
		return
	}

	accountId := accountQuery.Result.Id

	visitor := func(path string, f os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if f.IsDir() {
			return nil
		}
		if strings.HasSuffix(f.Name(), ".json") {
			if err := importDashboard(path, accountId); err != nil {
				log.Error(3, "Failed to import dashboard file: %v,  err: %v", path, err)
			}
		}
		return nil
	}

	if err := filepath.Walk(dir, visitor); err != nil {
		log.Error(3, "failed to scan dir for json files: %v", err)
	}
}

func importDashboard(path string, accountId int64) error {
	log.Info("Importing %v", path)

	reader, err := os.Open(path)
	if err != nil {
		return err
	}

	dash := m.NewDashboard("temp")
	jsonParser := json.NewDecoder(reader)

	if err := jsonParser.Decode(&dash.Data); err != nil {
		return err
	}
	dash.Data["id"] = nil

	cmd := m.SaveDashboardCommand{
		AccountId: accountId,
		Dashboard: dash.Data,
	}

	if err := bus.Dispatch(&cmd); err != nil {
		return err
	}

	return nil
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"fmt"
	"github.com/samuel/go-gettext/gettext"
	"html/template"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"
)

// page model
type Page struct {
	IsTor    bool
	UpToDate bool
	NotSmall bool
	OnOff    string
	Lang     string
	IP       string
	Extra    string
	Locales  map[string]string
}

var (

	// map the exit list
	// TODO: investigate other data structures
	ExitMap  map[string]bool
	ExitLock = new(sync.RWMutex)

	// layout template
	Layout = template.New("")

	// public file server
	Phttp = http.NewServeMux()

	// locales map
	Locales = map[string]string{
		"ar":    "عربية (Arabiya)",
		"bms":   "Burmese",
		"cs":    "česky",
		"da":    "Dansk",
		"de":    "Deutsch",
		"el":    "Ελληνικά (Ellinika)",
		"en_US": "English",
		"es":    "Español",
		"et":    "Estonian",
		"fa_IR": "فارسی (Fārsī)",
		"fr":    "Français",
		"it_IT": "Italiano",
		"ja":    "日本語 (Nihongo)",
		"nb":    "Norsk (Bokmål)",
		"nl":    "Nederlands",
		"pl":    "Polski",
		"pt":    "Português",
		"pt_BR": "Português do Brasil",
		"ro":    "Română",
		"fi":    "Suomi",
		"ru":    "Русский (Russkij)",
		"th":    "Thai",
		"tr":    "Türkçe",
		"uk":    "українська (Ukrajins\"ka)",
		"vi":    "Vietnamese",
		"zh_CN": "中文(简)",
	}
)

func GetExits() map[string]bool {
	ExitLock.RLock()
	defer ExitLock.RUnlock()
	return ExitMap
}

// load exit list
func LoadList() {

	file, err := os.Open("public/exit-addresses")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	exits := make(map[string]bool)
	scan := bufio.NewScanner(file)
	for scan.Scan() {
		strs := strings.Fields(scan.Text())
		if strs[0] == "ExitAddress" {
			exits[strs[1]] = true
		}
	}

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

	// swap in exits
	ExitLock.Lock()
	ExitMap = exits
	ExitLock.Unlock()

}

func IsTor(remoteAddr string) bool {

	if net.ParseIP(remoteAddr).To4() == nil {
		return false
	}
	return GetExits()[remoteAddr]

	// rejigger this to not make dns queries
	// ips := strings.Split(remoteAddr, ".")
	// var ip string
	// for i := len(ips) - 1; i >= 0; i-- {
	//   ip += ips[i] + "."
	// }
	// host := "80.38.229.70.31.ip-port.exitlist.torproject.org"
	// addresses, err := net.LookupHost(ip + host)
	// if err != nil {
	//   return false
	// }
	// inTor := true
	// for _, val := range addresses {
	//   if val != "127.0.0.2" {
	//     inTor = false
	//     break
	//   }
	// }
	// return inTor

}

func UpToDate(r *http.Request) bool {
	if r.URL.Query().Get("uptodate") == "0" {
		return false
	}
	return true
}

func Small(r *http.Request) bool {
	if len(r.URL.Query().Get("small")) > 0 {
		return true
	}
	return false
}

// determine which language to use. default to english
func Lang(r *http.Request) string {
	lang := r.URL.Query().Get("lang")
	if len(lang) == 0 {
		lang = "en_US"
	}
	return lang
}

func RootHandler(w http.ResponseWriter, r *http.Request) {

	// serve public files
	if len(r.URL.Path) > 1 {
		Phttp.ServeHTTP(w, r)
		return
	}

	// get remote ip
	host, _, _ := net.SplitHostPort(r.RemoteAddr)

	// determine if we're in Tor
	isTor := IsTor(host)

	// short circuit for torbutton
	if len(r.URL.Query().Get("TorButton")) > 0 {
		Layout.ExecuteTemplate(w, "torbutton.html", isTor)
		return
	}

	// string used for classes and such
	// in the template
	var onOff string
	if isTor {
		onOff = "on"
	} else {
		onOff = "off"
	}

	small := Small(r)
	upToDate := UpToDate(r)

	// querystring params
	extra := ""
	if small {
		extra += "&small=1"
	}
	if !upToDate {
		extra += "&uptodate=0"
	}

	// instance of your page model
	p := Page{
		isTor,
		isTor && !upToDate,
		!small,
		onOff,
		Lang(r),
		host,
		extra,
		Locales,
	}

	// render the template
	Layout.ExecuteTemplate(w, "index.html", p)

}

func main() {

	// determine which port to run on
	port := os.Getenv("PORT")
	if len(port) == 0 {
		port = "9000"
	}

	// load i18n
	domain, err := gettext.NewDomain("check", "locale")
	if err != nil {
		log.Fatal(err)
	}

	// add template funcs
	Layout = Layout.Funcs(template.FuncMap{
		"UnEscaped": func(x string) interface{} {
			return template.HTML(x)
		},
		"UnEscapedURL": func(x string) interface{} {
			return template.URL(x)
		},
		"GetText": func(lang string, text string) string {
			return domain.GetText(lang, text)
		},
	})

	// load layout
	Layout, err = Layout.ParseFiles(
		"public/index.html",
		"public/torbutton.html",
	)
	if err != nil {
		log.Fatal(err)
	}

	// load exists
	LoadList()

	// listen for signal to reload exits
	s := make(chan os.Signal, 1)
	signal.Notify(s, syscall.SIGUSR2)
	go func() {
		for {
			<-s
			LoadList()
			log.Println("Exit list reloaded.")
		}
	}()

	// routes
	http.HandleFunc("/", RootHandler)
	Phttp.Handle("/", http.FileServer(http.Dir("./public")))

	// start the server
	log.Printf("Listening on port: %s\n", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))

}
<commit_msg>typo noticed by @armadev<commit_after>package main

import (
	"bufio"
	"fmt"
	"github.com/samuel/go-gettext/gettext"
	"html/template"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"
)

// page model
type Page struct {
	IsTor    bool
	UpToDate bool
	NotSmall bool
	OnOff    string
	Lang     string
	IP       string
	Extra    string
	Locales  map[string]string
}

var (

	// map the exit list
	// TODO: investigate other data structures
	ExitMap  map[string]bool
	ExitLock = new(sync.RWMutex)

	// layout template
	Layout = template.New("")

	// public file server
	Phttp = http.NewServeMux()

	// locales map
	Locales = map[string]string{
		"ar":    "عربية (Arabiya)",
		"bms":   "Burmese",
		"cs":    "česky",
		"da":    "Dansk",
		"de":    "Deutsch",
		"el":    "Ελληνικά (Ellinika)",
		"en_US": "English",
		"es":    "Español",
		"et":    "Estonian",
		"fa_IR": "فارسی (Fārsī)",
		"fr":    "Français",
		"it_IT": "Italiano",
		"ja":    "日本語 (Nihongo)",
		"nb":    "Norsk (Bokmål)",
		"nl":    "Nederlands",
		"pl":    "Polski",
		"pt":    "Português",
		"pt_BR": "Português do Brasil",
		"ro":    "Română",
		"fi":    "Suomi",
		"ru":    "Русский (Russkij)",
		"th":    "Thai",
		"tr":    "Türkçe",
		"uk":    "українська (Ukrajins\"ka)",
		"vi":    "Vietnamese",
		"zh_CN": "中文(简)",
	}
)

func GetExits() map[string]bool {
	ExitLock.RLock()
	defer ExitLock.RUnlock()
	return ExitMap
}

// load exit list
func LoadList() {

	file, err := os.Open("public/exit-addresses")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	exits := make(map[string]bool)
	scan := bufio.NewScanner(file)
	for scan.Scan() {
		strs := strings.Fields(scan.Text())
		if strs[0] == "ExitAddress" {
			exits[strs[1]] = true
		}
	}

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

	// swap in exits
	ExitLock.Lock()
	ExitMap = exits
	ExitLock.Unlock()

}

func IsTor(remoteAddr string) bool {

	if net.ParseIP(remoteAddr).To4() == nil {
		return false
	}
	return GetExits()[remoteAddr]

	// rejigger this to not make dns queries
	// ips := strings.Split(remoteAddr, ".")
	// var ip string
	// for i := len(ips) - 1; i >= 0; i-- {
	//   ip += ips[i] + "."
	// }
	// host := "80.38.229.70.31.ip-port.exitlist.torproject.org"
	// addresses, err := net.LookupHost(ip + host)
	// if err != nil {
	//   return false
	// }
	// inTor := true
	// for _, val := range addresses {
	//   if val != "127.0.0.2" {
	//     inTor = false
	//     break
	//   }
	// }
	// return inTor

}

func UpToDate(r *http.Request) bool {
	if r.URL.Query().Get("uptodate") == "0" {
		return false
	}
	return true
}

func Small(r *http.Request) bool {
	if len(r.URL.Query().Get("small")) > 0 {
		return true
	}
	return false
}

// determine which language to use. default to english
func Lang(r *http.Request) string {
	lang := r.URL.Query().Get("lang")
	if len(lang) == 0 {
		lang = "en_US"
	}
	return lang
}

func RootHandler(w http.ResponseWriter, r *http.Request) {

	// serve public files
	if len(r.URL.Path) > 1 {
		Phttp.ServeHTTP(w, r)
		return
	}

	// get remote ip
	host, _, _ := net.SplitHostPort(r.RemoteAddr)

	// determine if we're in Tor
	isTor := IsTor(host)

	// short circuit for torbutton
	if len(r.URL.Query().Get("TorButton")) > 0 {
		Layout.ExecuteTemplate(w, "torbutton.html", isTor)
		return
	}

	// string used for classes and such
	// in the template
	var onOff string
	if isTor {
		onOff = "on"
	} else {
		onOff = "off"
	}

	small := Small(r)
	upToDate := UpToDate(r)

	// querystring params
	extra := ""
	if small {
		extra += "&small=1"
	}
	if !upToDate {
		extra += "&uptodate=0"
	}

	// instance of your page model
	p := Page{
		isTor,
		isTor && !upToDate,
		!small,
		onOff,
		Lang(r),
		host,
		extra,
		Locales,
	}

	// render the template
	Layout.ExecuteTemplate(w, "index.html", p)

}

func main() {

	// determine which port to run on
	port := os.Getenv("PORT")
	if len(port) == 0 {
		port = "9000"
	}

	// load i18n
	domain, err := gettext.NewDomain("check", "locale")
	if err != nil {
		log.Fatal(err)
	}

	// add template funcs
	Layout = Layout.Funcs(template.FuncMap{
		"UnEscaped": func(x string) interface{} {
			return template.HTML(x)
		},
		"UnEscapedURL": func(x string) interface{} {
			return template.URL(x)
		},
		"GetText": func(lang string, text string) string {
			return domain.GetText(lang, text)
		},
	})

	// load layout
	Layout, err = Layout.ParseFiles(
		"public/index.html",
		"public/torbutton.html",
	)
	if err != nil {
		log.Fatal(err)
	}

	// load exits
	LoadList()

	// listen for signal to reload exits
	s := make(chan os.Signal, 1)
	signal.Notify(s, syscall.SIGUSR2)
	go func() {
		for {
			<-s
			LoadList()
			log.Println("Exit list reloaded.")
		}
	}()

	// routes
	http.HandleFunc("/", RootHandler)
	Phttp.Handle("/", http.FileServer(http.Dir("./public")))

	// start the server
	log.Printf("Listening on port: %s\n", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))

}
<|endoftext|>


<commit_before>package cookie

import (
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"unicode/utf8"

	// "github.com/vouch/vouch-proxy/pkg/structs"
	"github.com/vouch/vouch-proxy/pkg/cfg"
	"github.com/vouch/vouch-proxy/pkg/domains"
)

const maxCookieSize = 4000

var log = cfg.Cfg.Logger

// SetCookie http
func SetCookie(w http.ResponseWriter, r *http.Request, val string) {
	setCookie(w, r, val, cfg.Cfg.Cookie.MaxAge*60) // convert minutes to seconds
}

func setCookie(w http.ResponseWriter, r *http.Request, val string, maxAge int) {
	cookieName := cfg.Cfg.Cookie.Name
	// foreach domain
	domain := domains.Matches(r.Host)
	// Allow overriding the cookie domain in the config file
	if cfg.Cfg.Cookie.Domain != "" {
		domain = cfg.Cfg.Cookie.Domain
		log.Debugf("setting the cookie domain to %v", domain)
	}
	cookie := http.Cookie{
		Name:     cfg.Cfg.Cookie.Name,
		Value:    val,
		Path:     "/",
		Domain:   domain,
		MaxAge:   maxAge,
		Secure:   cfg.Cfg.Cookie.Secure,
		HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
	}
	cookieSize := len(cookie.String())
	cookie.Value = ""
	emptyCookieSize := len(cookie.String())
	// Cookies have a max size of 4096 bytes, but to support most browsers, we should stay below 4000 bytes
	// https://tools.ietf.org/html/rfc6265#section-6.1
	// http://browsercookielimits.squawky.net/
	if cookieSize > maxCookieSize {
		// https://www.lifewire.com/cookie-limit-per-domain-3466809
		log.Warnf("cookie size: %d.  cookie sizes over ~4093 bytes(depending on the browser and platform) have shown to cause issues or simply aren't supported.", cookieSize)
		cookieParts := SplitCookie(val, maxCookieSize-emptyCookieSize)
		for i, cookiePart := range cookieParts {
			// Cookies are named 1of3, 2of3, 3of3
			cookieName = fmt.Sprintf("%s_%dof%d", cfg.Cfg.Cookie.Name, i+1, len(cookieParts))
			http.SetCookie(w, &http.Cookie{
				Name:     cookieName,
				Value:    cookiePart,
				Path:     "/",
				Domain:   domain,
				MaxAge:   maxAge,
				Secure:   cfg.Cfg.Cookie.Secure,
				HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
			})
		}
	} else {
		http.SetCookie(w, &http.Cookie{
			Name:     cookieName,
			Value:    val,
			Path:     "/",
			Domain:   domain,
			MaxAge:   maxAge,
			Secure:   cfg.Cfg.Cookie.Secure,
			HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
		})
	}
}

// Cookie get the vouch jwt cookie
func Cookie(r *http.Request) (string, error) {

	var cookieParts []string
	var numParts = -1

	var err error
	cookies := r.Cookies()
	// Get the remaining parts
	// search for cookie parts in order
	// this is the hotpath so we're trying to only walk once
	for _, cookie := range cookies {
		if cookie.Name == cfg.Cfg.Cookie.Name {
			return cookie.Value, nil
		}
		if strings.HasPrefix(cookie.Name, fmt.Sprintf("%s_", cfg.Cfg.Cookie.Name)) {
			log.Debugw("cookie",
				"cookieName", cookie.Name,
				"cookieValue", cookie.Value,
			)
			xOFy := strings.Split(cookie.Name, "_")[1]
			xyArray := strings.Split(xOFy, "of")
			if numParts == -1 { // then its uninitialized
				if numParts, err = strconv.Atoi(xyArray[1]); err != nil {
					return "", fmt.Errorf("multipart cookie fail: %s", err)
				}
				log.Debugf("make cookieParts of size %d", numParts)
				cookieParts = make([]string, numParts)
			}
			var i int
			if i, err = strconv.Atoi(xyArray[0]); err != nil {
				return "", fmt.Errorf("multipart cookie fail: %s", err)
			}
			cookieParts[i-1] = cookie.Value
		}

	}
	// combinedCookieStr := combinedCookie.String()
	combinedCookieStr := strings.Join(cookieParts, "")
	if combinedCookieStr == "" {
		return "", errors.New("Cookie token empty")
	}

	log.Debugw("combined cookie",
		"cookieValue", combinedCookieStr,
	)
	return combinedCookieStr, err
}

// ClearCookie get rid of the existing cookie
func ClearCookie(w http.ResponseWriter, r *http.Request) {
	cookies := r.Cookies()
	domain := domains.Matches(r.Host)
	// Allow overriding the cookie domain in the config file
	if cfg.Cfg.Cookie.Domain != "" {
		domain = cfg.Cfg.Cookie.Domain
		log.Debugf("setting the cookie domain to %v", domain)
	}
	// search for cookie parts
	for _, cookie := range cookies {
		if strings.HasPrefix(cookie.Name, cfg.Cfg.Cookie.Name) {
			log.Debugf("deleting cookie: %s", cookie.Name)
			http.SetCookie(w, &http.Cookie{
				Name:     cookie.Name,
				Value:    "delete",
				Path:     "/",
				Domain:   domain,
				MaxAge:   -1,
				Secure:   cfg.Cfg.Cookie.Secure,
				HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
			})
		}
	}
}

// SplitCookie separate string into several strings of specified length
func SplitCookie(longString string, maxLen int) []string {
	splits := []string{}

	var l, r int
	for l, r = 0, maxLen; r < len(longString); l, r = r, r+maxLen {
		for !utf8.RuneStart(longString[r]) {
			r--
		}
		splits = append(splits, longString[l:r])
	}
	splits = append(splits, longString[l:])
	return splits
}
<commit_msg>Using vouch.cookie.SameSite configuration option to set SameSite attribute on VouchCookie. Defaults to no SameSite attribute set on VouchCookie<commit_after>package cookie

import (
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"unicode/utf8"

	// "github.com/vouch/vouch-proxy/pkg/structs"
	"github.com/vouch/vouch-proxy/pkg/cfg"
	"github.com/vouch/vouch-proxy/pkg/domains"
)

const maxCookieSize = 4000

var log = cfg.Cfg.Logger

// SetCookie http
func SetCookie(w http.ResponseWriter, r *http.Request, val string) {
	setCookie(w, r, val, cfg.Cfg.Cookie.MaxAge*60) // convert minutes to seconds
}

func setCookie(w http.ResponseWriter, r *http.Request, val string, maxAge int) {
	cookieName := cfg.Cfg.Cookie.Name
	// foreach domain
	domain := domains.Matches(r.Host)
	// Allow overriding the cookie domain in the config file
	if cfg.Cfg.Cookie.Domain != "" {
		domain = cfg.Cfg.Cookie.Domain
		log.Debugf("setting the cookie domain to %v", domain)
	}
	sameSite := http.SameSiteDefaultMode
	if cfg.Cfg.Cookie.SameSite != 0 {
		sameSite = http.SameSite(cfg.Cfg.Cookie.SameSite)
	}
	cookie := http.Cookie{
		Name:     cfg.Cfg.Cookie.Name,
		Value:    val,
		Path:     "/",
		Domain:   domain,
		MaxAge:   maxAge,
		Secure:   cfg.Cfg.Cookie.Secure,
		HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
		SameSite: sameSite,
	}
	cookieSize := len(cookie.String())
	cookie.Value = ""
	emptyCookieSize := len(cookie.String())
	// Cookies have a max size of 4096 bytes, but to support most browsers, we should stay below 4000 bytes
	// https://tools.ietf.org/html/rfc6265#section-6.1
	// http://browsercookielimits.squawky.net/
	if cookieSize > maxCookieSize {
		// https://www.lifewire.com/cookie-limit-per-domain-3466809
		log.Warnf("cookie size: %d.  cookie sizes over ~4093 bytes(depending on the browser and platform) have shown to cause issues or simply aren't supported.", cookieSize)
		cookieParts := SplitCookie(val, maxCookieSize-emptyCookieSize)
		for i, cookiePart := range cookieParts {
			// Cookies are named 1of3, 2of3, 3of3
			cookieName = fmt.Sprintf("%s_%dof%d", cfg.Cfg.Cookie.Name, i+1, len(cookieParts))
			http.SetCookie(w, &http.Cookie{
				Name:     cookieName,
				Value:    cookiePart,
				Path:     "/",
				Domain:   domain,
				MaxAge:   maxAge,
				Secure:   cfg.Cfg.Cookie.Secure,
				HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
				SameSite: sameSite,
			})
		}
	} else {
		http.SetCookie(w, &http.Cookie{
			Name:     cookieName,
			Value:    val,
			Path:     "/",
			Domain:   domain,
			MaxAge:   maxAge,
			Secure:   cfg.Cfg.Cookie.Secure,
			HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
			SameSite: sameSite,
		})
	}
}

// Cookie get the vouch jwt cookie
func Cookie(r *http.Request) (string, error) {

	var cookieParts []string
	var numParts = -1

	var err error
	cookies := r.Cookies()
	// Get the remaining parts
	// search for cookie parts in order
	// this is the hotpath so we're trying to only walk once
	for _, cookie := range cookies {
		if cookie.Name == cfg.Cfg.Cookie.Name {
			return cookie.Value, nil
		}
		if strings.HasPrefix(cookie.Name, fmt.Sprintf("%s_", cfg.Cfg.Cookie.Name)) {
			log.Debugw("cookie",
				"cookieName", cookie.Name,
				"cookieValue", cookie.Value,
			)
			xOFy := strings.Split(cookie.Name, "_")[1]
			xyArray := strings.Split(xOFy, "of")
			if numParts == -1 { // then its uninitialized
				if numParts, err = strconv.Atoi(xyArray[1]); err != nil {
					return "", fmt.Errorf("multipart cookie fail: %s", err)
				}
				log.Debugf("make cookieParts of size %d", numParts)
				cookieParts = make([]string, numParts)
			}
			var i int
			if i, err = strconv.Atoi(xyArray[0]); err != nil {
				return "", fmt.Errorf("multipart cookie fail: %s", err)
			}
			cookieParts[i-1] = cookie.Value
		}

	}
	// combinedCookieStr := combinedCookie.String()
	combinedCookieStr := strings.Join(cookieParts, "")
	if combinedCookieStr == "" {
		return "", errors.New("Cookie token empty")
	}

	log.Debugw("combined cookie",
		"cookieValue", combinedCookieStr,
	)
	return combinedCookieStr, err
}

// ClearCookie get rid of the existing cookie
func ClearCookie(w http.ResponseWriter, r *http.Request) {
	cookies := r.Cookies()
	domain := domains.Matches(r.Host)
	// Allow overriding the cookie domain in the config file
	if cfg.Cfg.Cookie.Domain != "" {
		domain = cfg.Cfg.Cookie.Domain
		log.Debugf("setting the cookie domain to %v", domain)
	}
	// search for cookie parts
	for _, cookie := range cookies {
		if strings.HasPrefix(cookie.Name, cfg.Cfg.Cookie.Name) {
			log.Debugf("deleting cookie: %s", cookie.Name)
			http.SetCookie(w, &http.Cookie{
				Name:     cookie.Name,
				Value:    "delete",
				Path:     "/",
				Domain:   domain,
				MaxAge:   -1,
				Secure:   cfg.Cfg.Cookie.Secure,
				HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
			})
		}
	}
}

// SplitCookie separate string into several strings of specified length
func SplitCookie(longString string, maxLen int) []string {
	splits := []string{}

	var l, r int
	for l, r = 0, maxLen; r < len(longString); l, r = r, r+maxLen {
		for !utf8.RuneStart(longString[r]) {
			r--
		}
		splits = append(splits, longString[l:r])
	}
	splits = append(splits, longString[l:])
	return splits
}
<|endoftext|>


<commit_before>// Copyright 2018 Authors of Cilium
//
// 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 xds

import (
	"errors"

	"github.com/cilium/cilium/pkg/completion"
	"github.com/cilium/cilium/pkg/lock"
	"github.com/cilium/cilium/pkg/logging/logfields"

	"github.com/golang/protobuf/proto"
	"github.com/sirupsen/logrus"
)

// ProxyError wraps the error and the detail received from the proxy in to a new type
// that implements the error interface.
type ProxyError struct {
	Err    error
	Detail string
}

func (pe *ProxyError) Error() string {
	return pe.Err.Error() + ": " + pe.Detail
}

var (
	ErrNackReceived error = errors.New("NACK received")
)

// ResourceVersionAckObserver defines the HandleResourceVersionAck method
// which is called whenever a node acknowledges having applied a version of
// the resources of a given type.
type ResourceVersionAckObserver interface {
	// HandleResourceVersionAck notifies that the node with the given NodeIP
	// has acknowledged having applied the resources.
	// Calls to this function must not block.
	HandleResourceVersionAck(ackVersion uint64, nackVersion uint64, nodeIP string, resourceNames []string, typeURL string, detail string)
}

// AckingResourceMutatorRevertFunc is a function which reverts the effects of
// an update on a AckingResourceMutator.
// The completion is called back when the new resource update is
// ACKed by the Envoy nodes.
type AckingResourceMutatorRevertFunc func(completion *completion.Completion)

// AckingResourceMutator is a variant of ResourceMutator which calls back a
// Completion when a resource update is ACKed by a set of Envoy nodes.
type AckingResourceMutator interface {
	// Upsert inserts or updates a resource from this set by name and increases
	// the set's version number atomically if the resource is actually inserted
	// or updated.
	// The completion is called back when the new upserted resources' version is
	// ACKed by the Envoy nodes which IDs are given in nodeIDs.
	// A call to the returned revert function reverts the effects of this
	// method call.
	Upsert(typeURL string, resourceName string, resource proto.Message, nodeIDs []string, wg *completion.WaitGroup, callback func(error)) AckingResourceMutatorRevertFunc

	// UseCurrent inserts a completion that allows the caller to wait for the current
	// version of the given typeURL to be ACKed.
	UseCurrent(typeURL string, nodeIDs []string, wg *completion.WaitGroup)

	// DeleteNode frees resources held for the named node
	DeleteNode(nodeID string)

	// Delete deletes a resource from this set by name and increases the cache's
	// version number atomically if the resource is actually deleted.
	// The completion is called back when the new deleted resources' version is
	// ACKed by the Envoy nodes which IDs are given in nodeIDs.
	// A call to the returned revert function reverts the effects of this
	// method call.
	Delete(typeURL string, resourceName string, nodeIDs []string, wg *completion.WaitGroup, callback func(error)) AckingResourceMutatorRevertFunc
}

// AckingResourceMutatorWrapper is an AckingResourceMutator which wraps a
// ResourceMutator to notifies callers when resource updates are ACKed by
// nodes.
// AckingResourceMutatorWrapper also implements ResourceVersionAckObserver in
// order to be notified of ACKs from nodes.
type AckingResourceMutatorWrapper struct {
	// mutator is the wrapped resource mutator.
	mutator ResourceMutator

	// locker locks all accesses to the remaining fields.
	locker lock.Mutex

	// Last version stored by 'mutator'
	version uint64

	// ackedVersions is the last version acked by a node for this cache.
	// The key is the IPv4 address in string format for an Istio sidecar,
	// or "127.0.0.1" for the host proxy.
	ackedVersions map[string]uint64

	// pendingCompletions is the list of updates that are pending completion.
	pendingCompletions map[*completion.Completion]*pendingCompletion
}

// pendingCompletion is an update that is pending completion.
type pendingCompletion struct {
	// version is the version to be ACKed.
	version uint64

	// typeURL is the type URL of the resources to be ACKed.
	typeURL string

	// remainingNodesResources maps each pending node ID to pending resource
	// name.
	remainingNodesResources map[string]map[string]struct{}
}

// NewAckingResourceMutatorWrapper creates a new AckingResourceMutatorWrapper
// to wrap the given ResourceMutator.
func NewAckingResourceMutatorWrapper(mutator ResourceMutator) *AckingResourceMutatorWrapper {
	return &AckingResourceMutatorWrapper{
		mutator:            mutator,
		ackedVersions:      make(map[string]uint64),
		pendingCompletions: make(map[*completion.Completion]*pendingCompletion),
	}
}

// AddVersionCompletion adds a completion to wait for any ACK for the
// version and type URL, ignoring the ACKed resource names.
func (m *AckingResourceMutatorWrapper) addVersionCompletion(typeURL string, version uint64, nodeIDs []string, c *completion.Completion) {
	comp := &pendingCompletion{
		version:                 version,
		typeURL:                 typeURL,
		remainingNodesResources: make(map[string]map[string]struct{}, len(nodeIDs)),
	}
	for _, nodeID := range nodeIDs {
		comp.remainingNodesResources[nodeID] = nil
	}
	m.pendingCompletions[c] = comp
}

// DeleteNode frees resources held for the named nodes
func (m *AckingResourceMutatorWrapper) DeleteNode(nodeID string) {
	delete(m.ackedVersions, nodeID)
}

func (m *AckingResourceMutatorWrapper) Upsert(typeURL string, resourceName string, resource proto.Message, nodeIDs []string, wg *completion.WaitGroup, callback func(error)) AckingResourceMutatorRevertFunc {
	m.locker.Lock()
	defer m.locker.Unlock()

	var updated bool
	var revert ResourceMutatorRevertFunc
	m.version, updated, revert = m.mutator.Upsert(typeURL, resourceName, resource)

	if !updated {
		if wg != nil {
			m.useCurrent(typeURL, nodeIDs, wg)
		}
		return func(completion *completion.Completion) {}
	}

	if wg != nil {
		c := wg.AddCompletionWithCallback(callback)
		if _, found := m.pendingCompletions[c]; found {
			log.WithFields(logrus.Fields{
				logfields.XDSTypeURL:      typeURL,
				logfields.XDSResourceName: resourceName,
			}).Fatalf("attempt to reuse completion to upsert xDS resource: %v", c)
		}

		comp := &pendingCompletion{
			version:                 m.version,
			typeURL:                 typeURL,
			remainingNodesResources: make(map[string]map[string]struct{}, len(nodeIDs)),
		}
		for _, nodeID := range nodeIDs {
			comp.remainingNodesResources[nodeID] = make(map[string]struct{}, 1)
			comp.remainingNodesResources[nodeID][resourceName] = struct{}{}
		}
		m.pendingCompletions[c] = comp
	}

	return func(completion *completion.Completion) {
		m.locker.Lock()
		defer m.locker.Unlock()

		if revert != nil {
			m.version, _ = revert()

			if completion != nil {
				// We don't know whether the revert did an Upsert or a Delete, so as a
				// best effort, just wait for any ACK for the version and type URL,
				// and ignore the ACKed resource names, like for a Delete.
				m.addVersionCompletion(typeURL, m.version, nodeIDs, completion)
			}
		}
	}
}

func (m *AckingResourceMutatorWrapper) useCurrent(typeURL string, nodeIDs []string, wg *completion.WaitGroup) {
	if !m.currentVersionAcked(nodeIDs) {
		// Add a completion object for 'version' so that the caller may wait for the N/ACK
		m.addVersionCompletion(typeURL, m.version, nodeIDs, wg.AddCompletion())
	}
}

// UseCurrent adds a completion to the WaitGroup if the current
// version of the cached resource has not been acked yet, allowing the
// caller to wait for the ACK.
func (m *AckingResourceMutatorWrapper) UseCurrent(typeURL string, nodeIDs []string, wg *completion.WaitGroup) {
	m.locker.Lock()
	defer m.locker.Unlock()

	m.useCurrent(typeURL, nodeIDs, wg)
}

func (m *AckingResourceMutatorWrapper) currentVersionAcked(nodeIDs []string) bool {
	for _, node := range nodeIDs {
		if acked, exists := m.ackedVersions[node]; !exists || acked < m.version {
			ackLog := log.WithFields(logrus.Fields{
				logfields.XDSCachedVersion: m.version,
				logfields.XDSAckedVersion:  acked,
				logfields.XDSClientNode:    node,
			})
			ackLog.Debugf("Node has not acked the current cached version yet")
			return false
		}
	}
	return true
}

func (m *AckingResourceMutatorWrapper) Delete(typeURL string, resourceName string, nodeIDs []string, wg *completion.WaitGroup, callback func(error)) AckingResourceMutatorRevertFunc {
	m.locker.Lock()
	defer m.locker.Unlock()

	// Always delete the resource, even if the completion's context was
	// canceled before we even started, since we have no way to signal whether
	// the resource is actually deleted.

	// There is no explicit ACK for resource deletion in the xDS protocol.
	// As a best effort, just wait for any ACK for the version and type URL,
	// and ignore the ACKed resource names.

	var updated bool
	var revert ResourceMutatorRevertFunc
	m.version, updated, revert = m.mutator.Delete(typeURL, resourceName)

	if !updated {
		if wg != nil {
			m.useCurrent(typeURL, nodeIDs, wg)
		}
		return func(completion *completion.Completion) {}
	}

	if wg != nil {
		c := wg.AddCompletionWithCallback(callback)
		if _, found := m.pendingCompletions[c]; found {
			log.WithFields(logrus.Fields{
				logfields.XDSTypeURL:      typeURL,
				logfields.XDSResourceName: resourceName,
			}).Fatalf("attempt to reuse completion to delete xDS resource: %v", c)
		}

		m.addVersionCompletion(typeURL, m.version, nodeIDs, c)
	}

	return func(completion *completion.Completion) {
		m.locker.Lock()
		defer m.locker.Unlock()

		if revert != nil {
			m.version, _ = revert()

			if completion != nil {
				// We don't know whether the revert had any effect at all, so as a
				// best effort, just wait for any ACK for the version and type URL,
				// and ignore the ACKed resource names, like for a Delete.
				m.addVersionCompletion(typeURL, m.version, nodeIDs, completion)
			}
		}
	}
}

// 'ackVersion' is the last version that was acked. 'nackVersion', if greater than 'nackVersion', is the last version that was NACKed.
func (m *AckingResourceMutatorWrapper) HandleResourceVersionAck(ackVersion uint64, nackVersion uint64, nodeIP string, resourceNames []string, typeURL string, detail string) {
	ackLog := log.WithFields(logrus.Fields{
		logfields.XDSAckedVersion: ackVersion,
		logfields.XDSNonce:        nackVersion,
		logfields.XDSClientNode:   nodeIP,
		logfields.XDSTypeURL:      typeURL,
	})

	m.locker.Lock()
	defer m.locker.Unlock()

	// Update the last seen ACKed version if it advances the previously ACKed version.
	// Version 0 is special as it indicates that we have received the first xDS
	// resource request from Envoy. Prior to that we do not have a map entry for the
	// node at all.
	if previouslyAckedVersion, exists := m.ackedVersions[nodeIP]; !exists || previouslyAckedVersion < ackVersion {
		m.ackedVersions[nodeIP] = ackVersion
	}

	remainingCompletions := make(map[*completion.Completion]*pendingCompletion, len(m.pendingCompletions))

	for comp, pending := range m.pendingCompletions {
		if comp.Err() != nil {
			// Completion was canceled or timed out.
			// Remove from pending list.
			ackLog.Debugf("completion context was canceled: %v", pending)
			continue
		}

		if pending.typeURL == typeURL {
			if pending.version <= nackVersion {
				// Get the set of resource names we are still waiting for the node
				// to ACK.
				remainingResourceNames, found := pending.remainingNodesResources[nodeIP]
				if found {
					for _, name := range resourceNames {
						delete(remainingResourceNames, name)
					}
					if len(remainingResourceNames) == 0 {
						delete(pending.remainingNodesResources, nodeIP)
					}
					if len(pending.remainingNodesResources) == 0 {
						// Completed. Notify and remove from pending list.
						if pending.version <= ackVersion {
							ackLog.Debugf("completing ACK: %v", pending)
							comp.Complete(nil)
						} else {
							ackLog.Debugf("completing NACK: %v", pending)
							comp.Complete(&ProxyError{Err: ErrNackReceived, Detail: detail})
						}
						continue
					}
				}
			}
		}

		// Completion didn't match or is still waiting for some ACKs. Keep it
		// in the pending list.
		remainingCompletions[comp] = pending
	}

	m.pendingCompletions = remainingCompletions
}
<commit_msg>envoy: Take xds mutator lock for map access<commit_after>// Copyright 2018 Authors of Cilium
//
// 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 xds

import (
	"errors"

	"github.com/cilium/cilium/pkg/completion"
	"github.com/cilium/cilium/pkg/lock"
	"github.com/cilium/cilium/pkg/logging/logfields"

	"github.com/golang/protobuf/proto"
	"github.com/sirupsen/logrus"
)

// ProxyError wraps the error and the detail received from the proxy in to a new type
// that implements the error interface.
type ProxyError struct {
	Err    error
	Detail string
}

func (pe *ProxyError) Error() string {
	return pe.Err.Error() + ": " + pe.Detail
}

var (
	ErrNackReceived error = errors.New("NACK received")
)

// ResourceVersionAckObserver defines the HandleResourceVersionAck method
// which is called whenever a node acknowledges having applied a version of
// the resources of a given type.
type ResourceVersionAckObserver interface {
	// HandleResourceVersionAck notifies that the node with the given NodeIP
	// has acknowledged having applied the resources.
	// Calls to this function must not block.
	HandleResourceVersionAck(ackVersion uint64, nackVersion uint64, nodeIP string, resourceNames []string, typeURL string, detail string)
}

// AckingResourceMutatorRevertFunc is a function which reverts the effects of
// an update on a AckingResourceMutator.
// The completion is called back when the new resource update is
// ACKed by the Envoy nodes.
type AckingResourceMutatorRevertFunc func(completion *completion.Completion)

// AckingResourceMutator is a variant of ResourceMutator which calls back a
// Completion when a resource update is ACKed by a set of Envoy nodes.
type AckingResourceMutator interface {
	// Upsert inserts or updates a resource from this set by name and increases
	// the set's version number atomically if the resource is actually inserted
	// or updated.
	// The completion is called back when the new upserted resources' version is
	// ACKed by the Envoy nodes which IDs are given in nodeIDs.
	// A call to the returned revert function reverts the effects of this
	// method call.
	Upsert(typeURL string, resourceName string, resource proto.Message, nodeIDs []string, wg *completion.WaitGroup, callback func(error)) AckingResourceMutatorRevertFunc

	// UseCurrent inserts a completion that allows the caller to wait for the current
	// version of the given typeURL to be ACKed.
	UseCurrent(typeURL string, nodeIDs []string, wg *completion.WaitGroup)

	// DeleteNode frees resources held for the named node
	DeleteNode(nodeID string)

	// Delete deletes a resource from this set by name and increases the cache's
	// version number atomically if the resource is actually deleted.
	// The completion is called back when the new deleted resources' version is
	// ACKed by the Envoy nodes which IDs are given in nodeIDs.
	// A call to the returned revert function reverts the effects of this
	// method call.
	Delete(typeURL string, resourceName string, nodeIDs []string, wg *completion.WaitGroup, callback func(error)) AckingResourceMutatorRevertFunc
}

// AckingResourceMutatorWrapper is an AckingResourceMutator which wraps a
// ResourceMutator to notifies callers when resource updates are ACKed by
// nodes.
// AckingResourceMutatorWrapper also implements ResourceVersionAckObserver in
// order to be notified of ACKs from nodes.
type AckingResourceMutatorWrapper struct {
	// mutator is the wrapped resource mutator.
	mutator ResourceMutator

	// locker locks all accesses to the remaining fields.
	locker lock.Mutex

	// Last version stored by 'mutator'
	version uint64

	// ackedVersions is the last version acked by a node for this cache.
	// The key is the IPv4 address in string format for an Istio sidecar,
	// or "127.0.0.1" for the host proxy.
	ackedVersions map[string]uint64

	// pendingCompletions is the list of updates that are pending completion.
	pendingCompletions map[*completion.Completion]*pendingCompletion
}

// pendingCompletion is an update that is pending completion.
type pendingCompletion struct {
	// version is the version to be ACKed.
	version uint64

	// typeURL is the type URL of the resources to be ACKed.
	typeURL string

	// remainingNodesResources maps each pending node ID to pending resource
	// name.
	remainingNodesResources map[string]map[string]struct{}
}

// NewAckingResourceMutatorWrapper creates a new AckingResourceMutatorWrapper
// to wrap the given ResourceMutator.
func NewAckingResourceMutatorWrapper(mutator ResourceMutator) *AckingResourceMutatorWrapper {
	return &AckingResourceMutatorWrapper{
		mutator:            mutator,
		ackedVersions:      make(map[string]uint64),
		pendingCompletions: make(map[*completion.Completion]*pendingCompletion),
	}
}

// AddVersionCompletion adds a completion to wait for any ACK for the
// version and type URL, ignoring the ACKed resource names.
func (m *AckingResourceMutatorWrapper) addVersionCompletion(typeURL string, version uint64, nodeIDs []string, c *completion.Completion) {
	comp := &pendingCompletion{
		version:                 version,
		typeURL:                 typeURL,
		remainingNodesResources: make(map[string]map[string]struct{}, len(nodeIDs)),
	}
	for _, nodeID := range nodeIDs {
		comp.remainingNodesResources[nodeID] = nil
	}
	m.pendingCompletions[c] = comp
}

// DeleteNode frees resources held for the named nodes
func (m *AckingResourceMutatorWrapper) DeleteNode(nodeID string) {
	m.locker.Lock()
	defer m.locker.Unlock()

	delete(m.ackedVersions, nodeID)
}

func (m *AckingResourceMutatorWrapper) Upsert(typeURL string, resourceName string, resource proto.Message, nodeIDs []string, wg *completion.WaitGroup, callback func(error)) AckingResourceMutatorRevertFunc {
	m.locker.Lock()
	defer m.locker.Unlock()

	var updated bool
	var revert ResourceMutatorRevertFunc
	m.version, updated, revert = m.mutator.Upsert(typeURL, resourceName, resource)

	if !updated {
		if wg != nil {
			m.useCurrent(typeURL, nodeIDs, wg)
		}
		return func(completion *completion.Completion) {}
	}

	if wg != nil {
		c := wg.AddCompletionWithCallback(callback)
		if _, found := m.pendingCompletions[c]; found {
			log.WithFields(logrus.Fields{
				logfields.XDSTypeURL:      typeURL,
				logfields.XDSResourceName: resourceName,
			}).Fatalf("attempt to reuse completion to upsert xDS resource: %v", c)
		}

		comp := &pendingCompletion{
			version:                 m.version,
			typeURL:                 typeURL,
			remainingNodesResources: make(map[string]map[string]struct{}, len(nodeIDs)),
		}
		for _, nodeID := range nodeIDs {
			comp.remainingNodesResources[nodeID] = make(map[string]struct{}, 1)
			comp.remainingNodesResources[nodeID][resourceName] = struct{}{}
		}
		m.pendingCompletions[c] = comp
	}

	return func(completion *completion.Completion) {
		m.locker.Lock()
		defer m.locker.Unlock()

		if revert != nil {
			m.version, _ = revert()

			if completion != nil {
				// We don't know whether the revert did an Upsert or a Delete, so as a
				// best effort, just wait for any ACK for the version and type URL,
				// and ignore the ACKed resource names, like for a Delete.
				m.addVersionCompletion(typeURL, m.version, nodeIDs, completion)
			}
		}
	}
}

func (m *AckingResourceMutatorWrapper) useCurrent(typeURL string, nodeIDs []string, wg *completion.WaitGroup) {
	if !m.currentVersionAcked(nodeIDs) {
		// Add a completion object for 'version' so that the caller may wait for the N/ACK
		m.addVersionCompletion(typeURL, m.version, nodeIDs, wg.AddCompletion())
	}
}

// UseCurrent adds a completion to the WaitGroup if the current
// version of the cached resource has not been acked yet, allowing the
// caller to wait for the ACK.
func (m *AckingResourceMutatorWrapper) UseCurrent(typeURL string, nodeIDs []string, wg *completion.WaitGroup) {
	m.locker.Lock()
	defer m.locker.Unlock()

	m.useCurrent(typeURL, nodeIDs, wg)
}

func (m *AckingResourceMutatorWrapper) currentVersionAcked(nodeIDs []string) bool {
	for _, node := range nodeIDs {
		if acked, exists := m.ackedVersions[node]; !exists || acked < m.version {
			ackLog := log.WithFields(logrus.Fields{
				logfields.XDSCachedVersion: m.version,
				logfields.XDSAckedVersion:  acked,
				logfields.XDSClientNode:    node,
			})
			ackLog.Debugf("Node has not acked the current cached version yet")
			return false
		}
	}
	return true
}

func (m *AckingResourceMutatorWrapper) Delete(typeURL string, resourceName string, nodeIDs []string, wg *completion.WaitGroup, callback func(error)) AckingResourceMutatorRevertFunc {
	m.locker.Lock()
	defer m.locker.Unlock()

	// Always delete the resource, even if the completion's context was
	// canceled before we even started, since we have no way to signal whether
	// the resource is actually deleted.

	// There is no explicit ACK for resource deletion in the xDS protocol.
	// As a best effort, just wait for any ACK for the version and type URL,
	// and ignore the ACKed resource names.

	var updated bool
	var revert ResourceMutatorRevertFunc
	m.version, updated, revert = m.mutator.Delete(typeURL, resourceName)

	if !updated {
		if wg != nil {
			m.useCurrent(typeURL, nodeIDs, wg)
		}
		return func(completion *completion.Completion) {}
	}

	if wg != nil {
		c := wg.AddCompletionWithCallback(callback)
		if _, found := m.pendingCompletions[c]; found {
			log.WithFields(logrus.Fields{
				logfields.XDSTypeURL:      typeURL,
				logfields.XDSResourceName: resourceName,
			}).Fatalf("attempt to reuse completion to delete xDS resource: %v", c)
		}

		m.addVersionCompletion(typeURL, m.version, nodeIDs, c)
	}

	return func(completion *completion.Completion) {
		m.locker.Lock()
		defer m.locker.Unlock()

		if revert != nil {
			m.version, _ = revert()

			if completion != nil {
				// We don't know whether the revert had any effect at all, so as a
				// best effort, just wait for any ACK for the version and type URL,
				// and ignore the ACKed resource names, like for a Delete.
				m.addVersionCompletion(typeURL, m.version, nodeIDs, completion)
			}
		}
	}
}

// 'ackVersion' is the last version that was acked. 'nackVersion', if greater than 'nackVersion', is the last version that was NACKed.
func (m *AckingResourceMutatorWrapper) HandleResourceVersionAck(ackVersion uint64, nackVersion uint64, nodeIP string, resourceNames []string, typeURL string, detail string) {
	ackLog := log.WithFields(logrus.Fields{
		logfields.XDSAckedVersion: ackVersion,
		logfields.XDSNonce:        nackVersion,
		logfields.XDSClientNode:   nodeIP,
		logfields.XDSTypeURL:      typeURL,
	})

	m.locker.Lock()
	defer m.locker.Unlock()

	// Update the last seen ACKed version if it advances the previously ACKed version.
	// Version 0 is special as it indicates that we have received the first xDS
	// resource request from Envoy. Prior to that we do not have a map entry for the
	// node at all.
	if previouslyAckedVersion, exists := m.ackedVersions[nodeIP]; !exists || previouslyAckedVersion < ackVersion {
		m.ackedVersions[nodeIP] = ackVersion
	}

	remainingCompletions := make(map[*completion.Completion]*pendingCompletion, len(m.pendingCompletions))

	for comp, pending := range m.pendingCompletions {
		if comp.Err() != nil {
			// Completion was canceled or timed out.
			// Remove from pending list.
			ackLog.Debugf("completion context was canceled: %v", pending)
			continue
		}

		if pending.typeURL == typeURL {
			if pending.version <= nackVersion {
				// Get the set of resource names we are still waiting for the node
				// to ACK.
				remainingResourceNames, found := pending.remainingNodesResources[nodeIP]
				if found {
					for _, name := range resourceNames {
						delete(remainingResourceNames, name)
					}
					if len(remainingResourceNames) == 0 {
						delete(pending.remainingNodesResources, nodeIP)
					}
					if len(pending.remainingNodesResources) == 0 {
						// Completed. Notify and remove from pending list.
						if pending.version <= ackVersion {
							ackLog.Debugf("completing ACK: %v", pending)
							comp.Complete(nil)
						} else {
							ackLog.Debugf("completing NACK: %v", pending)
							comp.Complete(&ProxyError{Err: ErrNackReceived, Detail: detail})
						}
						continue
					}
				}
			}
		}

		// Completion didn't match or is still waiting for some ACKs. Keep it
		// in the pending list.
		remainingCompletions[comp] = pending
	}

	m.pendingCompletions = remainingCompletions
}
<|endoftext|>