<commit_before>package globalsettings

import (
	"errors"
	"fmt"
	"strings"
	"time"

	"gopkg.in/adsi.v0"
	"gopkg.in/dfsr.v0/config/membercache"
	"gopkg.in/dfsr.v0/core"
)

// GlobalSettings provides a means of querying DFSR global settings.
type GlobalSettings struct {
	client   *adsi.Client
	domainDN string
	mc       *membercache.Cache // Maps distinguished names to MemberInfo
}

// New returns a new DFSR global settings configuration manager for the given
// domain.
//
// The provided ADSI client is retained by the global settings and will be used
// internally to peform the necessary LDAP queries. It is the caller's
// responsibility to explicitly close the ADSI client at an appropriate time
// when finished with the global settings.
func New(client *adsi.Client, domain string) *GlobalSettings {
	return &GlobalSettings{
		client:   client,
		domainDN: domainDN(domain),
		mc:       membercache.New(),
	}
}

// Domain will fetch DFSR configuration data from the domain.
func (gs *GlobalSettings) Domain() (domain core.Domain, err error) {
	start := time.Now()

	nc, err := gs.NamingContext()
	if err != nil {
		return
	}

	groups, err := gs.Groups()
	if err != nil {
		return
	}

	return core.Domain{
		NamingContext:  nc,
		Groups:         groups,
		ConfigDuration: time.Now().Sub(start),
	}, nil
}

// NamingContext returns information about the default naming context for the
// domain.
func (gs *GlobalSettings) NamingContext() (nc core.NamingContext, err error) {
	domain, err := gs.client.Open(ldap(gs.domainDN))
	if err != nil {
		return
	}
	defer domain.Close()

	nc.ID, err = domain.GUID()
	if err != nil {
		return
	}

	nc.Path, err = domain.Path()
	if err != nil {
		return
	}

	nc.DN = strings.TrimPrefix(nc.Path, "LDAP://")

	nc.Description, err = domain.AttrString("description")
	if err != nil {
		return
	}
	return
}

// Groups retreives the DFSR group configuration for all groups contained in the
// domain.
func (gs *GlobalSettings) Groups() (groups []core.Group, err error) {
	container, err := gs.openContainer(makeDN("cn", "DFSR-GlobalSettings", "System"))
	if err != nil {
		return nil, err
	}
	defer container.Close()

	iter, err := container.Children()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	var results []chan groupResult

	for g, gerr := iter.Next(); gerr == nil; g, gerr = iter.Next() {
		ch := make(chan groupResult, 1)
		results = append(results, ch)

		go func(ch chan groupResult, g *adsi.Object) {
			defer g.Close()
			defer close(ch)
			group, werr := gs.group(g)
			ch <- groupResult{Group: group, Err: werr}
		}(ch, g)

		time.Sleep(groupQueryDelay) // Try to avoid rate-limiting
	}

	for i := 0; i < len(results); i++ {
		result := <-results[i]
		if err != nil {
			continue // Already hit an error, just drain the channels
		}

		if result.Err != nil {
			err = fmt.Errorf("Error retrieving configuration for replication group %v: %v", i, result.Err.Error())
			groups = nil
		} else {
			groups = append(groups, result.Group)
		}
	}

	return
}

/*
func (gs *GlobalSettings) groups() (groups []core.Group, err error) {
	container, err := gs.openContainer(makeDN("cn", "DFSR-GlobalSettings", "System"))
	if err != nil {
		return nil, err
	}
	defer container.Close()

	iter, err := container.Children()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	for g, err := iter.Next(); err == nil; g, err = iter.Next() {
		defer g.Close()

		group, err := gs.group(g)
		if err != nil {
			return nil, err
		}

		groups = append(groups, group)
	}

	return
}
*/

// GroupByName retreives the DFSR group configuration for the given name.
func (gs *GlobalSettings) GroupByName(groupName string) (group core.Group, err error) {
	groupName = strings.ToLower(groupName)

	container, err := gs.openContainer(makeDN("cn", "DFSR-GlobalSettings", "System"))
	if err != nil {
		return
	}
	defer container.Close()

	iter, err := container.Children()
	if err != nil {
		return
	}
	defer iter.Close()

	for g, gerr := iter.Next(); gerr == nil; g, gerr = iter.Next() {
		defer g.Close()

		candidate, cerr := g.Name()
		if err != nil {
			err = cerr
			return
		}
		candidate = strings.ToLower(candidate)

		if candidate == groupName || strings.TrimPrefix(candidate, "cn=") == groupName {
			return gs.group(g)
		}
	}

	err = errors.New("Replication Group not found.")
	return
}

// Group retreives the DFSR group configuration for the given distinguished
// name.
func (gs *GlobalSettings) Group(groupDN string) (group core.Group, err error) {
	g, err := gs.client.Open(ldap(groupDN))
	if err != nil {
		return
	}
	defer g.Close()

	return gs.group(g)
}

func (gs *GlobalSettings) group(g *adsi.Object) (group core.Group, err error) {
	start := time.Now()

	group.Name, err = g.Name()
	if err != nil {
		return
	}
	group.Name = strings.TrimPrefix(group.Name, "CN=")

	group.ID, err = g.GUID()
	if err != nil {
		return
	}

	gc, err := g.ToContainer()
	if err != nil {
		return
	}
	defer gc.Close()

	content, err := gc.Container("msDFSR-Content", "cn=Content")
	if err != nil {
		return
	}
	defer content.Close()

	group.Folders, err = gs.folders(content)
	if err != nil {
		return
	}

	topology, err := gc.Object("msDFSR-Topology", "cn=Topology")
	if err != nil {
		return
	}
	defer topology.Close()

	group.Members, err = gs.members(topology)
	if err != nil {
		return
	}

	group.ConfigDuration = time.Now().Sub(start)

	return
}

func (gs *GlobalSettings) folders(content *adsi.Container) (folders []core.Folder, err error) {
	iter, err := content.Children()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	for f, err := iter.Next(); err == nil; f, err = iter.Next() {
		defer f.Close()

		folder, err := gs.folder(f)
		if err != nil {
			return nil, err
		}

		folders = append(folders, folder)
	}

	return
}

func (gs *GlobalSettings) folder(f *adsi.Object) (folder core.Folder, err error) {
	folder.Name, err = f.Name()
	if err != nil {
		return
	}
	folder.Name = strings.TrimPrefix(folder.Name, "CN=")

	folder.ID, err = f.GUID()
	if err != nil {
		return
	}

	return
}

func (gs *GlobalSettings) members(topology *adsi.Object) (members []core.Member, err error) {
	tc, err := topology.ToContainer()
	if err != nil {
		return nil, err
	}
	defer tc.Close()

	iter, err := tc.Children()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	for m, err := iter.Next(); err == nil; m, err = iter.Next() {
		defer m.Close()

		member, err := gs.member(m, "")
		if err != nil {
			return nil, err
		}

		members = append(members, member)
	}

	return
}

// Member retreives the DFSR member configuration for the given distinguished
// name. The member's connection list is included in the returned data.
func (gs *GlobalSettings) Member(memberDN string) (member core.Member, err error) {
	m, err := gs.client.Open(ldap(memberDN))
	if err != nil {
		return
	}
	defer m.Close()

	return gs.member(m, memberDN)
}

func (gs *GlobalSettings) member(m *adsi.Object, dn string) (member core.Member, err error) {
	member.MemberInfo, err = gs.memberInfo(m, dn)
	if err != nil {
		return
	}

	member.Connections, err = gs.connections(m)
	if err != nil {
		return
	}

	return
}

// MemberInfo retreives the DFSR member configuration for the given
// distinguished name. The member's connection list is not included in the
// returned data.
func (gs *GlobalSettings) MemberInfo(memberDN string) (member core.MemberInfo, err error) {
	member, ok := gs.mc.Retrieve(memberDN)
	if ok {
		return
	}

	m, err := gs.client.Open(ldap(memberDN))
	if err != nil {
		return
	}
	defer m.Close()

	return gs.memberInfo(m, memberDN)
}

func (gs *GlobalSettings) memberInfo(m *adsi.Object, dn string) (member core.MemberInfo, err error) {
	if dn == "" {
		path, perr := m.Path()
		if err != nil {
			err = perr
			return
		}
		member.DN = strings.TrimPrefix(path, "LDAP://")
	} else {
		member.DN = dn
	}

	member.Name, err = m.Name()
	if err != nil {
		return
	}
	member.Name = strings.TrimPrefix(member.Name, "CN=")

	member.ID, err = m.GUID()
	if err != nil {
		return
	}

	compref, err := m.AttrString("msDFSR-ComputerReference")
	if err != nil {
		return
	}

	member.Computer, err = gs.Computer(compref)

	gs.mc.Set(member) // Add member info to the cache
	return
}

func (gs *GlobalSettings) connections(member *adsi.Object) (connections []core.Connection, err error) {
	mc, err := member.ToContainer()
	if err != nil {
		return nil, err
	}
	defer mc.Close()

	iter, err := mc.Children()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	for c, err := iter.Next(); err == nil; c, err = iter.Next() {
		defer c.Close()

		conn, err := gs.connection(c)
		if err != nil {
			return nil, err
		}

		connections = append(connections, conn)
	}

	return
}

func (gs *GlobalSettings) connection(c *adsi.Object) (conn core.Connection, err error) {
	conn.Name, err = c.Name()
	if err != nil {
		return
	}
	conn.Name = strings.TrimPrefix(conn.Name, "CN=")

	conn.ID, err = c.GUID()
	if err != nil {
		return
	}

	conn.MemberDN, err = c.AttrString("fromServer")
	if err != nil {
		return
	}

	conn.Enabled, err = c.AttrBool("msDFSR-Enabled")
	if err != nil {
		return
	}

	mi, err := gs.MemberInfo(conn.MemberDN)
	if err != nil {
		return
	}

	conn.Computer = mi.Computer

	return
}

// Computer retrieves the DNS host name for the given distinguished name.
func (gs *GlobalSettings) Computer(dn string) (computer core.Computer, err error) {
	c, err := gs.client.Open(ldap(dn))
	if err != nil {
		return
	}
	defer c.Close()

	return gs.computer(c)
}

func (gs *GlobalSettings) computer(c *adsi.Object) (computer core.Computer, err error) {
	computer.DN, err = c.Path()
	if err != nil {
		return
	}

	computer.Host, err = c.AttrString("dNSHostName")
	if err != nil {
		return
	}

	return
}

func (gs *GlobalSettings) openContainer(partialDN string) (*adsi.Container, error) {
	path := ldap(combineDN(partialDN, gs.domainDN))
	return gs.client.OpenContainer(path)
}
<commit_msg>Added basic domain system volume support to globalsettings<commit_after>package globalsettings

import (
	"errors"
	"fmt"
	"strings"
	"time"

	"gopkg.in/adsi.v0"
	"gopkg.in/dfsr.v0/config/membercache"
	"gopkg.in/dfsr.v0/core"
)

// GlobalSettings provides a means of querying DFSR global settings.
type GlobalSettings struct {
	client   *adsi.Client
	domainDN string
	mc       *membercache.Cache // Maps distinguished names to MemberInfo
}

// New returns a new DFSR global settings configuration manager for the given
// domain.
//
// The provided ADSI client is retained by the global settings and will be used
// internally to peform the necessary LDAP queries. It is the caller's
// responsibility to explicitly close the ADSI client at an appropriate time
// when finished with the global settings.
func New(client *adsi.Client, domain string) *GlobalSettings {
	return &GlobalSettings{
		client:   client,
		domainDN: domainDN(domain),
		mc:       membercache.New(),
	}
}

// Domain will fetch DFSR configuration data from the domain.
func (gs *GlobalSettings) Domain() (domain core.Domain, err error) {
	start := time.Now()

	nc, err := gs.NamingContext()
	if err != nil {
		return
	}

	groups, err := gs.Groups()
	if err != nil {
		return
	}

	return core.Domain{
		NamingContext:  nc,
		Groups:         groups,
		ConfigDuration: time.Now().Sub(start),
	}, nil
}

// NamingContext returns information about the default naming context for the
// domain.
func (gs *GlobalSettings) NamingContext() (nc core.NamingContext, err error) {
	domain, err := gs.client.Open(ldap(gs.domainDN))
	if err != nil {
		return
	}
	defer domain.Close()

	nc.ID, err = domain.GUID()
	if err != nil {
		return
	}

	nc.Path, err = domain.Path()
	if err != nil {
		return
	}

	nc.DN = strings.TrimPrefix(nc.Path, "LDAP://")

	nc.Description, err = domain.AttrString("description")
	if err != nil {
		return
	}
	return
}

// Groups retreives the DFSR group configuration for all groups contained in the
// domain.
func (gs *GlobalSettings) Groups() (groups []core.Group, err error) {
	container, err := gs.openContainer(makeDN("cn", "DFSR-GlobalSettings", "System"))
	if err != nil {
		return nil, err
	}
	defer container.Close()

	iter, err := container.Children()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	var results []chan groupResult

	for g, gerr := iter.Next(); gerr == nil; g, gerr = iter.Next() {
		ch := make(chan groupResult, 1)
		results = append(results, ch)

		go func(ch chan groupResult, g *adsi.Object) {
			defer g.Close()
			defer close(ch)
			group, werr := gs.group(g)
			ch <- groupResult{Group: group, Err: werr}
		}(ch, g)

		time.Sleep(groupQueryDelay) // Try to avoid rate-limiting
	}

	for i := 0; i < len(results); i++ {
		result := <-results[i]
		if err != nil {
			continue // Already hit an error, just drain the channels
		}

		if result.Err != nil {
			err = fmt.Errorf("Error retrieving configuration for replication group %v: %v", i, result.Err.Error())
			groups = nil
		} else {
			groups = append(groups, result.Group)
		}
	}

	return
}

/*
func (gs *GlobalSettings) groups() (groups []core.Group, err error) {
	container, err := gs.openContainer(makeDN("cn", "DFSR-GlobalSettings", "System"))
	if err != nil {
		return nil, err
	}
	defer container.Close()

	iter, err := container.Children()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	for g, err := iter.Next(); err == nil; g, err = iter.Next() {
		defer g.Close()

		group, err := gs.group(g)
		if err != nil {
			return nil, err
		}

		groups = append(groups, group)
	}

	return
}
*/

// GroupByName retreives the DFSR group configuration for the given name.
func (gs *GlobalSettings) GroupByName(groupName string) (group core.Group, err error) {
	groupName = strings.ToLower(groupName)

	container, err := gs.openContainer(makeDN("cn", "DFSR-GlobalSettings", "System"))
	if err != nil {
		return
	}
	defer container.Close()

	iter, err := container.Children()
	if err != nil {
		return
	}
	defer iter.Close()

	for g, gerr := iter.Next(); gerr == nil; g, gerr = iter.Next() {
		defer g.Close()

		candidate, cerr := g.Name()
		if err != nil {
			err = cerr
			return
		}
		candidate = strings.ToLower(candidate)

		if candidate == groupName || strings.TrimPrefix(candidate, "cn=") == groupName {
			return gs.group(g)
		}
	}

	err = errors.New("Replication Group not found.")
	return
}

// Group retreives the DFSR group configuration for the given distinguished
// name.
func (gs *GlobalSettings) Group(groupDN string) (group core.Group, err error) {
	g, err := gs.client.Open(ldap(groupDN))
	if err != nil {
		return
	}
	defer g.Close()

	return gs.group(g)
}

func (gs *GlobalSettings) group(g *adsi.Object) (group core.Group, err error) {
	start := time.Now()

	group.Name, err = g.Name()
	if err != nil {
		return
	}
	group.Name = strings.TrimPrefix(group.Name, "CN=")

	group.ID, err = g.GUID()
	if err != nil {
		return
	}

	gc, err := g.ToContainer()
	if err != nil {
		return
	}
	defer gc.Close()

	content, err := gc.Container("msDFSR-Content", "cn=Content")
	if err != nil {
		return
	}
	defer content.Close()

	group.Folders, err = gs.folders(content)
	if err != nil {
		return
	}

	topology, err := gc.Object("msDFSR-Topology", "cn=Topology")
	if err != nil {
		return
	}
	defer topology.Close()

	group.Members, err = gs.members(topology)
	if err != nil {
		return
	}

	group.ConfigDuration = time.Now().Sub(start)

	return
}

func (gs *GlobalSettings) folders(content *adsi.Container) (folders []core.Folder, err error) {
	iter, err := content.Children()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	for f, err := iter.Next(); err == nil; f, err = iter.Next() {
		defer f.Close()

		folder, err := gs.folder(f)
		if err != nil {
			return nil, err
		}

		folders = append(folders, folder)
	}

	return
}

func (gs *GlobalSettings) folder(f *adsi.Object) (folder core.Folder, err error) {
	folder.Name, err = f.Name()
	if err != nil {
		return
	}
	folder.Name = strings.TrimPrefix(folder.Name, "CN=")

	folder.ID, err = f.GUID()
	if err != nil {
		return
	}

	return
}

func (gs *GlobalSettings) members(topology *adsi.Object) (members []core.Member, err error) {
	tc, err := topology.ToContainer()
	if err != nil {
		return nil, err
	}
	defer tc.Close()

	iter, err := tc.Children()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	for m, err := iter.Next(); err == nil; m, err = iter.Next() {
		defer m.Close()

		member, err := gs.member(m, "")
		if err != nil {
			return nil, err
		}

		members = append(members, member)
	}

	return
}

// Member retreives the DFSR member configuration for the given distinguished
// name. The member's connection list is included in the returned data.
func (gs *GlobalSettings) Member(memberDN string) (member core.Member, err error) {
	m, err := gs.client.Open(ldap(memberDN))
	if err != nil {
		return
	}
	defer m.Close()

	return gs.member(m, memberDN)
}

func (gs *GlobalSettings) member(m *adsi.Object, dn string) (member core.Member, err error) {
	member.MemberInfo, err = gs.memberInfo(m, dn)
	if err != nil {
		return
	}

	serverref, _ := m.AttrString("serverReference")
	if serverref == "" {
		// Standard DFSR membership
		member.Connections, err = gs.connections(m)
		return
	}

	// Domain System Volume membership
	server, err := gs.client.Open(ldap(serverref))
	if err != nil {
		return
	}

	member.Connections, err = gs.connections(server)
	return
}

// MemberInfo retreives the DFSR member configuration for the given
// distinguished name. The member's connection list is not included in the
// returned data.
func (gs *GlobalSettings) MemberInfo(memberDN string) (member core.MemberInfo, err error) {
	member, ok := gs.mc.Retrieve(memberDN)
	if ok {
		return
	}

	m, err := gs.client.Open(ldap(memberDN))
	if err != nil {
		return
	}
	defer m.Close()

	return gs.memberInfo(m, memberDN)
}

func (gs *GlobalSettings) memberInfo(m *adsi.Object, dn string) (member core.MemberInfo, err error) {
	if dn == "" {
		path, perr := m.Path()
		if err != nil {
			err = perr
			return
		}
		member.DN = strings.TrimPrefix(path, "LDAP://")
	} else {
		member.DN = dn
	}

	class, err := m.Class()
	if err != nil {
		return
	}

	var compref string
	switch class {
	case "nTDSDSA":
		m, err = gs.openParent(m)
		defer m.Close()
		fallthrough
	case "server":
		compref, err = m.AttrString("serverReference")
		if err != nil {
			return
		}
	case "msDFSR-Member":
		compref, err = m.AttrString("msDFSR-ComputerReference")
		if err != nil {
			return
		}
	default:
		err = errors.New("Unknown Active Directory membership class")
		return
	}

	member.Name, err = m.Name()
	if err != nil {
		return
	}
	member.Name = strings.TrimPrefix(member.Name, "CN=")

	member.ID, err = m.GUID()
	if err != nil {
		return
	}

	member.Computer, err = gs.Computer(compref)

	gs.mc.Set(member) // Add member info to the cache
	return
}

func (gs *GlobalSettings) connections(member *adsi.Object) (connections []core.Connection, err error) {
	mc, err := member.ToContainer()
	if err != nil {
		return nil, err
	}
	defer mc.Close()

	iter, err := mc.Children()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	for c, err := iter.Next(); err == nil; c, err = iter.Next() {
		defer c.Close()

		conn, err := gs.connection(c)
		if err != nil {
			return nil, err
		}

		connections = append(connections, conn)
	}

	return
}

func (gs *GlobalSettings) connection(c *adsi.Object) (conn core.Connection, err error) {
	class, err := c.Class()
	if err != nil {
		return
	}

	conn.Name, err = c.Name()
	if err != nil {
		return
	}
	conn.Name = strings.TrimPrefix(conn.Name, "CN=")

	conn.ID, err = c.GUID()
	if err != nil {
		return
	}

	conn.MemberDN, err = c.AttrString("fromServer")
	if err != nil {
		return
	}

	if class == "msDFSR-Member" {
		// Standard DFSR connection
		conn.Enabled, err = c.AttrBool("msDFSR-Enabled")
		if err != nil {
			return
		}
	} else if class == "nTDSConnection" {
		// Domain System Volume membership
		conn.Enabled = true // These members are always enabled
	}

	mi, err := gs.MemberInfo(conn.MemberDN)
	if err != nil {
		return
	}

	conn.Computer = mi.Computer

	return
}

// Computer retrieves the DNS host name for the given distinguished name.
func (gs *GlobalSettings) Computer(dn string) (computer core.Computer, err error) {
	c, err := gs.client.Open(ldap(dn))
	if err != nil {
		return
	}
	defer c.Close()

	return gs.computer(c)
}

func (gs *GlobalSettings) computer(c *adsi.Object) (computer core.Computer, err error) {
	computer.DN, err = c.Path()
	if err != nil {
		return
	}
	computer.DN = strings.TrimPrefix(computer.DN, "LDAP://")

	computer.Host, err = c.AttrString("dNSHostName")
	if err != nil {
		return
	}

	return
}

func (gs *GlobalSettings) openParent(o *adsi.Object) (parent *adsi.Object, err error) {
	path, err := o.Parent()
	if err != nil {
		return
	}

	return gs.client.Open(path)
}

func (gs *GlobalSettings) openContainer(partialDN string) (*adsi.Container, error) {
	path := ldap(combineDN(partialDN, gs.domainDN))
	return gs.client.OpenContainer(path)
}
<|endoftext|>


<commit_before>// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"os"

	"github.com/bazelbuild/remote-apis-sdks/go/pkg/fakes"
	regrpc "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
	bsgrpc "google.golang.org/genproto/googleapis/bytestream"
	"google.golang.org/grpc"

	"go.chromium.org/luci/common/system/signals"
)

func main() {
	port := flag.Int("port", 9000, "local port number used by fake server")
	addrFile := flag.String("addr-file", "", "dump listening address in this file")
	flag.Parse()

	s := grpc.NewServer()
	cas := fakes.NewCAS()
	ex := &fakes.Exec{}
	bsgrpc.RegisterByteStreamServer(s, cas)
	regrpc.RegisterContentAddressableStorageServer(s, cas)
	regrpc.RegisterCapabilitiesServer(s, ex)

	lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v\n", err)
	}
	log.Printf("listening address: %s\n", lis.Addr())

	if *addrFile != "" {
		if err := os.WriteFile(*addrFile, []byte(lis.Addr().String()), 0600); err != nil {
			log.Fatalf("failed to write addrFile: %v", err)
		}
	}

	defer signals.HandleInterrupt(func() {
		log.Println("shutting down fake CAS gRPC server...")
		s.GracefulStop()
	})()

	log.Println("starting CAS fake server...")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve fake CAS gRPC server: %v\n", err)
	}
}
<commit_msg>[fakecas] Change fakecase to use dynamic port by default<commit_after>// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"flag"
	"fmt"
	"log"
	"net"
	"os"

	"github.com/bazelbuild/remote-apis-sdks/go/pkg/fakes"
	regrpc "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
	bsgrpc "google.golang.org/genproto/googleapis/bytestream"
	"google.golang.org/grpc"

	"go.chromium.org/luci/common/system/signals"
)

func main() {
	port := flag.Int("port", 0, "local port number used by fake server")
	addrFile := flag.String("addr-file", "", "dump listening address in this file")
	flag.Parse()

	s := grpc.NewServer()
	cas := fakes.NewCAS()
	ex := &fakes.Exec{}
	bsgrpc.RegisterByteStreamServer(s, cas)
	regrpc.RegisterContentAddressableStorageServer(s, cas)
	regrpc.RegisterCapabilitiesServer(s, ex)

	lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v\n", err)
	}
	log.Printf("listening address: %s\n", lis.Addr())

	if *addrFile != "" {
		if err := os.WriteFile(*addrFile, []byte(lis.Addr().String()), 0600); err != nil {
			log.Fatalf("failed to write addrFile: %v", err)
		}
	}

	defer signals.HandleInterrupt(func() {
		log.Println("shutting down fake CAS gRPC server...")
		s.GracefulStop()
	})()

	log.Println("starting CAS fake server...")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve fake CAS gRPC server: %v\n", err)
	}
}
<|endoftext|>


<commit_before>package acr

import (
	"bufio"
	"context"
	cr "github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2018-09-01/containerregistry"
	"github.com/Azure/go-autorest/autorest/azure/auth"
	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build"
	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/tag"
	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
	"github.com/pkg/errors"
	"io"
	"net/http"
	"regexp"
	"time"
)

const BUILD_STATUS_HEADER = "x-ms-meta-Complete"

func (b *Builder) Build(ctx context.Context, out io.Writer, tagger tag.Tagger, artifacts []*latest.Artifact) ([]build.Artifact, error) {
	return build.InParallel(ctx, out, tagger, artifacts, b.buildArtifact)
}

func (b *Builder) buildArtifact(ctx context.Context, out io.Writer, tagger tag.Tagger, artifact *latest.Artifact) (string, error) {
	client := cr.NewRegistriesClient(b.Credentials.SubscriptionId)
	authorizer, err := auth.NewClientCredentialsConfig(b.Credentials.ClientId, b.Credentials.ClientSecret, b.Credentials.TenantId).Authorizer()
	if err != nil {
		return "", errors.Wrap(err, "authorizing client")
	}
	client.Authorizer = authorizer

	result, err := client.GetBuildSourceUploadURL(ctx, b.ResourceGroup, b.ContainerRegistry)
	if err != nil {
		return "", errors.Wrap(err, "build source upload url")
	}
	blob := NewBlobStorage(*result.UploadURL)

	err = docker.CreateDockerTarGzContext(blob.Writer(), artifact.Workspace, artifact.DockerArtifact)
	if err != nil {
		return "", errors.Wrap(err, "create context tar.gz")
	}

	err = blob.UploadFileToBlob()
	if err != nil {
		return "", errors.Wrap(err, "upload file to blob")
	}

	imageTag, err := tagger.GenerateFullyQualifiedImageName(artifact.Workspace, &tag.Options{
		Digest:    util.RandomID(),
		ImageName: artifact.ImageName,
	})
	if err != nil {
		return "", errors.Wrap(err, "create fully qualified image name")
	}

	imageTag, err = getImageTagWithoutFQDN(imageTag)
	if err != nil {
		return "", errors.Wrap(err, "get azure image tag")
	}

	buildRequest := cr.DockerBuildRequest{
		ImageNames:     &[]string{imageTag},
		IsPushEnabled:  &[]bool{true}[0], //who invented bool pointers
		SourceLocation: result.RelativePath,
		Platform: &cr.PlatformProperties{
			Variant:      cr.V8,
			Os:           cr.Linux,
			Architecture: cr.Amd64,
		},
		DockerFilePath: &artifact.DockerArtifact.DockerfilePath,
		Type:           cr.TypeDockerBuildRequest,
	}
	future, err := client.ScheduleRun(ctx, b.ResourceGroup, b.ContainerRegistry, buildRequest)
	if err != nil {
		return "", errors.Wrap(err, "schedule build request")
	}

	run, err := future.Result(client)
	if err != nil {
		return "", errors.Wrap(err, "get run id")
	}
	runId := *run.RunID

	runsClient := cr.NewRunsClient(b.Credentials.SubscriptionId)
	runsClient.Authorizer = client.Authorizer
	logUrl, err := runsClient.GetLogSasURL(ctx, b.ResourceGroup, b.ContainerRegistry, runId)
	if err != nil {
		return "", errors.Wrap(err, "get log url")
	}

	err = pollBuildStatus(*logUrl.LogLink, out)
	if err != nil {
		return "", errors.Wrap(err, "polling build status")
	}

	return imageTag, nil
}

func pollBuildStatus(logUrl string, out io.Writer) error {
	offset := int32(0)
	for {
		resp, err := http.Get(logUrl)
		if err != nil {
			return err
		}

		if resp.StatusCode == http.StatusNotFound {
			//if blob is not available yet, try again
			time.Sleep(2 * time.Second)
			continue
		}

		scanner := bufio.NewScanner(resp.Body)
		line := int32(0)
		for scanner.Scan() {
			if line > offset {
				out.Write(scanner.Bytes())
				line++
				offset++
			}
		}
		resp.Body.Close()

		if offset > 0 {
			switch resp.Header.Get(BUILD_STATUS_HEADER) {
			case "": //run succeeded when there is no status header
				return nil
			case "internalerror":
			case "failed":
				return errors.New("run failed")
			case "timedout":
				return errors.New("run timed out")
			case "canceled":
				return errors.New("run was canceled")
			}
		}

		time.Sleep(2 * time.Second)
	}
}

// ACR needs the image tag in the following format
// <registryName>/<repository>:<tag>
func getImageTagWithoutFQDN(imageTag string) (string, error) {
	r, err := regexp.Compile("(.*)\\..*\\..*(/.*)")
	if err != nil {
		return "", errors.Wrap(err, "create regexp")
	}

	matches := r.FindStringSubmatch(imageTag)
	if len(matches) < 3 {
		return "", errors.New("invalid image tag")
	}

	return matches[1] + matches[2], nil
}
<commit_msg>always increment log lines<commit_after>package acr

import (
	"bufio"
	"context"
	cr "github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2018-09-01/containerregistry"
	"github.com/Azure/go-autorest/autorest/azure/auth"
	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build"
	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/tag"
	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
	"github.com/pkg/errors"
	"io"
	"net/http"
	"regexp"
	"time"
)

const BUILD_STATUS_HEADER = "x-ms-meta-Complete"

func (b *Builder) Build(ctx context.Context, out io.Writer, tagger tag.Tagger, artifacts []*latest.Artifact) ([]build.Artifact, error) {
	return build.InParallel(ctx, out, tagger, artifacts, b.buildArtifact)
}

func (b *Builder) buildArtifact(ctx context.Context, out io.Writer, tagger tag.Tagger, artifact *latest.Artifact) (string, error) {
	client := cr.NewRegistriesClient(b.Credentials.SubscriptionId)
	authorizer, err := auth.NewClientCredentialsConfig(b.Credentials.ClientId, b.Credentials.ClientSecret, b.Credentials.TenantId).Authorizer()
	if err != nil {
		return "", errors.Wrap(err, "authorizing client")
	}
	client.Authorizer = authorizer

	result, err := client.GetBuildSourceUploadURL(ctx, b.ResourceGroup, b.ContainerRegistry)
	if err != nil {
		return "", errors.Wrap(err, "build source upload url")
	}
	blob := NewBlobStorage(*result.UploadURL)

	err = docker.CreateDockerTarGzContext(blob.Writer(), artifact.Workspace, artifact.DockerArtifact)
	if err != nil {
		return "", errors.Wrap(err, "create context tar.gz")
	}

	err = blob.UploadFileToBlob()
	if err != nil {
		return "", errors.Wrap(err, "upload file to blob")
	}

	imageTag, err := tagger.GenerateFullyQualifiedImageName(artifact.Workspace, &tag.Options{
		Digest:    util.RandomID(),
		ImageName: artifact.ImageName,
	})
	if err != nil {
		return "", errors.Wrap(err, "create fully qualified image name")
	}

	imageTag, err = getImageTagWithoutFQDN(imageTag)
	if err != nil {
		return "", errors.Wrap(err, "get azure image tag")
	}

	buildRequest := cr.DockerBuildRequest{
		ImageNames:     &[]string{imageTag},
		IsPushEnabled:  &[]bool{true}[0], //who invented bool pointers
		SourceLocation: result.RelativePath,
		Platform: &cr.PlatformProperties{
			Variant:      cr.V8,
			Os:           cr.Linux,
			Architecture: cr.Amd64,
		},
		DockerFilePath: &artifact.DockerArtifact.DockerfilePath,
		Type:           cr.TypeDockerBuildRequest,
	}
	future, err := client.ScheduleRun(ctx, b.ResourceGroup, b.ContainerRegistry, buildRequest)
	if err != nil {
		return "", errors.Wrap(err, "schedule build request")
	}

	run, err := future.Result(client)
	if err != nil {
		return "", errors.Wrap(err, "get run id")
	}
	runId := *run.RunID

	runsClient := cr.NewRunsClient(b.Credentials.SubscriptionId)
	runsClient.Authorizer = client.Authorizer
	logUrl, err := runsClient.GetLogSasURL(ctx, b.ResourceGroup, b.ContainerRegistry, runId)
	if err != nil {
		return "", errors.Wrap(err, "get log url")
	}

	err = pollBuildStatus(*logUrl.LogLink, out)
	if err != nil {
		return "", errors.Wrap(err, "polling build status")
	}

	return imageTag, nil
}

func pollBuildStatus(logUrl string, out io.Writer) error {
	offset := int32(0)
	for {
		resp, err := http.Get(logUrl)
		if err != nil {
			return err
		}

		if resp.StatusCode == http.StatusNotFound {
			//if blob is not available yet, try again
			time.Sleep(2 * time.Second)
			continue
		}

		scanner := bufio.NewScanner(resp.Body)
		line := int32(0)
		for scanner.Scan() {
			if line > offset {
				out.Write(scanner.Bytes())
				offset++
			}
			line++
		}
		resp.Body.Close()

		if offset > 0 {
			switch resp.Header.Get(BUILD_STATUS_HEADER) {
			case "": //run succeeded when there is no status header
				return nil
			case "internalerror":
			case "failed":
				return errors.New("run failed")
			case "timedout":
				return errors.New("run timed out")
			case "canceled":
				return errors.New("run was canceled")
			}
		}

		time.Sleep(2 * time.Second)
	}
}

// ACR needs the image tag in the following format
// <registryName>/<repository>:<tag>
func getImageTagWithoutFQDN(imageTag string) (string, error) {
	r, err := regexp.Compile("(.*)\\..*\\..*(/.*)")
	if err != nil {
		return "", errors.Wrap(err, "create regexp")
	}

	matches := r.FindStringSubmatch(imageTag)
	if len(matches) < 3 {
		return "", errors.New("invalid image tag")
	}

	return matches[1] + matches[2], nil
}
<|endoftext|>


<commit_before>/*
Copyright 2018 The Skaffold Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package constants

import (
	"fmt"
	"runtime"

	"github.com/sirupsen/logrus"
)

const (
	// DefaultLogLevel is the default global verbosity
	DefaultLogLevel = logrus.WarnLevel

	// DefaultDockerfilePath is the dockerfile path is given relative to the
	// context directory
	DefaultDockerfilePath = "Dockerfile"

	DefaultDevTagStrategy = TagStrategySha256
	DefaultRunTagStrategy = TagStrategyGitCommit

	// TagStrategySha256 uses the checksum of the built artifact as the tag
	TagStrategySha256    = "sha256"
	TagStrategyGitCommit = "gitCommit"

	DefaultMinikubeContext         = "minikube"
	DefaultDockerForDesktopContext = "docker-for-desktop"
	GCSBucketSuffix                = "_cloudbuild"

	HelmOverridesFilename = "skaffold-overrides.yaml"

	DefaultKustomizationPath = "."

	DefaultKanikoImage             = "gcr.io/kaniko-project/executor@sha256:434bbb1d998ba1bd8ebc04c90d93afa859fd5c7ff93326bca9f6e7da0d6277ff"
	DefaultKanikoSecretName        = "kaniko-secret"
	DefaultKanikoTimeout           = "20m"
	DefaultKanikoContainerName     = "kaniko"
	DefaultKanikoEmptyDirName      = "kaniko-emptydir"
	DefaultKanikoEmptyDirMountPath = "/kaniko/buildcontext"

	DefaultAlpineImage = "alpine"

	UpdateCheckEnvironmentVariable = "SKAFFOLD_UPDATE_CHECK"

	DefaultCloudBuildDockerImage = "gcr.io/cloud-builders/docker"

	// A regex matching valid repository names (https://github.com/docker/distribution/blob/master/reference/reference.go)
	RepositoryComponentRegex string = `^[a-z\d]+(?:(?:[_.]|__|-+)[a-z\d]+)*$`
)

var DefaultKubectlManifests = []string{"k8s/*.yaml"}

var LatestDownloadURL = fmt.Sprintf("https://storage.googleapis.com/skaffold/releases/latest/skaffold-%s-%s", runtime.GOOS, runtime.GOARCH)

var Labels = struct {
	TagPolicy        string
	Deployer         string
	Builder          string
	DockerAPIVersion string
	DefaultLabels    map[string]string
}{
	DefaultLabels: map[string]string{
		"deployed-with": "skaffold",
	},
	TagPolicy:        "skaffold-tag-policy",
	Deployer:         "skaffold-deployer",
	Builder:          "skaffold-builder",
	DockerAPIVersion: "docker-api-version",
}
<commit_msg>Update kaniko image to latest version<commit_after>/*
Copyright 2018 The Skaffold Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package constants

import (
	"fmt"
	"runtime"

	"github.com/sirupsen/logrus"
)

const (
	// DefaultLogLevel is the default global verbosity
	DefaultLogLevel = logrus.WarnLevel

	// DefaultDockerfilePath is the dockerfile path is given relative to the
	// context directory
	DefaultDockerfilePath = "Dockerfile"

	DefaultDevTagStrategy = TagStrategySha256
	DefaultRunTagStrategy = TagStrategyGitCommit

	// TagStrategySha256 uses the checksum of the built artifact as the tag
	TagStrategySha256    = "sha256"
	TagStrategyGitCommit = "gitCommit"

	DefaultMinikubeContext         = "minikube"
	DefaultDockerForDesktopContext = "docker-for-desktop"
	GCSBucketSuffix                = "_cloudbuild"

	HelmOverridesFilename = "skaffold-overrides.yaml"

	DefaultKustomizationPath = "."

	DefaultKanikoImage             = "gcr.io/kaniko-project/executor:v0.7.0@sha256:0b4e0812aa17c54a9b8d8c8d7cb35559a892a341650acf7cb428c3e8cb4a3919"
	DefaultKanikoSecretName        = "kaniko-secret"
	DefaultKanikoTimeout           = "20m"
	DefaultKanikoContainerName     = "kaniko"
	DefaultKanikoEmptyDirName      = "kaniko-emptydir"
	DefaultKanikoEmptyDirMountPath = "/kaniko/buildcontext"

	DefaultAlpineImage = "alpine"

	UpdateCheckEnvironmentVariable = "SKAFFOLD_UPDATE_CHECK"

	DefaultCloudBuildDockerImage = "gcr.io/cloud-builders/docker"

	// A regex matching valid repository names (https://github.com/docker/distribution/blob/master/reference/reference.go)
	RepositoryComponentRegex string = `^[a-z\d]+(?:(?:[_.]|__|-+)[a-z\d]+)*$`
)

var DefaultKubectlManifests = []string{"k8s/*.yaml"}

var LatestDownloadURL = fmt.Sprintf("https://storage.googleapis.com/skaffold/releases/latest/skaffold-%s-%s", runtime.GOOS, runtime.GOARCH)

var Labels = struct {
	TagPolicy        string
	Deployer         string
	Builder          string
	DockerAPIVersion string
	DefaultLabels    map[string]string
}{
	DefaultLabels: map[string]string{
		"deployed-with": "skaffold",
	},
	TagPolicy:        "skaffold-tag-policy",
	Deployer:         "skaffold-deployer",
	Builder:          "skaffold-builder",
	DockerAPIVersion: "docker-api-version",
}
<|endoftext|>


<commit_before>/*
Copyright 2018 Google LLC

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 deploy

import (
	"bytes"
	"context"
	"fmt"
	"testing"

	"github.com/GoogleCloudPlatform/skaffold/pkg/skaffold/build"
	"github.com/GoogleCloudPlatform/skaffold/pkg/skaffold/config"
	"github.com/GoogleCloudPlatform/skaffold/pkg/skaffold/util"
	"github.com/GoogleCloudPlatform/skaffold/testutil"
	"github.com/spf13/afero"
)

const testKubeContext = "kubecontext"

const deploymentYAML = `apiVersion: apps/v1
kind: Deployment
metadata:
  name: leeroy-web
  labels:
    app: leeroy-web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: leeroy-web
  template:
    metadata:
      labels:
        app: leeroy-web
    spec:
      containers:
      - name: leeroy-web
        image: IMAGE_NAME
        ports:
		- containerPort: 8080
`

func TestKubectlRun(t *testing.T) {
	var tests = []struct {
		description string
		cfg         *config.DeployConfig
		b           *build.BuildResult
		command     util.Command

		expected  *Result
		shouldErr bool
	}{
		{
			description: "parameter mismatch",
			shouldErr:   true,
			cfg: &config.DeployConfig{
				DeployType: config.DeployType{
					KubectlDeploy: &config.KubectlDeploy{
						Manifests: []config.Manifest{
							{
								Paths: []string{"test/deployment.yaml"},
								Parameters: map[string]string{
									"IMAGE_NAME": "abc",
								},
							},
						},
					},
				},
			},
			b: &build.BuildResult{
				Builds: []build.Build{
					{
						ImageName: "not_abc",
						Tag:       "not_abc:123",
					},
				},
			},
		},
		{
			description: "missing manifest file",
			shouldErr:   true,
			cfg: &config.DeployConfig{
				DeployType: config.DeployType{
					KubectlDeploy: &config.KubectlDeploy{
						Manifests: []config.Manifest{
							{
								Paths: []string{"test/not_deployment.yaml"},
								Parameters: map[string]string{
									"IMAGE_NAME": "abc",
								},
							},
						},
					},
				},
			},
			b: &build.BuildResult{
				Builds: []build.Build{
					{
						ImageName: "abc",
						Tag:       "abc:123",
					},
				},
			},
		},
		{
			description: "deploy success",
			cfg: &config.DeployConfig{
				DeployType: config.DeployType{
					KubectlDeploy: &config.KubectlDeploy{
						Manifests: []config.Manifest{
							{
								Paths: []string{"test/deployment.yaml"},
								Parameters: map[string]string{
									"IMAGE_NAME": "abc",
								},
							},
						},
					},
				},
			},
			command: testutil.NewFakeRunCommand("", "", nil),
			b: &build.BuildResult{
				Builds: []build.Build{
					{
						ImageName: "abc",
						Tag:       "abc:123",
					},
				},
			},
			expected: &Result{},
		},
		{
			description: "deploy command error",
			shouldErr:   true,
			cfg: &config.DeployConfig{
				DeployType: config.DeployType{
					KubectlDeploy: &config.KubectlDeploy{
						Manifests: []config.Manifest{
							{
								Paths: []string{"test/not_deployment.yaml"},
								Parameters: map[string]string{
									"IMAGE_NAME": "abc",
								},
							},
						},
					},
				},
			},
			command: testutil.NewFakeRunCommand("", "", fmt.Errorf("")),
			b: &build.BuildResult{
				Builds: []build.Build{
					{
						ImageName: "abc",
						Tag:       "abc:123",
					},
				},
			},
		},
	}

	util.Fs = afero.NewMemMapFs()
	defer util.ResetFs()
	util.Fs.MkdirAll("test", 0750)
	files := map[string]string{
		"test/deployment.yaml": deploymentYAML,
	}
	for path, contents := range files {
		afero.WriteFile(util.Fs, path, []byte(contents), 0644)
	}

	for _, test := range tests {
		t.Run(test.description, func(t *testing.T) {
			if test.command != nil {
				util.DefaultExecCommand = test.command
				defer util.ResetDefaultExecCommand()
			}

			k := NewKubectlDeployer(test.cfg, testKubeContext)
			res, err := k.Deploy(context.Background(), &bytes.Buffer{}, test.b)

			testutil.CheckErrorAndDeepEqual(t, test.shouldErr, err, test.expected, res)
		})

	}
}

func TestReplaceParameters(t *testing.T) {
	manifest := "[IMAGE_NAME][IMAGE_NAME_OTHER][OTHER]"
	expectedManifest := "[image:v1][image_other:v1][other:v1]"

	manifest = replaceParameters(manifest, map[string]build.Build{
		"IMAGE_NAME":       {Tag: "image:v1"},
		"IMAGE_NAME_OTHER": {Tag: "image_other:v1"},
		"OTHER":            {Tag: "other:v1"},
	})

	if manifest != expectedManifest {
		t.Errorf("Expected: '%s'. Got: '%s'", expectedManifest, manifest)
	}
}
<commit_msg>kubectl: fix tests<commit_after>/*
Copyright 2018 Google LLC

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 deploy

import (
	"bytes"
	"context"
	"fmt"
	"testing"

	"github.com/GoogleCloudPlatform/skaffold/pkg/skaffold/build"
	"github.com/GoogleCloudPlatform/skaffold/pkg/skaffold/config"
	"github.com/GoogleCloudPlatform/skaffold/pkg/skaffold/util"
	"github.com/GoogleCloudPlatform/skaffold/testutil"
	"github.com/spf13/afero"
)

const testKubeContext = "kubecontext"

const deploymentYAML = `apiVersion: apps/v1
kind: Deployment
metadata:
  name: leeroy-web
  labels:
    app: leeroy-web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: leeroy-web
  template:
    metadata:
      labels:
        app: leeroy-web
    spec:
      containers:
      - name: leeroy-web
        image: leeroy-web-image
        ports:
		- containerPort: 8080
`

func TestKubectlRun(t *testing.T) {
	var tests = []struct {
		description string
		cfg         *config.DeployConfig
		b           *build.BuildResult
		command     util.Command

		expected  *Result
		shouldErr bool
	}{
		{
			description: "parameter mismatch",
			shouldErr:   true,
			cfg: &config.DeployConfig{
				DeployType: config.DeployType{
					KubectlDeploy: &config.KubectlDeploy{
						Manifests: []config.Manifest{
							{
								Paths:  []string{"test/deployment.yaml"},
								Images: []string{"leeroy-web-image"},
							},
						},
					},
				},
			},
			b: &build.BuildResult{
				Builds: []build.Build{
					{
						ImageName: "leeroy-web-image",
						Tag:       "leeroy-web-image:v1",
					},
				},
			},
		},
		{
			description: "missing manifest file",
			shouldErr:   true,
			cfg: &config.DeployConfig{
				DeployType: config.DeployType{
					KubectlDeploy: &config.KubectlDeploy{
						Manifests: []config.Manifest{
							{
								Paths:  []string{"test/not_deployment.yaml"},
								Images: []string{"leeroy-web-image"},
							},
						},
					},
				},
			},
			b: &build.BuildResult{
				Builds: []build.Build{
					{
						ImageName: "leeroy-web-image",
						Tag:       "leeroy-web-image:123",
					},
				},
			},
		},
		{
			description: "deploy success",
			cfg: &config.DeployConfig{
				DeployType: config.DeployType{
					KubectlDeploy: &config.KubectlDeploy{
						Manifests: []config.Manifest{
							{
								Paths:  []string{"test/deployment.yaml"},
								Images: []string{"leeroy-web-image"},
							},
						},
					},
				},
			},
			command: testutil.NewFakeRunCommand("", "", nil),
			b: &build.BuildResult{
				Builds: []build.Build{
					{
						ImageName: "leeroy-web-image",
						Tag:       "leeroy-web-image:123",
					},
				},
			},
			expected: &Result{},
		},
		{
			description: "deploy command error",
			shouldErr:   true,
			cfg: &config.DeployConfig{
				DeployType: config.DeployType{
					KubectlDeploy: &config.KubectlDeploy{
						Manifests: []config.Manifest{
							{
								Paths:  []string{"test/not_deployment.yaml"},
								Images: []string{"leeroy-web-image"},
							},
						},
					},
				},
			},
			command: testutil.NewFakeRunCommand("", "", fmt.Errorf("")),
			b: &build.BuildResult{
				Builds: []build.Build{
					{
						ImageName: "leeroy-web-image",
						Tag:       "leeroy-web-image:123",
					},
				},
			},
		},
	}

	util.Fs = afero.NewMemMapFs()
	defer util.ResetFs()
	util.Fs.MkdirAll("test", 0750)
	files := map[string]string{
		"test/deployment.yaml": deploymentYAML,
	}
	for path, contents := range files {
		afero.WriteFile(util.Fs, path, []byte(contents), 0644)
	}

	for _, test := range tests {
		t.Run(test.description, func(t *testing.T) {
			if test.command != nil {
				util.DefaultExecCommand = test.command
				defer util.ResetDefaultExecCommand()
			}

			k := NewKubectlDeployer(test.cfg, testKubeContext)
			res, err := k.Deploy(context.Background(), &bytes.Buffer{}, test.b)

			testutil.CheckErrorAndDeepEqual(t, test.shouldErr, err, test.expected, res)
		})

	}
}

func TestReplaceParameters(t *testing.T) {
	manifest := "[IMAGE_NAME][IMAGE_NAME_OTHER][OTHER]"
	expectedManifest := "[image:v1][image_other:v1][other:v1]"

	manifest = replaceParameters(manifest, map[string]build.Build{
		"IMAGE_NAME":       {Tag: "image:v1"},
		"IMAGE_NAME_OTHER": {Tag: "image_other:v1"},
		"OTHER":            {Tag: "other:v1"},
	})

	if manifest != expectedManifest {
		t.Errorf("Expected: '%s'. Got: '%s'", expectedManifest, manifest)
	}
}
<|endoftext|>


<commit_before>// +build !nounit

package smokescreen

import (
	"fmt"
	"log"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"sync/atomic"
	"testing"
	"time"

	"github.com/sirupsen/logrus"
	logrustest "github.com/sirupsen/logrus/hooks/test"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stripe/smokescreen/pkg/smokescreen/conntrack"
)

var allowRanges = []string{
	"8.8.9.0/24",
	"10.0.1.0/24",
	"172.16.1.0/24",
	"192.168.1.0/24",
	"127.0.1.0/24",
}
var allowAddresses = []string{
	"10.0.0.1:321",
}
var denyRanges = []string{
	"1.1.1.1/32",
}
var denyAddresses = []string{
	"8.8.8.8:321",
}

type testCase struct {
	ip       string
	port     int
	expected ipType
}

func TestClassifyAddr(t *testing.T) {
	a := assert.New(t)

	conf := NewConfig()
	a.NoError(conf.SetDenyRanges(denyRanges))
	a.NoError(conf.SetDenyAddresses(denyAddresses))
	a.NoError(conf.SetAllowRanges(allowRanges))
	a.NoError(conf.SetAllowAddresses(allowAddresses))
	conf.ConnectTimeout = 10 * time.Second
	conf.ExitTimeout = 10 * time.Second
	conf.AdditionalErrorMessageOnDeny = "Proxy denied"

	testIPs := []testCase{
		testCase{"8.8.8.8", 1, ipAllowDefault},
		testCase{"8.8.9.8", 1, ipAllowUserConfigured},

		// Specific blocked networks
		testCase{"10.0.0.1", 1, ipDenyPrivateRange},
		testCase{"10.0.0.1", 321, ipAllowUserConfigured},
		testCase{"10.0.1.1", 1, ipAllowUserConfigured},
		testCase{"172.16.0.1", 1, ipDenyPrivateRange},
		testCase{"172.16.1.1", 1, ipAllowUserConfigured},
		testCase{"192.168.0.1", 1, ipDenyPrivateRange},
		testCase{"192.168.1.1", 1, ipAllowUserConfigured},
		testCase{"8.8.8.8", 321, ipDenyUserConfigured},
		testCase{"1.1.1.1", 1, ipDenyUserConfigured},

		// localhost
		testCase{"127.0.0.1", 1, ipDenyNotGlobalUnicast},
		testCase{"127.255.255.255", 1, ipDenyNotGlobalUnicast},
		testCase{"::1", 1, ipDenyNotGlobalUnicast},
		testCase{"127.0.1.1", 1, ipAllowUserConfigured},

		// ec2 metadata endpoint
		testCase{"169.254.169.254", 1, ipDenyNotGlobalUnicast},

		// Broadcast addresses
		testCase{"255.255.255.255", 1, ipDenyNotGlobalUnicast},
		testCase{"ff02:0:0:0:0:0:0:2", 1, ipDenyNotGlobalUnicast},
	}

	for _, test := range testIPs {
		localIP := net.ParseIP(test.ip)
		if localIP == nil {
			t.Errorf("Could not parse IP from string: %s", test.ip)
			continue
		}
		localAddr := net.TCPAddr{
			IP:   localIP,
			Port: test.port,
		}

		got := classifyAddr(conf, &localAddr)
		if got != test.expected {
			t.Errorf("Misclassified IP (%s): should be %s, but is instead %s.", localIP, test.expected, got)
		}
	}
}

func TestClearsErrorHeader(t *testing.T) {
	r := require.New(t)

	log.SetFlags(log.LstdFlags | log.Lshortfile)

	proxySrv, _, err := proxyServer()
	r.NoError(err)
	defer proxySrv.Close()

	// Create a http.Client that uses our proxy
	client, err := proxyClient(proxySrv.URL)
	r.NoError(err)

	// Talk "through" the proxy to our malicious upstream that sets the
	// error header.
	resp, err := client.Get("http://httpbin.org/response-headers?X-Smokescreen-Error=foobar&X-Smokescreen-Test=yes")
	r.NoError(err)

	// Should succeed
	if resp.StatusCode != 200 {
		t.Errorf("response had bad status: expected 200, got %d", resp.StatusCode)
	}

	// Verify the error header is not set.
	if h := resp.Header.Get(errorHeader); h != "" {
		t.Errorf("proxy did not strip %q header: %q", errorHeader, h)
	}

	// Verify we did get the other header, to confirm we're talking to the right thing
	if h := resp.Header.Get("X-Smokescreen-Test"); h != "yes" {
		t.Errorf("did not get expected header X-Smokescreen-Test: expected \"yes\", got %q", h)
	}
}

func TestConsistentHostHeader(t *testing.T) {
	r := require.New(t)
	a := assert.New(t)

	hostCh := make(chan string)

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("OK"))
		hostCh <- r.Host
	}))
	defer ts.Close()

	// Custom proxy config for the "remote" httptest.NewServer
	conf := NewConfig()
	conf.ConnTracker = conntrack.NewTracker(conf.IdleThreshold, nil, conf.Log, atomic.Value{})
	err := conf.SetAllowAddresses([]string{"127.0.0.1"})
	r.NoError(err)

	proxy := BuildProxy(conf)
	proxySrv := httptest.NewServer(proxy)

	client, err := proxyClient(proxySrv.URL)
	r.NoError(err)

	req, err := http.NewRequest("GET", ts.URL, nil)
	r.NoError(err)

	expectedHostHeader := req.Host
	go client.Do(req)

	select {
	case receivedHostHeader := <-hostCh:
		a.Equal(expectedHostHeader, receivedHostHeader)
	case <-time.After(3 * time.Second):
		t.Fatal("timed out waiting for client request")
	}
}

func TestClearsTraceIDHeader(t *testing.T) {
	r := require.New(t)
	a := assert.New(t)

	headerCh := make(chan string)

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("OK"))
		headerCh <- r.Header.Get("X-Smokescreen-Trace-ID")
	}))
	defer ts.Close()

	// Custom proxy config for the "remote" httptest.NewServer
	conf := NewConfig()
	conf.ConnTracker = conntrack.NewTracker(conf.IdleThreshold, nil, conf.Log, atomic.Value{})
	err := conf.SetAllowAddresses([]string{"127.0.0.1"})
	r.NoError(err)

	proxy := BuildProxy(conf)
	proxySrv := httptest.NewServer(proxy)

	client, err := proxyClient(proxySrv.URL)
	r.NoError(err)

	req, err := http.NewRequest("GET", ts.URL, nil)
	r.NoError(err)
	req.Header.Set("X-Smokescreen-Trace-ID", "7fa4587f-7362-4515-ba44-e44490241af0")

	go client.Do(req)

	select {
	case receivedTraceIDCh := <-headerCh:
		a.Empty(receivedTraceIDCh)
	case <-time.After(3 * time.Second):
		t.Fatal("timed out waiting for client request")
	}
}

func TestShuttingDownValue(t *testing.T) {
	a := assert.New(t)

	conf := NewConfig()
	conf.Port = 39381

	quit := make(chan interface{})
	go StartWithConfig(conf, quit)

	// These sleeps are not ideal, but there is a race with checking the
	// ShuttingDown value from these tests. The server has to bootstrap
	// itself with an initial value before it returns false, and has to
	// set the value to true after we send on the quit channel.
	time.Sleep(500 * time.Millisecond)
	a.Equal(false, conf.ShuttingDown.Load())

	quit <- true

	time.Sleep(500 * time.Millisecond)
	a.Equal(true, conf.ShuttingDown.Load())

}

func TestHealthcheck(t *testing.T) {
	r := require.New(t)
	a := assert.New(t)

	healthcheckCh := make(chan string)

	testHealthcheck := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("OK"))
		healthcheckCh <- "OK"
	})

	conf := NewConfig()

	// We set this here so that we can deterministically test the Healthcheck
	// handler. Otherwise we would have to call StartWithConfig() in a goroutine,
	// which creates a race between the test and the listener accepting
	// connections.
	handler := HealthcheckMiddleware{
		Proxy:       BuildProxy(conf),
		Healthcheck: testHealthcheck,
	}

	server := httptest.NewServer(handler)

	go func() {
		select {
		case healthy := <-healthcheckCh:
			a.Equal("OK", healthy)
		case <-time.After(5 * time.Second):
			t.Fatal("timed out waiting for client request")
		}
	}()

	resp, err := http.Get(fmt.Sprintf("%s/healthcheck", server.URL))
	r.NoError(err)
	a.Equal(http.StatusOK, resp.StatusCode)
}

var invalidHostCases = []struct {
	scheme    string
	expectErr bool
	proxyType string
}{
	{"http", false, "http"},
	{"https", true, "connect"},
}

func TestInvalidHost(t *testing.T) {
	for _, testCase := range invalidHostCases {
		t.Run(testCase.scheme, func(t *testing.T) {
			a := assert.New(t)
			r := require.New(t)

			proxySrv, logHook, err := proxyServer()
			require.NoError(t, err)
			defer proxySrv.Close()

			// Create a http.Client that uses our proxy
			client, err := proxyClient(proxySrv.URL)
			r.NoError(err)

			resp, err := client.Get(fmt.Sprintf("%s://neversaynever.stripe.com", testCase.scheme))
			if testCase.expectErr {
				r.EqualError(err, "Get https://neversaynever.stripe.com: Request Rejected by Proxy")
			} else {
				r.NoError(err)
				r.Equal(http.StatusProxyAuthRequired, resp.StatusCode)
			}

			entry := findCanonicalProxyDecision(logHook.AllEntries())
			r.NotNil(entry)

			if a.Contains(entry.Data, "allow") {
				a.Equal(true, entry.Data["allow"])
			}
			if a.Contains(entry.Data, "error") {
				a.Contains(entry.Data["error"], "no such host")
			}
			if a.Contains(entry.Data, "proxy_type") {
				a.Contains(entry.Data["proxy_type"], testCase.proxyType)
			}
		})
	}
}

func findCanonicalProxyDecision(logs []*logrus.Entry) *logrus.Entry {
	for _, entry := range logs {
		if entry.Message == LOGLINE_CANONICAL_PROXY_DECISION {
			return entry
		}
	}
	return nil
}

func proxyServer() (*httptest.Server, *logrustest.Hook, error) {
	var logHook logrustest.Hook

	conf := NewConfig()
	conf.Port = 39381
	if err := conf.SetAllowRanges(allowRanges); err != nil {
		return nil, nil, err
	}
	conf.ConnectTimeout = 10 * time.Second
	conf.ExitTimeout = 10 * time.Second
	conf.AdditionalErrorMessageOnDeny = "Proxy denied"
	conf.Resolver = &net.Resolver{}
	conf.Log.AddHook(&logHook)
	conf.ConnTracker = conntrack.NewTracker(conf.IdleThreshold, nil, conf.Log, atomic.Value{})

	proxy := BuildProxy(conf)
	return httptest.NewServer(proxy), &logHook, nil
}

func proxyClient(proxy string) (*http.Client, error) {
	proxyUrl, err := url.Parse(proxy)
	if err != nil {
		return nil, err
	}

	return &http.Client{
		Transport: &http.Transport{
			Proxy:                 http.ProxyURL(proxyUrl),
			TLSHandshakeTimeout:   10 * time.Second,
			ExpectContinueTimeout: 1 * time.Second,
		},
	}, nil
}
<commit_msg>Update test to check logs<commit_after>// +build !nounit

package smokescreen

import (
	"fmt"
	"log"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"sync/atomic"
	"testing"
	"time"

	"github.com/sirupsen/logrus"
	logrustest "github.com/sirupsen/logrus/hooks/test"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stripe/smokescreen/pkg/smokescreen/conntrack"
)

var allowRanges = []string{
	"8.8.9.0/24",
	"10.0.1.0/24",
	"172.16.1.0/24",
	"192.168.1.0/24",
	"127.0.1.0/24",
}
var allowAddresses = []string{
	"10.0.0.1:321",
}
var denyRanges = []string{
	"1.1.1.1/32",
}
var denyAddresses = []string{
	"8.8.8.8:321",
}

type testCase struct {
	ip       string
	port     int
	expected ipType
}

func TestClassifyAddr(t *testing.T) {
	a := assert.New(t)

	conf := NewConfig()
	a.NoError(conf.SetDenyRanges(denyRanges))
	a.NoError(conf.SetDenyAddresses(denyAddresses))
	a.NoError(conf.SetAllowRanges(allowRanges))
	a.NoError(conf.SetAllowAddresses(allowAddresses))
	conf.ConnectTimeout = 10 * time.Second
	conf.ExitTimeout = 10 * time.Second
	conf.AdditionalErrorMessageOnDeny = "Proxy denied"

	testIPs := []testCase{
		testCase{"8.8.8.8", 1, ipAllowDefault},
		testCase{"8.8.9.8", 1, ipAllowUserConfigured},

		// Specific blocked networks
		testCase{"10.0.0.1", 1, ipDenyPrivateRange},
		testCase{"10.0.0.1", 321, ipAllowUserConfigured},
		testCase{"10.0.1.1", 1, ipAllowUserConfigured},
		testCase{"172.16.0.1", 1, ipDenyPrivateRange},
		testCase{"172.16.1.1", 1, ipAllowUserConfigured},
		testCase{"192.168.0.1", 1, ipDenyPrivateRange},
		testCase{"192.168.1.1", 1, ipAllowUserConfigured},
		testCase{"8.8.8.8", 321, ipDenyUserConfigured},
		testCase{"1.1.1.1", 1, ipDenyUserConfigured},

		// localhost
		testCase{"127.0.0.1", 1, ipDenyNotGlobalUnicast},
		testCase{"127.255.255.255", 1, ipDenyNotGlobalUnicast},
		testCase{"::1", 1, ipDenyNotGlobalUnicast},
		testCase{"127.0.1.1", 1, ipAllowUserConfigured},

		// ec2 metadata endpoint
		testCase{"169.254.169.254", 1, ipDenyNotGlobalUnicast},

		// Broadcast addresses
		testCase{"255.255.255.255", 1, ipDenyNotGlobalUnicast},
		testCase{"ff02:0:0:0:0:0:0:2", 1, ipDenyNotGlobalUnicast},
	}

	for _, test := range testIPs {
		localIP := net.ParseIP(test.ip)
		if localIP == nil {
			t.Errorf("Could not parse IP from string: %s", test.ip)
			continue
		}
		localAddr := net.TCPAddr{
			IP:   localIP,
			Port: test.port,
		}

		got := classifyAddr(conf, &localAddr)
		if got != test.expected {
			t.Errorf("Misclassified IP (%s): should be %s, but is instead %s.", localIP, test.expected, got)
		}
	}
}

func TestClearsErrorHeader(t *testing.T) {
	r := require.New(t)

	log.SetFlags(log.LstdFlags | log.Lshortfile)

	proxySrv, _, err := proxyServer()
	r.NoError(err)
	defer proxySrv.Close()

	// Create a http.Client that uses our proxy
	client, err := proxyClient(proxySrv.URL)
	r.NoError(err)

	// Talk "through" the proxy to our malicious upstream that sets the
	// error header.
	resp, err := client.Get("http://httpbin.org/response-headers?X-Smokescreen-Error=foobar&X-Smokescreen-Test=yes")
	r.NoError(err)

	// Should succeed
	if resp.StatusCode != 200 {
		t.Errorf("response had bad status: expected 200, got %d", resp.StatusCode)
	}

	// Verify the error header is not set.
	if h := resp.Header.Get(errorHeader); h != "" {
		t.Errorf("proxy did not strip %q header: %q", errorHeader, h)
	}

	// Verify we did get the other header, to confirm we're talking to the right thing
	if h := resp.Header.Get("X-Smokescreen-Test"); h != "yes" {
		t.Errorf("did not get expected header X-Smokescreen-Test: expected \"yes\", got %q", h)
	}
}

func TestConsistentHostHeader(t *testing.T) {
	r := require.New(t)
	a := assert.New(t)

	hostCh := make(chan string)

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("OK"))
		hostCh <- r.Host
	}))
	defer ts.Close()

	// Custom proxy config for the "remote" httptest.NewServer
	conf := NewConfig()
	conf.ConnTracker = conntrack.NewTracker(conf.IdleThreshold, nil, conf.Log, atomic.Value{})
	err := conf.SetAllowAddresses([]string{"127.0.0.1"})
	r.NoError(err)

	proxy := BuildProxy(conf)
	proxySrv := httptest.NewServer(proxy)

	client, err := proxyClient(proxySrv.URL)
	r.NoError(err)

	req, err := http.NewRequest("GET", ts.URL, nil)
	r.NoError(err)

	expectedHostHeader := req.Host
	go client.Do(req)

	select {
	case receivedHostHeader := <-hostCh:
		a.Equal(expectedHostHeader, receivedHostHeader)
	case <-time.After(3 * time.Second):
		t.Fatal("timed out waiting for client request")
	}
}

func TestClearsTraceIDHeader(t *testing.T) {
	r := require.New(t)
	a := assert.New(t)

	headerCh := make(chan string)
	respCh := make(chan bool)

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("OK"))
		headerCh <- r.Header.Get("X-Smokescreen-Trace-ID")
	}))
	defer ts.Close()

	// Custom proxy config for the "remote" httptest.NewServer
	var logHook logrustest.Hook
	conf := NewConfig()
	conf.Log.AddHook(&logHook)
	conf.ConnTracker = conntrack.NewTracker(conf.IdleThreshold, nil, conf.Log, atomic.Value{})
	err := conf.SetAllowAddresses([]string{"127.0.0.1"})
	r.NoError(err)

	proxy := BuildProxy(conf)
	proxySrv := httptest.NewServer(proxy)

	client, err := proxyClient(proxySrv.URL)
	r.NoError(err)

	req, err := http.NewRequest("GET", ts.URL, nil)
	r.NoError(err)
	req.Header.Set("X-Smokescreen-Trace-ID", "7fa4587f-7362-4515-ba44-e44490241af0")

	go func() {
		client.Do(req)
		respCh <- true
	}()

	for i := 0; i < 2; i++ {
		select {
		case receivedTraceIDCh := <-headerCh:
			a.Empty(receivedTraceIDCh)
		case <-time.After(3 * time.Second):
			t.Fatal("timed out waiting for client request")
		case <-respCh:
			entry := findCanonicalProxyDecision(logHook.AllEntries())
			r.NotNil(entry)
			a.NotEmpty(entry.Data["smokescreen_trace_id"])
		}
	}
}

func TestShuttingDownValue(t *testing.T) {
	a := assert.New(t)

	conf := NewConfig()
	conf.Port = 39381

	quit := make(chan interface{})
	go StartWithConfig(conf, quit)

	// These sleeps are not ideal, but there is a race with checking the
	// ShuttingDown value from these tests. The server has to bootstrap
	// itself with an initial value before it returns false, and has to
	// set the value to true after we send on the quit channel.
	time.Sleep(500 * time.Millisecond)
	a.Equal(false, conf.ShuttingDown.Load())

	quit <- true

	time.Sleep(500 * time.Millisecond)
	a.Equal(true, conf.ShuttingDown.Load())

}

func TestHealthcheck(t *testing.T) {
	r := require.New(t)
	a := assert.New(t)

	healthcheckCh := make(chan string)

	testHealthcheck := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("OK"))
		healthcheckCh <- "OK"
	})

	conf := NewConfig()

	// We set this here so that we can deterministically test the Healthcheck
	// handler. Otherwise we would have to call StartWithConfig() in a goroutine,
	// which creates a race between the test and the listener accepting
	// connections.
	handler := HealthcheckMiddleware{
		Proxy:       BuildProxy(conf),
		Healthcheck: testHealthcheck,
	}

	server := httptest.NewServer(handler)

	go func() {
		select {
		case healthy := <-healthcheckCh:
			a.Equal("OK", healthy)
		case <-time.After(5 * time.Second):
			t.Fatal("timed out waiting for client request")
		}
	}()

	resp, err := http.Get(fmt.Sprintf("%s/healthcheck", server.URL))
	r.NoError(err)
	a.Equal(http.StatusOK, resp.StatusCode)
}

var invalidHostCases = []struct {
	scheme    string
	expectErr bool
	proxyType string
}{
	{"http", false, "http"},
	{"https", true, "connect"},
}

func TestInvalidHost(t *testing.T) {
	for _, testCase := range invalidHostCases {
		t.Run(testCase.scheme, func(t *testing.T) {
			a := assert.New(t)
			r := require.New(t)

			proxySrv, logHook, err := proxyServer()
			require.NoError(t, err)
			defer proxySrv.Close()

			// Create a http.Client that uses our proxy
			client, err := proxyClient(proxySrv.URL)
			r.NoError(err)

			resp, err := client.Get(fmt.Sprintf("%s://neversaynever.stripe.com", testCase.scheme))
			if testCase.expectErr {
				r.EqualError(err, "Get https://neversaynever.stripe.com: Request Rejected by Proxy")
			} else {
				r.NoError(err)
				r.Equal(http.StatusProxyAuthRequired, resp.StatusCode)
			}

			entry := findCanonicalProxyDecision(logHook.AllEntries())
			r.NotNil(entry)

			if a.Contains(entry.Data, "allow") {
				a.Equal(true, entry.Data["allow"])
			}
			if a.Contains(entry.Data, "error") {
				a.Contains(entry.Data["error"], "no such host")
			}
			if a.Contains(entry.Data, "proxy_type") {
				a.Contains(entry.Data["proxy_type"], testCase.proxyType)
			}
		})
	}
}

func findCanonicalProxyDecision(logs []*logrus.Entry) *logrus.Entry {
	for _, entry := range logs {
		if entry.Message == LOGLINE_CANONICAL_PROXY_DECISION {
			return entry
		}
	}
	return nil
}

func proxyServer() (*httptest.Server, *logrustest.Hook, error) {
	var logHook logrustest.Hook

	conf := NewConfig()
	conf.Port = 39381
	if err := conf.SetAllowRanges(allowRanges); err != nil {
		return nil, nil, err
	}
	conf.ConnectTimeout = 10 * time.Second
	conf.ExitTimeout = 10 * time.Second
	conf.AdditionalErrorMessageOnDeny = "Proxy denied"
	conf.Resolver = &net.Resolver{}
	conf.Log.AddHook(&logHook)
	conf.ConnTracker = conntrack.NewTracker(conf.IdleThreshold, nil, conf.Log, atomic.Value{})

	proxy := BuildProxy(conf)
	return httptest.NewServer(proxy), &logHook, nil
}

func proxyClient(proxy string) (*http.Client, error) {
	proxyUrl, err := url.Parse(proxy)
	if err != nil {
		return nil, err
	}

	return &http.Client{
		Transport: &http.Transport{
			Proxy:                 http.ProxyURL(proxyUrl),
			TLSHandshakeTimeout:   10 * time.Second,
			ExpectContinueTimeout: 1 * time.Second,
		},
	}, nil
}
<|endoftext|>


<commit_before>package dishes

import "math/rand"

// randNumMorsels returns a random number of morsels between 5 and 10
func randNumMorsels() int {
	return 5 + rand.Intn(5)
}

// Manager is an actor that manages the internal list of dishes and number of morsels left in each.
type Manager struct {
	// The slice of shared dishes. This is the shared state that the Manager actor is managing.
	dishes []dish
	// This is the message passing channel.
	//
	// The manager receive loop only reads from it (in a 'range Ch') and writes to the 'chan *string' channel values.
	//
	// The message sender must only write to Ch and then read from the channel values. Simply close this channel to terminate the receive loop.
	//
	// The manager will write a valid dish name back on the 'chan *string' if there are any left, or nil if there are none left.
	Ch chan chan *string
}

// NewManager creates a new Manager actor and starts the receive loop in a background goroutine.
//
// Example usage:
//
//  mgr := NewManager()
//  // get a dish that has morsels left
//  dishNameCh := make(chan *string)
//  mgr.Ch <- dishNameCh
//  dishName := <-dishNameCh
//  if dishName == nil {
//    // there are no more morsels left of any dish!
//  } else {
//    fmt.Println("got a morsel of %s!", *dishName)
//  }
func NewManager() *Manager {
	ch := make(chan chan *string)
	mgr := &Manager{
		// this is the shared state that we're managing
		dishes: []dish{
			{name: "chorizo", numMorsels: randNumMorsels()},
			{name: "chopitos", numMorsels: randNumMorsels()},
			{name: "pimientos de padrón", numMorsels: randNumMorsels()},
			{name: "croquetas", numMorsels: randNumMorsels()},
			{name: "patatas bravas", numMorsels: randNumMorsels()},
		},
		Ch: ch,
	}

	go func() {
		for retCh := range mgr.Ch {
			if len(mgr.dishes) == 0 {
				retCh <- nil
				continue
			}
			idx := rand.Intn(len(mgr.dishes))
			dish := &mgr.dishes[idx]
			dish.numMorsels--
			if dish.numMorsels == 0 {
				// remove the dish from the list
				mgr.dishes = append(mgr.dishes[0:idx], mgr.dishes[idx+1:]...)
			}
			retCh <- &dish.name
		}
	}()
	return mgr
}

// randDishIdx returns a random index into the dishes slice, or -1 if the slice
// is empty
func (m *Manager) randDishIdx() int {
	if len(m.dishes) == 0 {
		return -1
	}
	return rand.Intn(len(m.dishes))
}
<commit_msg>moving un-exported code to bottom<commit_after>package dishes

import "math/rand"

// Manager is an actor that manages the internal list of dishes and number of morsels left in each.
type Manager struct {
	// The slice of shared dishes. This is the shared state that the Manager actor is managing.
	dishes []dish
	// This is the message passing channel.
	//
	// The manager receive loop only reads from it (in a 'range Ch') and writes to the 'chan *string' channel values.
	//
	// The message sender must only write to Ch and then read from the channel values. Simply close this channel to terminate the receive loop.
	//
	// The manager will write a valid dish name back on the 'chan *string' if there are any left, or nil if there are none left.
	Ch chan chan *string
}

// NewManager creates a new Manager actor and starts the receive loop in a background goroutine.
//
// Example usage:
//
//  mgr := NewManager()
//  // get a dish that has morsels left
//  dishNameCh := make(chan *string)
//  mgr.Ch <- dishNameCh
//  dishName := <-dishNameCh
//  if dishName == nil {
//    // there are no more morsels left of any dish!
//  } else {
//    fmt.Println("got a morsel of %s!", *dishName)
//  }
func NewManager() *Manager {
	ch := make(chan chan *string)
	mgr := &Manager{
		// this is the shared state that we're managing
		dishes: []dish{
			{name: "chorizo", numMorsels: randNumMorsels()},
			{name: "chopitos", numMorsels: randNumMorsels()},
			{name: "pimientos de padrón", numMorsels: randNumMorsels()},
			{name: "croquetas", numMorsels: randNumMorsels()},
			{name: "patatas bravas", numMorsels: randNumMorsels()},
		},
		Ch: ch,
	}

	go func() {
		for retCh := range mgr.Ch {
			if len(mgr.dishes) == 0 {
				retCh <- nil
				continue
			}
			idx := rand.Intn(len(mgr.dishes))
			dish := &mgr.dishes[idx]
			dish.numMorsels--
			if dish.numMorsels == 0 {
				// remove the dish from the list
				mgr.dishes = append(mgr.dishes[0:idx], mgr.dishes[idx+1:]...)
			}
			retCh <- &dish.name
		}
	}()
	return mgr
}

// randDishIdx returns a random index into the dishes slice, or -1 if the slice
// is empty
func (m *Manager) randDishIdx() int {
	if len(m.dishes) == 0 {
		return -1
	}
	return rand.Intn(len(m.dishes))
}

type dish struct {
	name       string
	numMorsels int
}

// randNumMorsels returns a random number of morsels between 5 and 10
func randNumMorsels() int {
	return 5 + rand.Intn(5)
}
<|endoftext|>


<commit_before>/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you 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 electron

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

func testAuthClientServer(t *testing.T, copts []ConnectionOption, sopts []ConnectionOption) (got connectionSettings, err error) {
	client, server := newClientServerOpts(t, copts, sopts)
	defer closeClientServer(client, server)

	go func() {
		for in := range server.Incoming() {
			switch in := in.(type) {
			case *IncomingConnection:
				got = connectionSettings{user: in.User(), virtualHost: in.VirtualHost()}
			}
			in.Accept()
		}
	}()

	err = client.Sync()
	return
}

func TestAuthAnonymous(t *testing.T) {
	fatalIf(t, configureSASL())
	got, err := testAuthClientServer(t,
		[]ConnectionOption{User("fred"), VirtualHost("vhost"), SASLAllowInsecure(true)},
		[]ConnectionOption{SASLAllowedMechs("ANONYMOUS"), SASLAllowInsecure(true)})
	fatalIf(t, err)
	errorIf(t, checkEqual(connectionSettings{user: "anonymous", virtualHost: "vhost"}, got))
}

func TestAuthPlain(t *testing.T) {
	if !SASLExtended() {
		t.Skip()
	}
	fatalIf(t, configureSASL())
	got, err := testAuthClientServer(t,
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN"), User("fred@proton"), Password([]byte("xxx"))},
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN")})
	fatalIf(t, err)
	errorIf(t, checkEqual(connectionSettings{user: "fred@proton"}, got))
}

func TestAuthBadPass(t *testing.T) {
	if !SASLExtended() {
		t.Skip()
	}
	fatalIf(t, configureSASL())
	_, err := testAuthClientServer(t,
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN"), User("fred@proton"), Password([]byte("yyy"))},
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN")})
	if err == nil {
		t.Error("Expected auth failure for bad pass")
	}
}

func TestAuthBadUser(t *testing.T) {
	if !SASLExtended() {
		t.Skip()
	}
	fatalIf(t, configureSASL())
	_, err := testAuthClientServer(t,
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN"), User("foo@bar"), Password([]byte("yyy"))},
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN")})
	if err == nil {
		t.Error("Expected auth failure for bad user")
	}
}

var confDir string
var confErr error

func configureSASL() error {
	if confDir != "" || confErr != nil {
		return confErr
	}
	confDir, confErr = ioutil.TempDir("", "")
	if confErr != nil {
		return confErr
	}

	GlobalSASLConfigDir(confDir)
	GlobalSASLConfigName("test")
	conf := filepath.Join(confDir, "test.conf")

	db := filepath.Join(confDir, "proton.sasldb")
	cmd := exec.Command("saslpasswd2", "-c", "-p", "-f", db, "-u", "proton", "fred")
	cmd.Stdin = strings.NewReader("xxx") // Password
	if out, err := cmd.CombinedOutput(); err != nil {
		confErr = fmt.Errorf("saslpasswd2 failed: %s\n%s", err, out)
		return confErr
	}
	confStr := "sasldb_path: " + db + "\nmech_list: EXTERNAL DIGEST-MD5 SCRAM-SHA-1 CRAM-MD5 PLAIN ANONYMOUS\n"
	if err := ioutil.WriteFile(conf, []byte(confStr), os.ModePerm); err != nil {
		confErr = fmt.Errorf("write conf file %s failed: %s", conf, err)
	}
	return confErr
}

func TestMain(m *testing.M) {
	status := m.Run()
	if confDir != "" {
		_ = os.RemoveAll(confDir)
	}
	os.Exit(status)
}
<commit_msg>PROTON-1696/PROTON-522: Go Anonymous SASL test should work even without Cyrus SASL installed - It was relying on saslpasswd2 even when it didn't need to<commit_after>/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you 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 electron

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

func testAuthClientServer(t *testing.T, copts []ConnectionOption, sopts []ConnectionOption) (got connectionSettings, err error) {
	client, server := newClientServerOpts(t, copts, sopts)
	defer closeClientServer(client, server)

	go func() {
		for in := range server.Incoming() {
			switch in := in.(type) {
			case *IncomingConnection:
				got = connectionSettings{user: in.User(), virtualHost: in.VirtualHost()}
			}
			in.Accept()
		}
	}()

	err = client.Sync()
	return
}

func TestAuthAnonymous(t *testing.T) {
	configureSASL()
	got, err := testAuthClientServer(t,
		[]ConnectionOption{User("fred"), VirtualHost("vhost"), SASLAllowInsecure(true)},
		[]ConnectionOption{SASLAllowedMechs("ANONYMOUS"), SASLAllowInsecure(true)})
	fatalIf(t, err)
	errorIf(t, checkEqual(connectionSettings{user: "anonymous", virtualHost: "vhost"}, got))
}

func TestAuthPlain(t *testing.T) {
	if !SASLExtended() {
		t.Skip()
	}
	fatalIf(t, configureSASL())
	got, err := testAuthClientServer(t,
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN"), User("fred@proton"), Password([]byte("xxx"))},
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN")})
	fatalIf(t, err)
	errorIf(t, checkEqual(connectionSettings{user: "fred@proton"}, got))
}

func TestAuthBadPass(t *testing.T) {
	if !SASLExtended() {
		t.Skip()
	}
	fatalIf(t, configureSASL())
	_, err := testAuthClientServer(t,
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN"), User("fred@proton"), Password([]byte("yyy"))},
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN")})
	if err == nil {
		t.Error("Expected auth failure for bad pass")
	}
}

func TestAuthBadUser(t *testing.T) {
	if !SASLExtended() {
		t.Skip()
	}
	fatalIf(t, configureSASL())
	_, err := testAuthClientServer(t,
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN"), User("foo@bar"), Password([]byte("yyy"))},
		[]ConnectionOption{SASLAllowInsecure(true), SASLAllowedMechs("PLAIN")})
	if err == nil {
		t.Error("Expected auth failure for bad user")
	}
}

var confDir string
var confErr error

func configureSASL() error {
	if confDir != "" || confErr != nil {
		return confErr
	}
	confDir, confErr = ioutil.TempDir("", "")
	if confErr != nil {
		return confErr
	}

	GlobalSASLConfigDir(confDir)
	GlobalSASLConfigName("test")
	conf := filepath.Join(confDir, "test.conf")

	db := filepath.Join(confDir, "proton.sasldb")
	cmd := exec.Command("saslpasswd2", "-c", "-p", "-f", db, "-u", "proton", "fred")
	cmd.Stdin = strings.NewReader("xxx") // Password
	if out, err := cmd.CombinedOutput(); err != nil {
		confErr = fmt.Errorf("saslpasswd2 failed: %s\n%s", err, out)
		return confErr
	}
	confStr := "sasldb_path: " + db + "\nmech_list: EXTERNAL DIGEST-MD5 SCRAM-SHA-1 CRAM-MD5 PLAIN ANONYMOUS\n"
	if err := ioutil.WriteFile(conf, []byte(confStr), os.ModePerm); err != nil {
		confErr = fmt.Errorf("write conf file %s failed: %s", conf, err)
	}
	return confErr
}

func TestMain(m *testing.M) {
	status := m.Run()
	if confDir != "" {
		_ = os.RemoveAll(confDir)
	}
	os.Exit(status)
}
<|endoftext|>