<commit_before>// Package dnsserver implements all the interfaces from Caddy, so that CoreDNS can be a servertype plugin.
package dnsserver

import (
	"context"
	"fmt"
	"net"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/coredns/caddy"
	"github.com/coredns/coredns/plugin"
	"github.com/coredns/coredns/plugin/metrics/vars"
	"github.com/coredns/coredns/plugin/pkg/edns"
	"github.com/coredns/coredns/plugin/pkg/log"
	"github.com/coredns/coredns/plugin/pkg/rcode"
	"github.com/coredns/coredns/plugin/pkg/reuseport"
	"github.com/coredns/coredns/plugin/pkg/trace"
	"github.com/coredns/coredns/plugin/pkg/transport"
	"github.com/coredns/coredns/request"

	"github.com/miekg/dns"
	ot "github.com/opentracing/opentracing-go"
)

// Server represents an instance of a server, which serves
// DNS requests at a particular address (host and port). A
// server is capable of serving numerous zones on
// the same address and the listener may be stopped for
// graceful termination (POSIX only).
type Server struct {
	Addr string // Address we listen on

	server [2]*dns.Server // 0 is a net.Listener, 1 is a net.PacketConn (a *UDPConn) in our case.
	m      sync.Mutex     // protects the servers

	zones        map[string]*Config // zones keyed by their address
	dnsWg        sync.WaitGroup     // used to wait on outstanding connections
	graceTimeout time.Duration      // the maximum duration of a graceful shutdown
	trace        trace.Trace        // the trace plugin for the server
	debug        bool               // disable recover()
	classChaos   bool               // allow non-INET class queries
}

// NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class
// queries are blocked unless queries from enableChaos are loaded.
func NewServer(addr string, group []*Config) (*Server, error) {

	s := &Server{
		Addr:         addr,
		zones:        make(map[string]*Config),
		graceTimeout: 5 * time.Second,
	}

	// We have to bound our wg with one increment
	// to prevent a "race condition" that is hard-coded
	// into sync.WaitGroup.Wait() - basically, an add
	// with a positive delta must be guaranteed to
	// occur before Wait() is called on the wg.
	// In a way, this kind of acts as a safety barrier.
	s.dnsWg.Add(1)

	for _, site := range group {
		if site.Debug {
			s.debug = true
			log.D.Set()
		}
		// set the config per zone
		s.zones[site.Zone] = site

		// compile custom plugin for everything
		var stack plugin.Handler
		for i := len(site.Plugin) - 1; i >= 0; i-- {
			stack = site.Plugin[i](stack)

			// register the *handler* also
			site.registerHandler(stack)

			if s.trace == nil && stack.Name() == "trace" {
				// we have to stash away the plugin, not the
				// Tracer object, because the Tracer won't be initialized yet
				if t, ok := stack.(trace.Trace); ok {
					s.trace = t
				}
			}
			// Unblock CH class queries when any of these plugins are loaded.
			if _, ok := EnableChaos[stack.Name()]; ok {
				s.classChaos = true
			}
		}
		site.pluginChain = stack
	}

	if !s.debug {
		// When reloading we need to explicitly disable debug logging if it is now disabled.
		log.D.Clear()
	}

	return s, nil
}

// Compile-time check to ensure Server implements the caddy.GracefulServer interface
var _ caddy.GracefulServer = &Server{}

// Serve starts the server with an existing listener. It blocks until the server stops.
// This implements caddy.TCPServer interface.
func (s *Server) Serve(l net.Listener) error {
	s.m.Lock()
	s.server[tcp] = &dns.Server{Listener: l, Net: "tcp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
		ctx := context.WithValue(context.Background(), Key{}, s)
		ctx = context.WithValue(ctx, LoopKey{}, 0)
		s.ServeDNS(ctx, w, r)
	})}
	s.m.Unlock()

	return s.server[tcp].ActivateAndServe()
}

// ServePacket starts the server with an existing packetconn. It blocks until the server stops.
// This implements caddy.UDPServer interface.
func (s *Server) ServePacket(p net.PacketConn) error {
	s.m.Lock()
	s.server[udp] = &dns.Server{PacketConn: p, Net: "udp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
		ctx := context.WithValue(context.Background(), Key{}, s)
		ctx = context.WithValue(ctx, LoopKey{}, 0)
		s.ServeDNS(ctx, w, r)
	})}
	s.m.Unlock()

	return s.server[udp].ActivateAndServe()
}

// Listen implements caddy.TCPServer interface.
func (s *Server) Listen() (net.Listener, error) {
	l, err := reuseport.Listen("tcp", s.Addr[len(transport.DNS+"://"):])
	if err != nil {
		return nil, err
	}
	return l, nil
}

// WrapListener Listen implements caddy.GracefulServer interface.
func (s *Server) WrapListener(ln net.Listener) net.Listener {
	return ln
}

// ListenPacket implements caddy.UDPServer interface.
func (s *Server) ListenPacket() (net.PacketConn, error) {
	p, err := reuseport.ListenPacket("udp", s.Addr[len(transport.DNS+"://"):])
	if err != nil {
		return nil, err
	}

	return p, nil
}

// Stop stops the server. It blocks until the server is
// totally stopped. On POSIX systems, it will wait for
// connections to close (up to a max timeout of a few
// seconds); on Windows it will close the listener
// immediately.
// This implements Caddy.Stopper interface.
func (s *Server) Stop() (err error) {

	if runtime.GOOS != "windows" {
		// force connections to close after timeout
		done := make(chan struct{})
		go func() {
			s.dnsWg.Done() // decrement our initial increment used as a barrier
			s.dnsWg.Wait()
			close(done)
		}()

		// Wait for remaining connections to finish or
		// force them all to close after timeout
		select {
		case <-time.After(s.graceTimeout):
		case <-done:
		}
	}

	// Close the listener now; this stops the server without delay
	s.m.Lock()
	for _, s1 := range s.server {
		// We might not have started and initialized the full set of servers
		if s1 != nil {
			err = s1.Shutdown()
		}
	}
	s.m.Unlock()
	return
}

// Address together with Stop() implement caddy.GracefulServer.
func (s *Server) Address() string { return s.Addr }

// ServeDNS is the entry point for every request to the address that
// is bound to. It acts as a multiplexer for the requests zonename as
// defined in the request so that the correct zone
// (configuration and plugin stack) will handle the request.
func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
	// The default dns.Mux checks the question section size, but we have our
	// own mux here. Check if we have a question section. If not drop them here.
	if r == nil || len(r.Question) == 0 {
		errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure)
		return
	}

	if !s.debug {
		defer func() {
			// In case the user doesn't enable error plugin, we still
			// need to make sure that we stay alive up here
			if rec := recover(); rec != nil {
				log.Errorf("Recovered from panic in server: %q", s.Addr)
				vars.Panic.Inc()
				errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure)
			}
		}()
	}

	if !s.classChaos && r.Question[0].Qclass != dns.ClassINET {
		errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
		return
	}

	if m, err := edns.Version(r); err != nil { // Wrong EDNS version, return at once.
		w.WriteMsg(m)
		return
	}

	// Wrap the response writer in a ScrubWriter so we automatically make the reply fit in the client's buffer.
	w = request.NewScrubWriter(r, w)

	q := strings.ToLower(r.Question[0].Name)
	var (
		off       int
		end       bool
		dshandler *Config
	)

	for {
		if h, ok := s.zones[q[off:]]; ok {
			if h.pluginChain == nil { // zone defined, but has not got any plugins
				errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
				return
			}
			if r.Question[0].Qtype != dns.TypeDS {
				rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
				if !plugin.ClientWrite(rcode) {
					errorFunc(s.Addr, w, r, rcode)
				}
				return
			}
			// The type is DS, keep the handler, but keep on searching as maybe we are serving
			// the parent as well and the DS should be routed to it - this will probably *misroute* DS
			// queries to a possibly grand parent, but there is no way for us to know at this point
			// if there is an actual delegation from grandparent -> parent -> zone.
			// In all fairness: direct DS queries should not be needed.
			dshandler = h
		}
		off, end = dns.NextLabel(q, off)
		if end {
			break
		}
	}

	if r.Question[0].Qtype == dns.TypeDS && dshandler != nil && dshandler.pluginChain != nil {
		// DS request, and we found a zone, use the handler for the query.
		rcode, _ := dshandler.pluginChain.ServeDNS(ctx, w, r)
		if !plugin.ClientWrite(rcode) {
			errorFunc(s.Addr, w, r, rcode)
		}
		return
	}

	// Wildcard match, if we have found nothing try the root zone as a last resort.
	if h, ok := s.zones["."]; ok && h.pluginChain != nil {
		rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
		if !plugin.ClientWrite(rcode) {
			errorFunc(s.Addr, w, r, rcode)
		}
		return
	}

	// Still here? Error out with REFUSED.
	errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
}

// OnStartupComplete lists the sites served by this server
// and any relevant information, assuming Quiet is false.
func (s *Server) OnStartupComplete() {
	if Quiet {
		return
	}

	out := startUpZones("", s.Addr, s.zones)
	if out != "" {
		fmt.Print(out)
	}
}

// Tracer returns the tracer in the server if defined.
func (s *Server) Tracer() ot.Tracer {
	if s.trace == nil {
		return nil
	}

	return s.trace.Tracer()
}

// errorFunc responds to an DNS request with an error.
func errorFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int) {
	state := request.Request{W: w, Req: r}

	answer := new(dns.Msg)
	answer.SetRcode(r, rc)
	state.SizeAndDo(answer)

	w.WriteMsg(answer)
}

func errorAndMetricsFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int) {
	state := request.Request{W: w, Req: r}

	answer := new(dns.Msg)
	answer.SetRcode(r, rc)
	state.SizeAndDo(answer)

	vars.Report(server, state, vars.Dropped, rcode.ToString(rc), "" /* plugin */, answer.Len(), time.Now())

	w.WriteMsg(answer)
}

const (
	tcp = 0
	udp = 1
)

type (
	// Key is the context key for the current server added to the context.
	Key struct{}

	// LoopKey is the context key to detect server wide loops.
	LoopKey struct{}
)

// EnableChaos is a map with plugin names for which we should open CH class queries as we block these by default.
var EnableChaos = map[string]struct{}{
	"chaos":   {},
	"forward": {},
	"proxy":   {},
}

// Quiet mode will not show any informative output on initialization.
var Quiet bool
<commit_msg>Log panic cause for better debugging (#5091)<commit_after>// Package dnsserver implements all the interfaces from Caddy, so that CoreDNS can be a servertype plugin.
package dnsserver

import (
	"context"
	"fmt"
	"net"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/coredns/caddy"
	"github.com/coredns/coredns/plugin"
	"github.com/coredns/coredns/plugin/metrics/vars"
	"github.com/coredns/coredns/plugin/pkg/edns"
	"github.com/coredns/coredns/plugin/pkg/log"
	"github.com/coredns/coredns/plugin/pkg/rcode"
	"github.com/coredns/coredns/plugin/pkg/reuseport"
	"github.com/coredns/coredns/plugin/pkg/trace"
	"github.com/coredns/coredns/plugin/pkg/transport"
	"github.com/coredns/coredns/request"

	"github.com/miekg/dns"
	ot "github.com/opentracing/opentracing-go"
)

// Server represents an instance of a server, which serves
// DNS requests at a particular address (host and port). A
// server is capable of serving numerous zones on
// the same address and the listener may be stopped for
// graceful termination (POSIX only).
type Server struct {
	Addr string // Address we listen on

	server [2]*dns.Server // 0 is a net.Listener, 1 is a net.PacketConn (a *UDPConn) in our case.
	m      sync.Mutex     // protects the servers

	zones        map[string]*Config // zones keyed by their address
	dnsWg        sync.WaitGroup     // used to wait on outstanding connections
	graceTimeout time.Duration      // the maximum duration of a graceful shutdown
	trace        trace.Trace        // the trace plugin for the server
	debug        bool               // disable recover()
	classChaos   bool               // allow non-INET class queries
}

// NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class
// queries are blocked unless queries from enableChaos are loaded.
func NewServer(addr string, group []*Config) (*Server, error) {

	s := &Server{
		Addr:         addr,
		zones:        make(map[string]*Config),
		graceTimeout: 5 * time.Second,
	}

	// We have to bound our wg with one increment
	// to prevent a "race condition" that is hard-coded
	// into sync.WaitGroup.Wait() - basically, an add
	// with a positive delta must be guaranteed to
	// occur before Wait() is called on the wg.
	// In a way, this kind of acts as a safety barrier.
	s.dnsWg.Add(1)

	for _, site := range group {
		if site.Debug {
			s.debug = true
			log.D.Set()
		}
		// set the config per zone
		s.zones[site.Zone] = site

		// compile custom plugin for everything
		var stack plugin.Handler
		for i := len(site.Plugin) - 1; i >= 0; i-- {
			stack = site.Plugin[i](stack)

			// register the *handler* also
			site.registerHandler(stack)

			if s.trace == nil && stack.Name() == "trace" {
				// we have to stash away the plugin, not the
				// Tracer object, because the Tracer won't be initialized yet
				if t, ok := stack.(trace.Trace); ok {
					s.trace = t
				}
			}
			// Unblock CH class queries when any of these plugins are loaded.
			if _, ok := EnableChaos[stack.Name()]; ok {
				s.classChaos = true
			}
		}
		site.pluginChain = stack
	}

	if !s.debug {
		// When reloading we need to explicitly disable debug logging if it is now disabled.
		log.D.Clear()
	}

	return s, nil
}

// Compile-time check to ensure Server implements the caddy.GracefulServer interface
var _ caddy.GracefulServer = &Server{}

// Serve starts the server with an existing listener. It blocks until the server stops.
// This implements caddy.TCPServer interface.
func (s *Server) Serve(l net.Listener) error {
	s.m.Lock()
	s.server[tcp] = &dns.Server{Listener: l, Net: "tcp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
		ctx := context.WithValue(context.Background(), Key{}, s)
		ctx = context.WithValue(ctx, LoopKey{}, 0)
		s.ServeDNS(ctx, w, r)
	})}
	s.m.Unlock()

	return s.server[tcp].ActivateAndServe()
}

// ServePacket starts the server with an existing packetconn. It blocks until the server stops.
// This implements caddy.UDPServer interface.
func (s *Server) ServePacket(p net.PacketConn) error {
	s.m.Lock()
	s.server[udp] = &dns.Server{PacketConn: p, Net: "udp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
		ctx := context.WithValue(context.Background(), Key{}, s)
		ctx = context.WithValue(ctx, LoopKey{}, 0)
		s.ServeDNS(ctx, w, r)
	})}
	s.m.Unlock()

	return s.server[udp].ActivateAndServe()
}

// Listen implements caddy.TCPServer interface.
func (s *Server) Listen() (net.Listener, error) {
	l, err := reuseport.Listen("tcp", s.Addr[len(transport.DNS+"://"):])
	if err != nil {
		return nil, err
	}
	return l, nil
}

// WrapListener Listen implements caddy.GracefulServer interface.
func (s *Server) WrapListener(ln net.Listener) net.Listener {
	return ln
}

// ListenPacket implements caddy.UDPServer interface.
func (s *Server) ListenPacket() (net.PacketConn, error) {
	p, err := reuseport.ListenPacket("udp", s.Addr[len(transport.DNS+"://"):])
	if err != nil {
		return nil, err
	}

	return p, nil
}

// Stop stops the server. It blocks until the server is
// totally stopped. On POSIX systems, it will wait for
// connections to close (up to a max timeout of a few
// seconds); on Windows it will close the listener
// immediately.
// This implements Caddy.Stopper interface.
func (s *Server) Stop() (err error) {

	if runtime.GOOS != "windows" {
		// force connections to close after timeout
		done := make(chan struct{})
		go func() {
			s.dnsWg.Done() // decrement our initial increment used as a barrier
			s.dnsWg.Wait()
			close(done)
		}()

		// Wait for remaining connections to finish or
		// force them all to close after timeout
		select {
		case <-time.After(s.graceTimeout):
		case <-done:
		}
	}

	// Close the listener now; this stops the server without delay
	s.m.Lock()
	for _, s1 := range s.server {
		// We might not have started and initialized the full set of servers
		if s1 != nil {
			err = s1.Shutdown()
		}
	}
	s.m.Unlock()
	return
}

// Address together with Stop() implement caddy.GracefulServer.
func (s *Server) Address() string { return s.Addr }

// ServeDNS is the entry point for every request to the address that
// is bound to. It acts as a multiplexer for the requests zonename as
// defined in the request so that the correct zone
// (configuration and plugin stack) will handle the request.
func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
	// The default dns.Mux checks the question section size, but we have our
	// own mux here. Check if we have a question section. If not drop them here.
	if r == nil || len(r.Question) == 0 {
		errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure)
		return
	}

	if !s.debug {
		defer func() {
			// In case the user doesn't enable error plugin, we still
			// need to make sure that we stay alive up here
			if rec := recover(); rec != nil {
				log.Errorf("Recovered from panic in server: %q %v", s.Addr, rec)
				vars.Panic.Inc()
				errorAndMetricsFunc(s.Addr, w, r, dns.RcodeServerFailure)
			}
		}()
	}

	if !s.classChaos && r.Question[0].Qclass != dns.ClassINET {
		errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
		return
	}

	if m, err := edns.Version(r); err != nil { // Wrong EDNS version, return at once.
		w.WriteMsg(m)
		return
	}

	// Wrap the response writer in a ScrubWriter so we automatically make the reply fit in the client's buffer.
	w = request.NewScrubWriter(r, w)

	q := strings.ToLower(r.Question[0].Name)
	var (
		off       int
		end       bool
		dshandler *Config
	)

	for {
		if h, ok := s.zones[q[off:]]; ok {
			if h.pluginChain == nil { // zone defined, but has not got any plugins
				errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
				return
			}
			if r.Question[0].Qtype != dns.TypeDS {
				rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
				if !plugin.ClientWrite(rcode) {
					errorFunc(s.Addr, w, r, rcode)
				}
				return
			}
			// The type is DS, keep the handler, but keep on searching as maybe we are serving
			// the parent as well and the DS should be routed to it - this will probably *misroute* DS
			// queries to a possibly grand parent, but there is no way for us to know at this point
			// if there is an actual delegation from grandparent -> parent -> zone.
			// In all fairness: direct DS queries should not be needed.
			dshandler = h
		}
		off, end = dns.NextLabel(q, off)
		if end {
			break
		}
	}

	if r.Question[0].Qtype == dns.TypeDS && dshandler != nil && dshandler.pluginChain != nil {
		// DS request, and we found a zone, use the handler for the query.
		rcode, _ := dshandler.pluginChain.ServeDNS(ctx, w, r)
		if !plugin.ClientWrite(rcode) {
			errorFunc(s.Addr, w, r, rcode)
		}
		return
	}

	// Wildcard match, if we have found nothing try the root zone as a last resort.
	if h, ok := s.zones["."]; ok && h.pluginChain != nil {
		rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
		if !plugin.ClientWrite(rcode) {
			errorFunc(s.Addr, w, r, rcode)
		}
		return
	}

	// Still here? Error out with REFUSED.
	errorAndMetricsFunc(s.Addr, w, r, dns.RcodeRefused)
}

// OnStartupComplete lists the sites served by this server
// and any relevant information, assuming Quiet is false.
func (s *Server) OnStartupComplete() {
	if Quiet {
		return
	}

	out := startUpZones("", s.Addr, s.zones)
	if out != "" {
		fmt.Print(out)
	}
}

// Tracer returns the tracer in the server if defined.
func (s *Server) Tracer() ot.Tracer {
	if s.trace == nil {
		return nil
	}

	return s.trace.Tracer()
}

// errorFunc responds to an DNS request with an error.
func errorFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int) {
	state := request.Request{W: w, Req: r}

	answer := new(dns.Msg)
	answer.SetRcode(r, rc)
	state.SizeAndDo(answer)

	w.WriteMsg(answer)
}

func errorAndMetricsFunc(server string, w dns.ResponseWriter, r *dns.Msg, rc int) {
	state := request.Request{W: w, Req: r}

	answer := new(dns.Msg)
	answer.SetRcode(r, rc)
	state.SizeAndDo(answer)

	vars.Report(server, state, vars.Dropped, rcode.ToString(rc), "" /* plugin */, answer.Len(), time.Now())

	w.WriteMsg(answer)
}

const (
	tcp = 0
	udp = 1
)

type (
	// Key is the context key for the current server added to the context.
	Key struct{}

	// LoopKey is the context key to detect server wide loops.
	LoopKey struct{}
)

// EnableChaos is a map with plugin names for which we should open CH class queries as we block these by default.
var EnableChaos = map[string]struct{}{
	"chaos":   {},
	"forward": {},
	"proxy":   {},
}

// Quiet mode will not show any informative output on initialization.
var Quiet bool
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/Sirupsen/logrus"
	"github.com/docker/distribution/manifest/schema1"
	humanize "github.com/dustin/go-humanize"
	"github.com/jessfraz/reg/clair"
	"github.com/jessfraz/reg/registry"
	"github.com/jessfraz/reg/utils"
	wordwrap "github.com/mitchellh/go-wordwrap"
	"github.com/urfave/cli"
)

const (
	// VERSION is the binary version.
	VERSION = "v0.1.0"

	dockerConfigPath = ".docker/config.json"
)

var (
	updating = false
	wg       sync.WaitGroup
	tmpl     *template.Template
)

// preload initializes any global options and configuration
// before the main or sub commands are run.
func preload(c *cli.Context) (err error) {
	if c.GlobalBool("debug") {
		logrus.SetLevel(logrus.DebugLevel)
	}

	return nil
}

func main() {
	app := cli.NewApp()
	app.Name = "reg-server"
	app.Version = VERSION
	app.Author = "@jessfraz"
	app.Email = "no-reply@butts.com"
	app.Usage = "Docker registry v2 static UI server."
	app.Before = preload
	app.Flags = []cli.Flag{
		cli.BoolFlag{
			Name:  "debug, d",
			Usage: "run in debug mode",
		},
		cli.StringFlag{
			Name:  "username, u",
			Usage: "username for the registry",
		},
		cli.StringFlag{
			Name:  "password, p",
			Usage: "password for the registry",
		},
		cli.StringFlag{
			Name:  "registry, r",
			Usage: "URL to the private registry (ex. r.j3ss.co)",
		},
		cli.StringFlag{
			Name:  "port",
			Value: "8080",
			Usage: "port for server to run on",
		},
		cli.StringFlag{
			Name:  "cert",
			Usage: "path to ssl cert",
		},
		cli.StringFlag{
			Name:  "key",
			Usage: "path to ssl key",
		},
		cli.StringFlag{
			Name:  "interval",
			Value: "5m",
			Usage: "interval to generate new index.html's at",
		},
		cli.StringFlag{
			Name:  "clair",
			Usage: "url to clair instance",
		},
	}
	app.Action = func(c *cli.Context) error {
		auth, err := utils.GetAuthConfig(c)
		if err != nil {
			logrus.Fatal(err)
		}

		// create the registry client
		r, err := registry.New(auth, c.GlobalBool("debug"))
		if err != nil {
			logrus.Fatal(err)
		}

		// get the path to the static directory
		wd, err := os.Getwd()
		if err != nil {
			logrus.Fatal(err)
		}
		staticDir := filepath.Join(wd, "static")

		// create the template
		templateDir := filepath.Join(staticDir, "../templates")
		funcMap := template.FuncMap{
			"trim": func(s string) string {
				return wordwrap.WrapString(s, 80)
			},
			"color": func(s string) string {
				switch s = strings.ToLower(s); s {
				case "high":
					return "danger"
				case "critical":
					return "danger"
				case "defcon1":
					return "danger"
				case "medium":
					return "warning"
				case "low":
					return "info"
				case "negligible":
					return "info"
				case "unknown":
					return "default"
				default:
					return "default"
				}
			},
		}
		tmpl = template.Must(template.New("").Funcs(funcMap).ParseFiles(filepath.Join(templateDir, "vulns.html"), filepath.Join(templateDir, "layout.html")))

		// create the initial index
		logrus.Info("creating initial static index")
		if err := createStaticIndex(r, staticDir, c.GlobalString("clair")); err != nil {
			logrus.Fatalf("Error creating index: %v", err)
		}

		// parse the duration
		dur, err := time.ParseDuration(c.String("interval"))
		if err != nil {
			logrus.Fatalf("parsing %s as duration failed: %v", c.String("interval"), err)
		}
		ticker := time.NewTicker(dur)

		go func() {
			// create more indexes every X minutes based off interval
			for range ticker.C {
				if !updating {
					logrus.Info("creating timer based static index")
					if err := createStaticIndex(r, staticDir, c.GlobalString("clair")); err != nil {
						logrus.Warnf("creating static index failed: %v", err)
						wg.Wait()
						updating = false
					}
					wg.Wait()
					logrus.Info("finished waiting for vulns wait group")
				} else {
					logrus.Warnf("skipping timer based static index update for %s", c.String("interval"))
				}
			}
		}()

		// create mux server
		mux := http.NewServeMux()

		// static files handler
		staticHandler := http.FileServer(http.Dir(staticDir))
		mux.Handle("/", staticHandler)

		// set up the server
		port := c.String("port")
		server := &http.Server{
			Addr:    ":" + port,
			Handler: mux,
		}
		logrus.Infof("Starting server on port %q", port)
		if c.String("cert") != "" && c.String("key") != "" {
			logrus.Fatal(server.ListenAndServeTLS(c.String("cert"), c.String("key")))
		} else {
			logrus.Fatal(server.ListenAndServe())
		}

		return nil
	}

	app.Run(os.Args)
}

type data struct {
	RegistryURL string
	LastUpdated string
	Repos       []repository
}

type repository struct {
	Name        string
	Tag         string
	RepoURI     string
	CreatedDate string
	VulnURI     string
}

type v1Compatibility struct {
	ID      string    `json:"id"`
	Created time.Time `json:"created"`
}

func createStaticIndex(r *registry.Registry, staticDir, clairURI string) error {
	updating = true
	logrus.Info("fetching catalog")
	repoList, err := r.Catalog("")
	if err != nil {
		return fmt.Errorf("getting catalog failed: %v", err)
	}

	logrus.Info("fetching tags")
	var repos []repository
	for i, repo := range repoList {
		// get the tags
		tags, err := r.Tags(repo)
		if err != nil {
			return fmt.Errorf("getting tags for %s failed: %v", repo, err)
		}
		for j, tag := range tags {
			// get the manifest

			m1, err := r.ManifestV1(repo, tag)
			if err != nil {
				logrus.Warnf("getting v1 manifest for %s:%s failed: %v", repo, tag, err)
			}

			var createdDate string
			for _, h := range m1.History {
				var comp v1Compatibility
				if err := json.Unmarshal([]byte(h.V1Compatibility), &comp); err != nil {
					return fmt.Errorf("unmarshal v1compatibility failed: %v", err)
				}
				createdDate = humanize.Time(comp.Created)
				break
			}

			repoURI := fmt.Sprintf("%s/%s", r.Domain, repo)
			if tag != "latest" {
				repoURI += ":" + tag
			}

			newrepo := repository{
				Name:        repo,
				Tag:         tag,
				RepoURI:     repoURI,
				CreatedDate: createdDate,
			}

			if clairURI != "" {
				wg.Add(1)

				go func(repo, tag string, i, j int) {
					defer wg.Done()

					throttle := time.Tick(time.Duration(time.Duration((i+1)*(j+1)*4) * time.Second))
					<-throttle

					logrus.Infof("creating vulns.txt for %s:%s", repo, tag)

					if err := createVulnStaticPage(r, staticDir, clairURI, repo, tag, m1); err != nil {
						// return fmt.Errorf("creating vuln static page for %s:%s failed: %v", repo, tag, err)
						logrus.Warnf("creating vuln static page for %s:%s failed: %v", repo, tag, err)
					}
				}(repo, tag, i, j)

				newrepo.VulnURI = filepath.Join(repo, tag)
			}
			repos = append(repos, newrepo)
		}
	}

	d := data{
		RegistryURL: r.Domain,
		Repos:       repos,
		LastUpdated: time.Now().Local().Format(time.RFC1123),
	}

	logrus.Info("rendering index template")
	if err := renderTemplate(staticDir, "index", "index.html", d); err != nil {
		return err
	}
	updating = false
	return nil
}

type vulnsReport struct {
	RegistryURL     string
	Repo            string
	Tag             string
	Date            string
	Vulns           []clair.Vulnerability
	VulnsBySeverity map[string][]clair.Vulnerability
	BadVulns        int
}

func createVulnStaticPage(r *registry.Registry, staticDir, clairURI, repo, tag string, m schema1.SignedManifest) error {
	report := vulnsReport{
		RegistryURL:     r.Domain,
		Repo:            repo,
		Tag:             tag,
		Date:            time.Now().Local().Format(time.RFC1123),
		VulnsBySeverity: make(map[string][]clair.Vulnerability),
	}

	// filter out the empty layers
	var filteredLayers []schema1.FSLayer
	for _, layer := range m.FSLayers {
		if layer.BlobSum != clair.EmptyLayerBlobSum {
			filteredLayers = append(filteredLayers, layer)
		}
	}
	m.FSLayers = filteredLayers
	if len(m.FSLayers) == 0 {
		fmt.Printf("No need to analyse image %s:%s as there is no non-emtpy layer", repo, tag)
		return nil
	}

	// initialize clair
	cr, err := clair.New(clairURI, false)
	if err != nil {
		return err
	}

	for i := len(m.FSLayers) - 1; i >= 0; i-- {
		// form the clair layer
		l, err := utils.NewClairLayer(r, repo, m.FSLayers, i)
		if err != nil {
			return err
		}

		// post the layer
		if _, err := cr.PostLayer(l); err != nil {
			return err
		}
	}

	vl, err := cr.GetLayer(m.FSLayers[0].BlobSum.String(), false, true)
	if err != nil {
		return err
	}

	// get the vulns
	for _, f := range vl.Features {
		for _, v := range f.Vulnerabilities {
			report.Vulns = append(report.Vulns, v)
		}
	}

	vulnsBy := func(sev string, store map[string][]clair.Vulnerability) []clair.Vulnerability {
		items, found := store[sev]
		if !found {
			items = make([]clair.Vulnerability, 0)
			store[sev] = items
		}
		return items
	}

	// group by severity
	for _, v := range report.Vulns {
		sevRow := vulnsBy(v.Severity, report.VulnsBySeverity)
		report.VulnsBySeverity[v.Severity] = append(sevRow, v)
	}

	// calculate number of bad vulns
	report.BadVulns = len(report.VulnsBySeverity["High"]) + len(report.VulnsBySeverity["Critical"]) + len(report.VulnsBySeverity["Defcon1"])

	path := filepath.Join(repo, tag, "index.html")
	if err := renderTemplate(staticDir, "vulns", path, report); err != nil {
		return err
	}
	return nil
}

func renderTemplate(staticDir, templateName, dest string, data interface{}) error {
	// parse & execute the template
	logrus.Debugf("executing the template %s", templateName)

	path := filepath.Join(staticDir, dest)
	if err := os.MkdirAll(filepath.Dir(path), 0644); err != nil {
		return err
	}
	logrus.Debugf("creating/opening file %s", path)
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer f.Close()

	if err := tmpl.ExecuteTemplate(f, templateName, data); err != nil {
		f.Close()
		return fmt.Errorf("execute template %s failed: %v", templateName, err)
	}

	return nil
}
<commit_msg>Do not panic if vulns/layout not found (#14)<commit_after>package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/Sirupsen/logrus"
	"github.com/docker/distribution/manifest/schema1"
	humanize "github.com/dustin/go-humanize"
	"github.com/jessfraz/reg/clair"
	"github.com/jessfraz/reg/registry"
	"github.com/jessfraz/reg/utils"
	wordwrap "github.com/mitchellh/go-wordwrap"
	"github.com/urfave/cli"
)

const (
	// VERSION is the binary version.
	VERSION = "v0.1.0"

	dockerConfigPath = ".docker/config.json"
)

var (
	updating = false
	wg       sync.WaitGroup
	tmpl     *template.Template
)

// preload initializes any global options and configuration
// before the main or sub commands are run.
func preload(c *cli.Context) (err error) {
	if c.GlobalBool("debug") {
		logrus.SetLevel(logrus.DebugLevel)
	}

	return nil
}

func main() {
	app := cli.NewApp()
	app.Name = "reg-server"
	app.Version = VERSION
	app.Author = "@jessfraz"
	app.Email = "no-reply@butts.com"
	app.Usage = "Docker registry v2 static UI server."
	app.Before = preload
	app.Flags = []cli.Flag{
		cli.BoolFlag{
			Name:  "debug, d",
			Usage: "run in debug mode",
		},
		cli.StringFlag{
			Name:  "username, u",
			Usage: "username for the registry",
		},
		cli.StringFlag{
			Name:  "password, p",
			Usage: "password for the registry",
		},
		cli.StringFlag{
			Name:  "registry, r",
			Usage: "URL to the private registry (ex. r.j3ss.co)",
		},
		cli.StringFlag{
			Name:  "port",
			Value: "8080",
			Usage: "port for server to run on",
		},
		cli.StringFlag{
			Name:  "cert",
			Usage: "path to ssl cert",
		},
		cli.StringFlag{
			Name:  "key",
			Usage: "path to ssl key",
		},
		cli.StringFlag{
			Name:  "interval",
			Value: "5m",
			Usage: "interval to generate new index.html's at",
		},
		cli.StringFlag{
			Name:  "clair",
			Usage: "url to clair instance",
		},
	}
	app.Action = func(c *cli.Context) error {
		auth, err := utils.GetAuthConfig(c)
		if err != nil {
			logrus.Fatal(err)
		}

		// create the registry client
		r, err := registry.New(auth, c.GlobalBool("debug"))
		if err != nil {
			logrus.Fatal(err)
		}

		// get the path to the static directory
		wd, err := os.Getwd()
		if err != nil {
			logrus.Fatal(err)
		}
		staticDir := filepath.Join(wd, "static")

		// create the template
		templateDir := filepath.Join(staticDir, "../templates")
		funcMap := template.FuncMap{
			"trim": func(s string) string {
				return wordwrap.WrapString(s, 80)
			},
			"color": func(s string) string {
				switch s = strings.ToLower(s); s {
				case "high":
					return "danger"
				case "critical":
					return "danger"
				case "defcon1":
					return "danger"
				case "medium":
					return "warning"
				case "low":
					return "info"
				case "negligible":
					return "info"
				case "unknown":
					return "default"
				default:
					return "default"
				}
			},
		}
		vulns := filepath.Join(templateDir, "vulns.html")
		if _, err := os.Stat(vulns); os.IsNotExist(err) {
			logrus.Fatalf("Template %s not found", vulns)
		}
		layout := filepath.Join(templateDir, "layout.html")
		if _, err := os.Stat(layout); os.IsNotExist(err) {
			logrus.Fatalf("Template %s not found", layout)
		}
		tmpl = template.Must(template.New("").Funcs(funcMap).ParseFiles(vulns, layout))

		// create the initial index
		logrus.Info("creating initial static index")
		if err := createStaticIndex(r, staticDir, c.GlobalString("clair")); err != nil {
			logrus.Fatalf("Error creating index: %v", err)
		}

		// parse the duration
		dur, err := time.ParseDuration(c.String("interval"))
		if err != nil {
			logrus.Fatalf("parsing %s as duration failed: %v", c.String("interval"), err)
		}
		ticker := time.NewTicker(dur)

		go func() {
			// create more indexes every X minutes based off interval
			for range ticker.C {
				if !updating {
					logrus.Info("creating timer based static index")
					if err := createStaticIndex(r, staticDir, c.GlobalString("clair")); err != nil {
						logrus.Warnf("creating static index failed: %v", err)
						wg.Wait()
						updating = false
					}
					wg.Wait()
					logrus.Info("finished waiting for vulns wait group")
				} else {
					logrus.Warnf("skipping timer based static index update for %s", c.String("interval"))
				}
			}
		}()

		// create mux server
		mux := http.NewServeMux()

		// static files handler
		staticHandler := http.FileServer(http.Dir(staticDir))
		mux.Handle("/", staticHandler)

		// set up the server
		port := c.String("port")
		server := &http.Server{
			Addr:    ":" + port,
			Handler: mux,
		}
		logrus.Infof("Starting server on port %q", port)
		if c.String("cert") != "" && c.String("key") != "" {
			logrus.Fatal(server.ListenAndServeTLS(c.String("cert"), c.String("key")))
		} else {
			logrus.Fatal(server.ListenAndServe())
		}

		return nil
	}

	app.Run(os.Args)
}

type data struct {
	RegistryURL string
	LastUpdated string
	Repos       []repository
}

type repository struct {
	Name        string
	Tag         string
	RepoURI     string
	CreatedDate string
	VulnURI     string
}

type v1Compatibility struct {
	ID      string    `json:"id"`
	Created time.Time `json:"created"`
}

func createStaticIndex(r *registry.Registry, staticDir, clairURI string) error {
	updating = true
	logrus.Info("fetching catalog")
	repoList, err := r.Catalog("")
	if err != nil {
		return fmt.Errorf("getting catalog failed: %v", err)
	}

	logrus.Info("fetching tags")
	var repos []repository
	for i, repo := range repoList {
		// get the tags
		tags, err := r.Tags(repo)
		if err != nil {
			return fmt.Errorf("getting tags for %s failed: %v", repo, err)
		}
		for j, tag := range tags {
			// get the manifest

			m1, err := r.ManifestV1(repo, tag)
			if err != nil {
				logrus.Warnf("getting v1 manifest for %s:%s failed: %v", repo, tag, err)
			}

			var createdDate string
			for _, h := range m1.History {
				var comp v1Compatibility
				if err := json.Unmarshal([]byte(h.V1Compatibility), &comp); err != nil {
					return fmt.Errorf("unmarshal v1compatibility failed: %v", err)
				}
				createdDate = humanize.Time(comp.Created)
				break
			}

			repoURI := fmt.Sprintf("%s/%s", r.Domain, repo)
			if tag != "latest" {
				repoURI += ":" + tag
			}

			newrepo := repository{
				Name:        repo,
				Tag:         tag,
				RepoURI:     repoURI,
				CreatedDate: createdDate,
			}

			if clairURI != "" {
				wg.Add(1)

				go func(repo, tag string, i, j int) {
					defer wg.Done()

					throttle := time.Tick(time.Duration(time.Duration((i+1)*(j+1)*4) * time.Second))
					<-throttle

					logrus.Infof("creating vulns.txt for %s:%s", repo, tag)

					if err := createVulnStaticPage(r, staticDir, clairURI, repo, tag, m1); err != nil {
						// return fmt.Errorf("creating vuln static page for %s:%s failed: %v", repo, tag, err)
						logrus.Warnf("creating vuln static page for %s:%s failed: %v", repo, tag, err)
					}
				}(repo, tag, i, j)

				newrepo.VulnURI = filepath.Join(repo, tag)
			}
			repos = append(repos, newrepo)
		}
	}

	d := data{
		RegistryURL: r.Domain,
		Repos:       repos,
		LastUpdated: time.Now().Local().Format(time.RFC1123),
	}

	logrus.Info("rendering index template")
	if err := renderTemplate(staticDir, "index", "index.html", d); err != nil {
		return err
	}
	updating = false
	return nil
}

type vulnsReport struct {
	RegistryURL     string
	Repo            string
	Tag             string
	Date            string
	Vulns           []clair.Vulnerability
	VulnsBySeverity map[string][]clair.Vulnerability
	BadVulns        int
}

func createVulnStaticPage(r *registry.Registry, staticDir, clairURI, repo, tag string, m schema1.SignedManifest) error {
	report := vulnsReport{
		RegistryURL:     r.Domain,
		Repo:            repo,
		Tag:             tag,
		Date:            time.Now().Local().Format(time.RFC1123),
		VulnsBySeverity: make(map[string][]clair.Vulnerability),
	}

	// filter out the empty layers
	var filteredLayers []schema1.FSLayer
	for _, layer := range m.FSLayers {
		if layer.BlobSum != clair.EmptyLayerBlobSum {
			filteredLayers = append(filteredLayers, layer)
		}
	}
	m.FSLayers = filteredLayers
	if len(m.FSLayers) == 0 {
		fmt.Printf("No need to analyse image %s:%s as there is no non-emtpy layer", repo, tag)
		return nil
	}

	// initialize clair
	cr, err := clair.New(clairURI, false)
	if err != nil {
		return err
	}

	for i := len(m.FSLayers) - 1; i >= 0; i-- {
		// form the clair layer
		l, err := utils.NewClairLayer(r, repo, m.FSLayers, i)
		if err != nil {
			return err
		}

		// post the layer
		if _, err := cr.PostLayer(l); err != nil {
			return err
		}
	}

	vl, err := cr.GetLayer(m.FSLayers[0].BlobSum.String(), false, true)
	if err != nil {
		return err
	}

	// get the vulns
	for _, f := range vl.Features {
		for _, v := range f.Vulnerabilities {
			report.Vulns = append(report.Vulns, v)
		}
	}

	vulnsBy := func(sev string, store map[string][]clair.Vulnerability) []clair.Vulnerability {
		items, found := store[sev]
		if !found {
			items = make([]clair.Vulnerability, 0)
			store[sev] = items
		}
		return items
	}

	// group by severity
	for _, v := range report.Vulns {
		sevRow := vulnsBy(v.Severity, report.VulnsBySeverity)
		report.VulnsBySeverity[v.Severity] = append(sevRow, v)
	}

	// calculate number of bad vulns
	report.BadVulns = len(report.VulnsBySeverity["High"]) + len(report.VulnsBySeverity["Critical"]) + len(report.VulnsBySeverity["Defcon1"])

	path := filepath.Join(repo, tag, "index.html")
	if err := renderTemplate(staticDir, "vulns", path, report); err != nil {
		return err
	}
	return nil
}

func renderTemplate(staticDir, templateName, dest string, data interface{}) error {
	// parse & execute the template
	logrus.Debugf("executing the template %s", templateName)

	path := filepath.Join(staticDir, dest)
	if err := os.MkdirAll(filepath.Dir(path), 0644); err != nil {
		return err
	}
	logrus.Debugf("creating/opening file %s", path)
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer f.Close()

	if err := tmpl.ExecuteTemplate(f, templateName, data); err != nil {
		f.Close()
		return fmt.Errorf("execute template %s failed: %v", templateName, err)
	}

	return nil
}
<|endoftext|>


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

package pubsubsql

import (
	"sync"
	"sync/atomic"
	"time"
)

type IStoper interface {
	Stoped() bool
}

// Stoper implements shutdown protocol to make sure that all active goroutines exit gracefully.
type Stoper struct {
	IStoper
	counter int64
	channel chan int
	stoped  bool
	mutex   sync.Mutex
}

// Stoper factory.
func NewStoper() *Stoper {
	return &Stoper{
		counter: 0,
		channel: make(chan int),
		stoped:  false,
	}
}

// Join starts goroutine participation in shutdown protocol.
func (this *Stoper) Join() {
	atomic.AddInt64(&this.counter, 1)
}

// Leave notifies that participating goroutine gracesfully exited.
// Should be called with defer symantics.
func (this *Stoper) Leave() {
	atomic.AddInt64(&this.counter, -1)
}

// GetChan returns channel to be used in select {} in order to react to Stop event.
func (this *Stoper) GetChan() chan int {
	return this.channel
}

// Stop notifies all participating goroutines that shutdown protocol is in progress
// and waits for all go routines to exit until timeouti.
// Returns false when timed out.
func (this *Stoper) Stop(timeout time.Duration) bool {
	this.stop()
	return this.Wait(timeout)
}

// stop helper
func (this *Stoper) stop() {
	this.mutex.Lock()
	defer this.mutex.Unlock()
	if !this.stoped {
		this.stoped = true
		close(this.channel)
	}
}

func (this *Stoper) Wait(timeout time.Duration) bool {
	if timeout == 0 {
		return atomic.LoadInt64(&this.counter) == 0
	}
	now := time.Now()
	for atomic.LoadInt64(&this.counter) > 0 {
		time.Sleep(time.Millisecond * 10)
		if time.Since(now) > timeout {
			return false
		}
	}
	return true
}

func (this *Stoper) Stoped() bool {
	return this.stoped
}

// Counter returns number of of participating goroutines.
func (this *Stoper) Counter() int64 {
	return atomic.LoadInt64(&this.counter)
}
<commit_msg>Stoper rename to Quitter<commit_after>/* Copyright (C) 2013 CompleteDB LLC.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with PubSubSQL.  If not, see <http://www.gnu.org/licenses/>.
 */

package pubsubsql

import (
	"sync"
	"sync/atomic"
	"time"
)

// IQuitter is a generic interface called from a participating goroutine to determine if it should quit.
type IQuitter interface {
	Done() bool
}

// Quitter implements a quit (shutdown) protocol.
// When the quit protocol is in progress, all participating goroutines should quit.
type Quitter struct {
	IQuitter
	counter int64
	channel chan int
	done bool
	mutex  sync.Mutex
}

// NewQuitter is a Quitter factory.
func NewQuitter() *Quitter {
	return &Quitter{
		counter: 0,
		channel: make(chan int),
		shouldQuitter:  false,
	}
}

// Done returns true if the quit protocol is in progress.
func (this *Quitter) Done() bool {
	return this.done
}

// Join causes the calling goroutine to participate in the quit protocol.
func (this *Quitter) Join() {
	atomic.AddInt64(&this.counter, 1)
}

// Leave signals that the participating goroutine has quit.
// Should be called with defer semantics.
func (this *Quitter) Leave() {
	atomic.AddInt64(&this.counter, -1)
}

// Quit notifies all participating goroutines that the quit protocol is in progress.
// It waits until all participating goroutines signal that they have quit or until a timeout occurs.
// Returns false when a timeout has occurred.
func (this *Quitter) Quit(timeout time.Duration) bool {
	this.quit()
	return this.Wait(timeout)
}

// quit is a helper function.
func (this *Quitter) quit() {
	this.mutex.Lock()
	defer this.mutex.Unlock()
	if !this.stoped {
		this.stoped = true
		close(this.channel)
	}
}

// GetChan returns the channel to be used in a go select statement in order to receive a Quit notification.
func (this *Quitter) GetChan() chan int {
	return this.channel
}

// Wait waits until all participating goroutines signal that they have quit or until a timeout occurs.
// Returns false when a timeout has occurred.
func (this *Quitter) Wait(timeout time.Duration) bool {
	if timeout == 0 {
		return atomic.LoadInt64(&this.counter) == 0
	}
	now := time.Now()
	for atomic.LoadInt64(&this.counter) > 0 {
		time.Sleep(time.Millisecond * 10)
		if time.Since(now) > timeout {
			return false
		}
	}
	return true
}

// GoRoutines returns the number of participating goroutines in the quit protocol.
func (this *Quitter) GoRoutines() int64 {
	return atomic.LoadInt64(&this.counter)
}

<|endoftext|>


<commit_before>package mackerel

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"reflect"
	"testing"
)

func TestFindServices(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		if req.URL.Path != "/api/v0/services" {
			t.Error("request URL should be /api/v0/services but :", req.URL.Path)
		}

		if req.Method != "GET" {
			t.Error("request method should be GET but :", req.Method)
		}

		respJSON, _ := json.Marshal(map[string][]map[string]interface{}{
			"services": {
				{
					"name":  "My-Service",
					"memo":  "hello",
					"roles": []string{"db-master", "db-slave"},
				},
			},
		})

		res.Header()["Content-Type"] = []string{"application/json"}
		fmt.Fprint(res, string(respJSON))
	}))
	defer ts.Close()

	client, _ := NewClientWithOptions("dummy-key", ts.URL, false)
	services, err := client.FindServices()

	if err != nil {
		t.Error("err shoud be nil but: ", err)
	}

	if services[0].Memo != "hello" {
		t.Error("request sends json including memo but: ", services[0])
	}

	if reflect.DeepEqual(services[0].Roles, []string{"db-master", "db-slave"}) != true {
		t.Errorf("Wrong data for roles: %v", services[0].Roles)
	}

}
<commit_msg>Test_CreateService<commit_after>package mackerel

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"reflect"
	"testing"
)

func TestFindServices(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		if req.URL.Path != "/api/v0/services" {
			t.Error("request URL should be /api/v0/services but :", req.URL.Path)
		}

		if req.Method != "GET" {
			t.Error("request method should be GET but :", req.Method)
		}

		respJSON, _ := json.Marshal(map[string][]map[string]interface{}{
			"services": {
				{
					"name":  "My-Service",
					"memo":  "hello",
					"roles": []string{"db-master", "db-slave"},
				},
			},
		})

		res.Header()["Content-Type"] = []string{"application/json"}
		fmt.Fprint(res, string(respJSON))
	}))
	defer ts.Close()

	client, _ := NewClientWithOptions("dummy-key", ts.URL, false)
	services, err := client.FindServices()

	if err != nil {
		t.Error("err shoud be nil but: ", err)
	}

	if services[0].Memo != "hello" {
		t.Error("request sends json including memo but: ", services[0])
	}

	if reflect.DeepEqual(services[0].Roles, []string{"db-master", "db-slave"}) != true {
		t.Errorf("Wrong data for roles: %v", services[0].Roles)
	}

}

func TestCreateService(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		if req.URL.Path != "/api/v0/services" {
			t.Error("request URL should be /api/v0/services but :", req.URL.Path)
		}

		if req.Method != "POST" {
			t.Error("request method should be POST but: ", req.Method)
		}

		respJSON, _ := json.Marshal(map[string]interface{}{
			"name":  "My-Service",
			"memo":  "hello",
			"roles": []string{},
		})

		res.Header()["Content-Type"] = []string{"application/json"}
		fmt.Fprint(res, string(respJSON))
	}))
	defer ts.Close()

	client, _ := NewClientWithOptions("dummy-key", ts.URL, false)

	service, err := client.CreateService(&CreateServiceParam{
		Name: "My-Service",
		Memo: "hello",
	})

	if err != nil {
		t.Error("err shoud be nil but: ", err)
	}

	if service.Name != "My-Service" {
		t.Error("request sends json including name but: ", service.Name)
	}

	if service.Memo != "hello" {
		t.Error("request sends json including name but: ", service.Memo)
	}

	if len(service.Roles) != 0 {
		t.Error("request sends json including name but: ", service.Roles)
	}
}
<|endoftext|>


<commit_before>// Copyright 2017 The Xorm 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 xorm

import (
	"errors"
	"fmt"
	"reflect"

	"github.com/go-xorm/builder"
)

// Exist returns true if the record exist otherwise return false
func (session *Session) Exist(bean ...interface{}) (bool, error) {
	if session.isAutoClose {
		defer session.Close()
	}

	var sqlStr string
	var args []interface{}
	var err error

	if session.statement.RawSQL == "" {
		if len(bean) == 0 {
			tableName := session.statement.TableName()
			if len(tableName) <= 0 {
				return false, ErrTableNotFound
			}

			if session.statement.cond.IsValid() {
				condSQL, condArgs, err := builder.ToSQL(session.statement.cond)
				if err != nil {
					return false, err
				}

				sqlStr = fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT 1", tableName, condSQL)
				args = condArgs
			} else {
				sqlStr = fmt.Sprintf("SELECT * FROM %s LIMIT 1", tableName)
				args = []interface{}{}
			}
		} else {
			beanValue := reflect.ValueOf(bean[0])
			if beanValue.Kind() != reflect.Ptr {
				return false, errors.New("needs a pointer")
			}

			if beanValue.Elem().Kind() == reflect.Struct {
				if err := session.statement.setRefValue(beanValue.Elem()); err != nil {
					return false, err
				}
			}

			if len(session.statement.TableName()) <= 0 {
				return false, ErrTableNotFound
			}
			session.statement.Limit(1)
			sqlStr, args, err = session.statement.genGetSQL(bean[0])
			if err != nil {
				return false, err
			}
		}
	} else {
		sqlStr = session.statement.RawSQL
		args = session.statement.RawParams
	}

	rows, err := session.queryRows(sqlStr, args...)
	if err != nil {
		return false, err
	}
	defer rows.Close()

	return rows.Next(), nil
}
<commit_msg>fix Exist bug when using on MSSQL<commit_after>// Copyright 2017 The Xorm 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 xorm

import (
	"errors"
	"fmt"
	"reflect"

	"github.com/go-xorm/builder"
	"github.com/xormplus/core"
)

// Exist returns true if the record exist otherwise return false
func (session *Session) Exist(bean ...interface{}) (bool, error) {
	if session.isAutoClose {
		defer session.Close()
	}

	var sqlStr string
	var args []interface{}
	var err error

	if session.statement.RawSQL == "" {
		if len(bean) == 0 {
			tableName := session.statement.TableName()
			if len(tableName) <= 0 {
				return false, ErrTableNotFound
			}

			if session.statement.cond.IsValid() {
				condSQL, condArgs, err := builder.ToSQL(session.statement.cond)
				if err != nil {
					return false, err
				}

				if session.engine.dialect.DBType() == core.MSSQL {
					sqlStr = fmt.Sprintf("SELECT top 1 * FROM %s WHERE %s", tableName, condSQL)
				} else {
					sqlStr = fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT 1", tableName, condSQL)
				}
				args = condArgs
			} else {
				if session.engine.dialect.DBType() == core.MSSQL {
					sqlStr = fmt.Sprintf("SELECT top 1 * FROM %s", tableName)
				} else {
					sqlStr = fmt.Sprintf("SELECT * FROM %s LIMIT 1", tableName)
				}
				args = []interface{}{}
			}
		} else {
			beanValue := reflect.ValueOf(bean[0])
			if beanValue.Kind() != reflect.Ptr {
				return false, errors.New("needs a pointer")
			}

			if beanValue.Elem().Kind() == reflect.Struct {
				if err := session.statement.setRefValue(beanValue.Elem()); err != nil {
					return false, err
				}
			}

			if len(session.statement.TableName()) <= 0 {
				return false, ErrTableNotFound
			}
			session.statement.Limit(1)
			sqlStr, args, err = session.statement.genGetSQL(bean[0])
			if err != nil {
				return false, err
			}
		}
	} else {
		sqlStr = session.statement.RawSQL
		args = session.statement.RawParams
	}

	rows, err := session.queryRows(sqlStr, args...)
	if err != nil {
		return false, err
	}
	defer rows.Close()

	return rows.Next(), nil
}
<|endoftext|>


<commit_before>package hitch

import (
	"context"
	"net/http"

	"github.com/julienschmidt/httprouter"
)

// Hitch ties httprouter, context, and middleware up in a bow.
type Hitch struct {
	Router     *httprouter.Router
	middleware []func(http.Handler) http.Handler
}

// New initializes a new Hitch.
func New() *Hitch {
	r := httprouter.New()
	r.HandleMethodNotAllowed = false // may cause problems otherwise
	return &Hitch{
		Router: r,
	}
}

// Use installs one or more middleware in the Hitch request cycle.
func (h *Hitch) Use(middleware ...func(http.Handler) http.Handler) {
	h.middleware = append(h.middleware, middleware...)
}

// UseHandler registers an http.Handler as a middleware.
func (h *Hitch) UseHandler(handler http.Handler) {
	h.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			handler.ServeHTTP(w, req)
			next.ServeHTTP(w, req)
		})
	})
}

// Next registers an http.Handler as a fallback/not-found handler.
func (h *Hitch) Next(handler http.Handler) {
	h.Router.NotFound = handler
}

// Handle registers a handler for the given method and path.
func (h *Hitch) Handle(method, path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	for i := len(middleware) - 1; i >= 0; i-- {
		handler = middleware[i](handler)
	}
	h.Router.Handle(method, path, wrap(handler))
}

// HandleFunc registers a func handler for the given method and path.
func (h *Hitch) HandleFunc(method, path string, handler func(http.ResponseWriter, *http.Request), middleware ...func(http.Handler) http.Handler) {
	h.Handle(method, path, http.HandlerFunc(handler), middleware...)
}

// Get registers a GET handler for the given path.
func (h *Hitch) Get(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("GET", path, handler, middleware...)
}

// Put registers a PUT handler for the given path.
func (h *Hitch) Put(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("PUT", path, handler, middleware...)
}

// Post registers a POST handler for the given path.
func (h *Hitch) Post(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("POST", path, handler, middleware...)
}

// Patch registers a PATCH handler for the given path.
func (h *Hitch) Patch(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("PATCH", path, handler, middleware...)
}

// Delete registers a DELETE handler for the given path.
func (h *Hitch) Delete(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("DELETE", path, handler, middleware...)
}

// Options registers a OPTIONS handler for the given path.
func (h *Hitch) Options(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("OPTIONS", path, handler, middleware...)
}

// Handler returns an http.Handler for the embedded router and middleware.
func (h *Hitch) Handler() http.Handler {
	handler := http.Handler(h.Router)
	for i := len(h.middleware) - 1; i >= 0; i-- {
		handler = h.middleware[i](handler)
	}
	return handler
}

type key int

const paramsKey key = 1

func wrap(handler http.Handler) httprouter.Handle {
	return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
		ctx := context.WithValue(req.Context(), paramsKey, params)
		*req = *req.WithContext(ctx)
		handler.ServeHTTP(w, req)
	}
}

// Params returns the httprouter.Params for req.
func Params(req *http.Request) httprouter.Params {
	// zero value (httprouter.Params{}) is returned when type assertion failed
	return req.Context().Value(paramsKey).(httprouter.Params)
}
<commit_msg>fixed to not dereference a request<commit_after>package hitch

import (
	"context"
	"net/http"

	"github.com/julienschmidt/httprouter"
)

// Hitch ties httprouter, context, and middleware up in a bow.
type Hitch struct {
	Router     *httprouter.Router
	middleware []func(http.Handler) http.Handler
}

// New initializes a new Hitch.
func New() *Hitch {
	r := httprouter.New()
	r.HandleMethodNotAllowed = false // may cause problems otherwise
	return &Hitch{
		Router: r,
	}
}

// Use installs one or more middleware in the Hitch request cycle.
func (h *Hitch) Use(middleware ...func(http.Handler) http.Handler) {
	h.middleware = append(h.middleware, middleware...)
}

// UseHandler registers an http.Handler as a middleware.
func (h *Hitch) UseHandler(handler http.Handler) {
	h.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			handler.ServeHTTP(w, req)
			next.ServeHTTP(w, req)
		})
	})
}

// Next registers an http.Handler as a fallback/not-found handler.
func (h *Hitch) Next(handler http.Handler) {
	h.Router.NotFound = handler
}

// Handle registers a handler for the given method and path.
func (h *Hitch) Handle(method, path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	for i := len(middleware) - 1; i >= 0; i-- {
		handler = middleware[i](handler)
	}
	h.Router.Handle(method, path, wrap(handler))
}

// HandleFunc registers a func handler for the given method and path.
func (h *Hitch) HandleFunc(method, path string, handler func(http.ResponseWriter, *http.Request), middleware ...func(http.Handler) http.Handler) {
	h.Handle(method, path, http.HandlerFunc(handler), middleware...)
}

// Get registers a GET handler for the given path.
func (h *Hitch) Get(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("GET", path, handler, middleware...)
}

// Put registers a PUT handler for the given path.
func (h *Hitch) Put(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("PUT", path, handler, middleware...)
}

// Post registers a POST handler for the given path.
func (h *Hitch) Post(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("POST", path, handler, middleware...)
}

// Patch registers a PATCH handler for the given path.
func (h *Hitch) Patch(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("PATCH", path, handler, middleware...)
}

// Delete registers a DELETE handler for the given path.
func (h *Hitch) Delete(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("DELETE", path, handler, middleware...)
}

// Options registers a OPTIONS handler for the given path.
func (h *Hitch) Options(path string, handler http.Handler, middleware ...func(http.Handler) http.Handler) {
	h.Handle("OPTIONS", path, handler, middleware...)
}

// Handler returns an http.Handler for the embedded router and middleware.
func (h *Hitch) Handler() http.Handler {
	handler := http.Handler(h.Router)
	for i := len(h.middleware) - 1; i >= 0; i-- {
		handler = h.middleware[i](handler)
	}
	return handler
}

type key int

const paramsKey key = 1

func wrap(handler http.Handler) httprouter.Handle {
	return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
		ctx := context.WithValue(req.Context(), paramsKey, params)
		handler.ServeHTTP(w, req.WithContext(ctx))
	}
}

// Params returns the httprouter.Params for req.
func Params(req *http.Request) httprouter.Params {
	// zero value (httprouter.Params{}) is returned when type assertion failed
	return req.Context().Value(paramsKey).(httprouter.Params)
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"

	"github.com/gorilla/mux"
)

var catalogue []Sock

var dev bool
var port string

func main() {

	flag.BoolVar(&dev, "dev", false, "Run in development mode")
	flag.StringVar(&port, "port", "8081", "Port on which to run")
	flag.Parse()

	var file string
	if dev {
		file = "./socks.json"
	} else {
		file = "/config/socks.json"
	}
	loadCatalogue(file)

	router := mux.NewRouter().StrictSlash(true)
	router.HandleFunc("/catalogue", catalogueHandler)
	router.HandleFunc("/catalogue/{catId}", itemHandler)
	fmt.Printf("Catalogue service running on port %s\n", port)
	http.ListenAndServe(":" + port, router)
}

func catalogueHandler(w http.ResponseWriter, r *http.Request) {
	var data []byte
	var err error

	data, err = json.Marshal(catalogue)
	if err != nil {
		panic(err)
	}
	w.Write(data)
}

func itemHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
    catId := vars["catId"]

	for _, sock := range catalogue {
		if sock.Id == catId {
			data, _ := json.Marshal(sock)
			w.Write(data)
			return
		}
	}
	w.WriteHeader(404)
}

func loadCatalogue(file string) {
	f, err := ioutil.ReadFile(file)
    if err != nil {
        panic(err)
    }

    json.Unmarshal(f, &catalogue)
    fmt.Printf("Loaded %d items into catalogue.\n", len(catalogue))
}

type Sock struct {
	Id string `json:id`
	Price int `json:price`
	Count int `json:"count"`
	// Size, image, name?
}<commit_msg>added pagination and sorting to catalogue service<commit_after>package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"sort"
	"strconv"
	"strings"

	"github.com/gorilla/mux"
)

var catalogue []Sock

var dev bool
var port string

func main() {

	flag.BoolVar(&dev, "dev", false, "Run in development mode")
	flag.StringVar(&port, "port", "8081", "Port on which to run")
	flag.Parse()

	var file string
	if dev {
		file = "./socks.json"
	} else {
		file = "/config/socks.json"
	}
	loadCatalogue(file)

	router := mux.NewRouter().StrictSlash(false)
	router.HandleFunc("/catalogue", catalogueHandler)
	router.HandleFunc("/catalogue/{catId}", itemHandler)
	router.HandleFunc("/images", imageHandler)
	router.HandleFunc("/tags/{tag}", tagHandler)
	fmt.Printf("Catalogue service running on port %s\n", port)
	http.ListenAndServe(":" + port, router)
}

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

	page := r.FormValue("page")
	size := r.FormValue("size")
	sortField := r.FormValue("sort")

	pageCount := 1
	if len(page) > 0 {
		pageCount, _ = strconv.Atoi(page)
	}
	perPage := 10
	if len(size) > 0 {
		perPage, _ = strconv.Atoi(size)
	}
	sortOn := "id"
	if len(sortField) > 0 {
		sortOn = strings.ToLower(sortField)
	}

	var sorted []Sock = catalogue[:]
	switch sortOn {
		case "id":
			sort.Sort(IdSorter(sorted))
		case "name":
			sort.Sort(NameSorter(sorted))
		case "description":
			sort.Sort(DescriptionSorter(sorted))
		case "price":
			sort.Sort(PriceSorter(sorted))
		case "count":
			sort.Sort(CountSorter(sorted))
		case "tag":
			sort.Sort(TagSorter(sorted))
	}
	end := (pageCount * perPage)
	start := end - perPage

	var data []byte
	var err error

	fmt.Printf("Fetching items from %d to %d. Sorted by %s\n", start, end, sortOn)

	data, err = json.Marshal(sorted[start:end])
	if err != nil {
		panic(err)
	}
	w.Write(data)
}

func itemHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
    catId := vars["catId"]

	for _, sock := range catalogue {
		if sock.Id == catId {
			data, _ := json.Marshal(sock)
			w.Write(data)
			return
		}
	}
	w.WriteHeader(404)
}

func imageHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(501)
}

func tagHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(501)
}

func loadCatalogue(file string) {
	f, err := ioutil.ReadFile(file)
    if err != nil {
        panic(err)
    }

    json.Unmarshal(f, &catalogue)
    fmt.Printf("Loaded %d items into catalogue.\n", len(catalogue))
}

type Sock struct {
	Id string `json:id`
	Name string `json:"name"`
	Description string `json:"description"`
	ImageURL string `json:"imageUrl"`
	Price int `json:price`
	Count int `json:"count"`
	Tag []string `json:"tag"`
}

type IdSorter []Sock

func (a IdSorter) Len() int           { return len(a) }
func (a IdSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a IdSorter) Less(i, j int) bool { return a[i].Id < a[j].Id }

type NameSorter []Sock

func (a NameSorter) Len() int           { return len(a) }
func (a NameSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a NameSorter) Less(i, j int) bool { return a[i].Name < a[j].Name }

type DescriptionSorter []Sock

func (a DescriptionSorter) Len() int           { return len(a) }
func (a DescriptionSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a DescriptionSorter) Less(i, j int) bool { return a[i].Description < a[j].Description }

type PriceSorter []Sock

func (a PriceSorter) Len() int           { return len(a) }
func (a PriceSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a PriceSorter) Less(i, j int) bool { return a[i].Price < a[j].Price }

type CountSorter []Sock

func (a CountSorter) Len() int           { return len(a) }
func (a CountSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a CountSorter) Less(i, j int) bool { return a[i].Count < a[j].Count }

type TagSorter []Sock

func (a TagSorter) Len() int           { return len(a) }
func (a TagSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a TagSorter) Less(i, j int) bool { return len(a[i].Tag) < len(a[j].Id) }<|endoftext|>


<commit_before>package gitlab

import (
	"fmt"
	"time"
)

// MergeRequestApprovalsService handles communication with the merge request
// approvals related methods of the GitLab API. This includes reading/updating
// approval settings and approve/unapproving merge requests
//
// GitLab API docs: https://docs.gitlab.com/ee/api/merge_request_approvals.html
type MergeRequestApprovalsService struct {
	client *Client
}

// MergeRequestApprovals represents GitLab merge request approvals.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#merge-request-level-mr-approvals
type MergeRequestApprovals struct {
	ID                   int                          `json:"id"`
	ProjectID            int                          `json:"project_id"`
	Title                string                       `json:"title"`
	Description          string                       `json:"description"`
	State                string                       `json:"state"`
	CreatedAt            *time.Time                   `json:"created_at"`
	UpdatedAt            *time.Time                   `json:"updated_at"`
	MergeStatus          string                       `json:"merge_status"`
	ApprovalsBeforeMerge int                          `json:"approvals_before_merge"`
	ApprovalsRequired    int                          `json:"approvals_required"`
	ApprovalsLeft        int                          `json:"approvals_left"`
	ApprovedBy           []*MergeRequestApproverUser  `json:"approved_by"`
	Approvers            []*MergeRequestApproverUser  `json:"approvers"`
	ApproverGroups       []*MergeRequestApproverGroup `json:"approver_groups"`
	SuggestedApprovers   []*BasicUser                 `json:"suggested_approvers"`
}

func (m MergeRequestApprovals) String() string {
	return Stringify(m)
}

// MergeRequestApproverGroup  represents GitLab project level merge request approver group.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#project-level-mr-approvals
type MergeRequestApproverGroup struct {
	Group struct {
		ID                   int    `json:"id"`
		Name                 string `json:"name"`
		Path                 string `json:"path"`
		Description          string `json:"description"`
		Visibility           string `json:"visibility"`
		AvatarURL            string `json:"avatar_url"`
		WebURL               string `json:"web_url"`
		FullName             string `json:"full_name"`
		FullPath             string `json:"full_path"`
		LFSEnabled           bool   `json:"lfs_enabled"`
		RequestAccessEnabled bool   `json:"request_access_enabled"`
	}
}

// MergeRequestApprovalRule represents a GitLab merge request approval rule.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-merge-request-level-rules
type MergeRequestApprovalRule struct {
	ID                   int                        `json:"id,omitempty"`
	Name                 string                     `json:"name,omitempty"`
	RuleType             string                     `json:"rule_type,omitempty"`
	EligibleApprovers    []*BasicUser               `json:"eligible_approvers,omitempty"`
	ApprovalsRequired    int                        `json:"approvals_required,omitempty"`
	SourceRule           *CreateApprovalRuleOptions `json:"source_rule,omitempty"`
	Users                []*BasicUser               `json:"users,omitempty"`
	Groups               []*Group                   `json:"groups,omitempty"`
	ContainsHiddenGroups bool                       `json:"contains_hidden_groups,omitempty"`
}

func (s MergeRequestApprovalRule) String() string {
	return Stringify(s)
}

// MergeRequestApproverUser  represents GitLab project level merge request approver user.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#project-level-mr-approvals
type MergeRequestApproverUser struct {
	User *BasicUser
}

// ApproveMergeRequestOptions represents the available ApproveMergeRequest() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#approve-merge-request
type ApproveMergeRequestOptions struct {
	SHA *string `url:"sha,omitempty" json:"sha,omitempty"`
}

// ApproveMergeRequest approves a merge request on GitLab. If a non-empty sha
// is provided then it must match the sha at the HEAD of the MR.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#approve-merge-request
func (s *MergeRequestApprovalsService) ApproveMergeRequest(pid interface{}, mr int, opt *ApproveMergeRequestOptions, options ...OptionFunc) (*MergeRequestApprovals, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approve", pathEscape(project), mr)

	req, err := s.client.NewRequest("POST", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	m := new(MergeRequestApprovals)
	resp, err := s.client.Do(req, m)
	if err != nil {
		return nil, resp, err
	}

	return m, resp, err
}

// UnapproveMergeRequest unapproves a previously approved merge request on GitLab.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#unapprove-merge-request
func (s *MergeRequestApprovalsService) UnapproveMergeRequest(pid interface{}, mr int, options ...OptionFunc) (*Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/unapprove", pathEscape(project), mr)

	req, err := s.client.NewRequest("POST", u, nil, options)
	if err != nil {
		return nil, err
	}

	return s.client.Do(req, nil)
}

// ChangeMergeRequestApprovalConfigurationOptions represents the available
// ChangeMergeRequestApprovalConfiguration() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-approval-configuration
type ChangeMergeRequestApprovalConfigurationOptions struct {
	ApprovalsRequired *int `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
}

// ChangeApprovalConfiguration updates the approval configuration of a merge request.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-approval-configuration
func (s *MergeRequestApprovalsService) ChangeApprovalConfiguration(pid interface{}, mergeRequestIID int, opt *ChangeMergeRequestApprovalConfigurationOptions, options ...OptionFunc) (*MergeRequest, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approvals", pathEscape(project), mergeRequestIID)

	req, err := s.client.NewRequest("POST", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	m := new(MergeRequest)
	resp, err := s.client.Do(req, m)
	if err != nil {
		return nil, resp, err
	}

	return m, resp, err
}

// ChangeMergeRequestAllowedApproversOptions represents the available
// ChangeMergeRequestAllowedApprovers() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-allowed-approvers-for-merge-request
type ChangeMergeRequestAllowedApproversOptions struct {
	ApproverIDs      []int `url:"approver_ids" json:"approver_ids"`
	ApproverGroupIDs []int `url:"approver_group_ids" json:"approver_group_ids"`
}

// ChangeAllowedApprovers updates the approvers for a merge request.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-allowed-approvers-for-merge-request
func (s *MergeRequestApprovalsService) ChangeAllowedApprovers(pid interface{}, mergeRequestIID int, opt *ChangeMergeRequestAllowedApproversOptions, options ...OptionFunc) (*MergeRequest, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approvers", pathEscape(project), mergeRequestIID)

	req, err := s.client.NewRequest("PUT", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	m := new(MergeRequest)
	resp, err := s.client.Do(req, m)
	if err != nil {
		return nil, resp, err
	}

	return m, resp, err
}

// GetApprovalRules requests information about a merge request’s approval rules
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-merge-request-level-rules
func (s *MergeRequestApprovalsService) GetApprovalRules(pid interface{}, mergeRequestIID int, options ...OptionFunc) ([]*MergeRequestApprovalRule, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules", pathEscape(project), mergeRequestIID)

	req, err := s.client.NewRequest("GET", u, nil, options)
	if err != nil {
		return nil, nil, err
	}

	var par []*MergeRequestApprovalRule
	resp, err := s.client.Do(req, &par)
	if err != nil {
		return nil, resp, err
	}

	return par, resp, err
}

// CreateMergeRequestApprovalRuleOptions represents the available CreateApprovalRule()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#create-merge-request-level-rule
type CreateMergeRequestApprovalRuleOptions struct {
	Name                  *string `url:"name,omitempty" json:"name,omitempty"`
	ApprovalsRequired     *int    `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
	ApprovalProjectRuleID *int    `url:"approval_project_rule_id,omitempty" json:"approval_project_rule_id,omitempty"`
	UserIDs               []int   `url:"user_ids,omitempty" json:"user_ids,omitempty"`
	GroupIDs              []int   `url:"group_ids,omitempty" json:"group_ids,omitempty"`
}

// CreateApprovalRule creates a new MR level approval rule.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#create-merge-request-level-rule
func (s *MergeRequestApprovalsService) CreateApprovalRule(pid interface{}, mergeRequestIID int, opt *CreateMergeRequestApprovalRuleOptions, options ...OptionFunc) (*MergeRequestApprovalRule, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules", pathEscape(project), mergeRequestIID)

	req, err := s.client.NewRequest("POST", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	par := new(MergeRequestApprovalRule)
	resp, err := s.client.Do(req, &par)
	if err != nil {
		return nil, resp, err
	}

	return par, resp, err
}

// UpdateMergeRequestApprovalRuleOptions represents the available UpdateApprovalRule()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#update-merge-request-level-rule
type UpdateMergeRequestApprovalRuleOptions struct {
	Name              *string `url:"name,omitempty" json:"name,omitempty"`
	ApprovalsRequired *int    `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
	UserIDs           []int   `url:"user_ids,omitempty" json:"user_ids,omitempty"`
	GroupIDs          []int   `url:"group_ids,omitempty" json:"group_ids,omitempty"`
}

// UpdateApprovalRule updates an existing approval rule with new options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#update-merge-request-level-rule
func (s *MergeRequestApprovalsService) UpdateApprovalRule(pid interface{}, mergeRequestIID int, approvalRule int, opt *UpdateMergeRequestApprovalRuleOptions, options ...OptionFunc) (*MergeRequestApprovalRule, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules/%d", pathEscape(project), mergeRequestIID, approvalRule)

	req, err := s.client.NewRequest("PUT", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	par := new(MergeRequestApprovalRule)
	resp, err := s.client.Do(req, &par)
	if err != nil {
		return nil, resp, err
	}

	return par, resp, err
}

// DeleteApprovalRule deletes a mr level approval rule.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#delete-merge-request-level-rule
func (s *MergeRequestApprovalsService) DeleteApprovalRule(pid interface{}, mergeRequestIID int, approvalRule int, options ...OptionFunc) (*Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules/%d", pathEscape(project), mergeRequestIID, approvalRule)

	req, err := s.client.NewRequest("DELETE", u, nil, options)
	if err != nil {
		return nil, err
	}

	return s.client.Do(req, nil)
}
<commit_msg>Fix MergeRequestApprovalRule struct<commit_after>package gitlab

import (
	"fmt"
	"time"
)

// MergeRequestApprovalsService handles communication with the merge request
// approvals related methods of the GitLab API. This includes reading/updating
// approval settings and approve/unapproving merge requests
//
// GitLab API docs: https://docs.gitlab.com/ee/api/merge_request_approvals.html
type MergeRequestApprovalsService struct {
	client *Client
}

// MergeRequestApprovals represents GitLab merge request approvals.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#merge-request-level-mr-approvals
type MergeRequestApprovals struct {
	ID                   int                          `json:"id"`
	ProjectID            int                          `json:"project_id"`
	Title                string                       `json:"title"`
	Description          string                       `json:"description"`
	State                string                       `json:"state"`
	CreatedAt            *time.Time                   `json:"created_at"`
	UpdatedAt            *time.Time                   `json:"updated_at"`
	MergeStatus          string                       `json:"merge_status"`
	ApprovalsBeforeMerge int                          `json:"approvals_before_merge"`
	ApprovalsRequired    int                          `json:"approvals_required"`
	ApprovalsLeft        int                          `json:"approvals_left"`
	ApprovedBy           []*MergeRequestApproverUser  `json:"approved_by"`
	Approvers            []*MergeRequestApproverUser  `json:"approvers"`
	ApproverGroups       []*MergeRequestApproverGroup `json:"approver_groups"`
	SuggestedApprovers   []*BasicUser                 `json:"suggested_approvers"`
}

func (m MergeRequestApprovals) String() string {
	return Stringify(m)
}

// MergeRequestApproverGroup  represents GitLab project level merge request approver group.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#project-level-mr-approvals
type MergeRequestApproverGroup struct {
	Group struct {
		ID                   int    `json:"id"`
		Name                 string `json:"name"`
		Path                 string `json:"path"`
		Description          string `json:"description"`
		Visibility           string `json:"visibility"`
		AvatarURL            string `json:"avatar_url"`
		WebURL               string `json:"web_url"`
		FullName             string `json:"full_name"`
		FullPath             string `json:"full_path"`
		LFSEnabled           bool   `json:"lfs_enabled"`
		RequestAccessEnabled bool   `json:"request_access_enabled"`
	}
}

// MergeRequestApprovalRule represents a GitLab merge request approval rule.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-merge-request-level-rules
type MergeRequestApprovalRule struct {
	ID                   int                                    `json:"id,omitempty"`
	Name                 string                                 `json:"name,omitempty"`
	RuleType             string                                 `json:"rule_type,omitempty"`
	EligibleApprovers    []*BasicUser                           `json:"eligible_approvers,omitempty"`
	ApprovalsRequired    int                                    `json:"approvals_required,omitempty"`
	SourceRule           *CreateMergeRequestApprovalRuleOptions `json:"source_rule,omitempty"`
	Users                []*BasicUser                           `json:"users,omitempty"`
	Groups               []*Group                               `json:"groups,omitempty"`
	ContainsHiddenGroups bool                                   `json:"contains_hidden_groups,omitempty"`
}

func (s MergeRequestApprovalRule) String() string {
	return Stringify(s)
}

// MergeRequestApproverUser  represents GitLab project level merge request approver user.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#project-level-mr-approvals
type MergeRequestApproverUser struct {
	User *BasicUser
}

// ApproveMergeRequestOptions represents the available ApproveMergeRequest() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#approve-merge-request
type ApproveMergeRequestOptions struct {
	SHA *string `url:"sha,omitempty" json:"sha,omitempty"`
}

// ApproveMergeRequest approves a merge request on GitLab. If a non-empty sha
// is provided then it must match the sha at the HEAD of the MR.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#approve-merge-request
func (s *MergeRequestApprovalsService) ApproveMergeRequest(pid interface{}, mr int, opt *ApproveMergeRequestOptions, options ...OptionFunc) (*MergeRequestApprovals, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approve", pathEscape(project), mr)

	req, err := s.client.NewRequest("POST", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	m := new(MergeRequestApprovals)
	resp, err := s.client.Do(req, m)
	if err != nil {
		return nil, resp, err
	}

	return m, resp, err
}

// UnapproveMergeRequest unapproves a previously approved merge request on GitLab.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#unapprove-merge-request
func (s *MergeRequestApprovalsService) UnapproveMergeRequest(pid interface{}, mr int, options ...OptionFunc) (*Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/unapprove", pathEscape(project), mr)

	req, err := s.client.NewRequest("POST", u, nil, options)
	if err != nil {
		return nil, err
	}

	return s.client.Do(req, nil)
}

// ChangeMergeRequestApprovalConfigurationOptions represents the available
// ChangeMergeRequestApprovalConfiguration() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-approval-configuration
type ChangeMergeRequestApprovalConfigurationOptions struct {
	ApprovalsRequired *int `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
}

// ChangeApprovalConfiguration updates the approval configuration of a merge request.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-approval-configuration
func (s *MergeRequestApprovalsService) ChangeApprovalConfiguration(pid interface{}, mergeRequestIID int, opt *ChangeMergeRequestApprovalConfigurationOptions, options ...OptionFunc) (*MergeRequest, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approvals", pathEscape(project), mergeRequestIID)

	req, err := s.client.NewRequest("POST", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	m := new(MergeRequest)
	resp, err := s.client.Do(req, m)
	if err != nil {
		return nil, resp, err
	}

	return m, resp, err
}

// ChangeMergeRequestAllowedApproversOptions represents the available
// ChangeMergeRequestAllowedApprovers() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-allowed-approvers-for-merge-request
type ChangeMergeRequestAllowedApproversOptions struct {
	ApproverIDs      []int `url:"approver_ids" json:"approver_ids"`
	ApproverGroupIDs []int `url:"approver_group_ids" json:"approver_group_ids"`
}

// ChangeAllowedApprovers updates the approvers for a merge request.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-allowed-approvers-for-merge-request
func (s *MergeRequestApprovalsService) ChangeAllowedApprovers(pid interface{}, mergeRequestIID int, opt *ChangeMergeRequestAllowedApproversOptions, options ...OptionFunc) (*MergeRequest, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approvers", pathEscape(project), mergeRequestIID)

	req, err := s.client.NewRequest("PUT", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	m := new(MergeRequest)
	resp, err := s.client.Do(req, m)
	if err != nil {
		return nil, resp, err
	}

	return m, resp, err
}

// GetApprovalRules requests information about a merge request’s approval rules
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-merge-request-level-rules
func (s *MergeRequestApprovalsService) GetApprovalRules(pid interface{}, mergeRequestIID int, options ...OptionFunc) ([]*MergeRequestApprovalRule, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules", pathEscape(project), mergeRequestIID)

	req, err := s.client.NewRequest("GET", u, nil, options)
	if err != nil {
		return nil, nil, err
	}

	var par []*MergeRequestApprovalRule
	resp, err := s.client.Do(req, &par)
	if err != nil {
		return nil, resp, err
	}

	return par, resp, err
}

// CreateMergeRequestApprovalRuleOptions represents the available CreateApprovalRule()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#create-merge-request-level-rule
type CreateMergeRequestApprovalRuleOptions struct {
	Name                  *string `url:"name,omitempty" json:"name,omitempty"`
	ApprovalsRequired     *int    `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
	ApprovalProjectRuleID *int    `url:"approval_project_rule_id,omitempty" json:"approval_project_rule_id,omitempty"`
	UserIDs               []int   `url:"user_ids,omitempty" json:"user_ids,omitempty"`
	GroupIDs              []int   `url:"group_ids,omitempty" json:"group_ids,omitempty"`
}

// CreateApprovalRule creates a new MR level approval rule.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#create-merge-request-level-rule
func (s *MergeRequestApprovalsService) CreateApprovalRule(pid interface{}, mergeRequestIID int, opt *CreateMergeRequestApprovalRuleOptions, options ...OptionFunc) (*MergeRequestApprovalRule, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules", pathEscape(project), mergeRequestIID)

	req, err := s.client.NewRequest("POST", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	par := new(MergeRequestApprovalRule)
	resp, err := s.client.Do(req, &par)
	if err != nil {
		return nil, resp, err
	}

	return par, resp, err
}

// UpdateMergeRequestApprovalRuleOptions represents the available UpdateApprovalRule()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#update-merge-request-level-rule
type UpdateMergeRequestApprovalRuleOptions struct {
	Name              *string `url:"name,omitempty" json:"name,omitempty"`
	ApprovalsRequired *int    `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
	UserIDs           []int   `url:"user_ids,omitempty" json:"user_ids,omitempty"`
	GroupIDs          []int   `url:"group_ids,omitempty" json:"group_ids,omitempty"`
}

// UpdateApprovalRule updates an existing approval rule with new options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#update-merge-request-level-rule
func (s *MergeRequestApprovalsService) UpdateApprovalRule(pid interface{}, mergeRequestIID int, approvalRule int, opt *UpdateMergeRequestApprovalRuleOptions, options ...OptionFunc) (*MergeRequestApprovalRule, *Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules/%d", pathEscape(project), mergeRequestIID, approvalRule)

	req, err := s.client.NewRequest("PUT", u, opt, options)
	if err != nil {
		return nil, nil, err
	}

	par := new(MergeRequestApprovalRule)
	resp, err := s.client.Do(req, &par)
	if err != nil {
		return nil, resp, err
	}

	return par, resp, err
}

// DeleteApprovalRule deletes a mr level approval rule.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/merge_request_approvals.html#delete-merge-request-level-rule
func (s *MergeRequestApprovalsService) DeleteApprovalRule(pid interface{}, mergeRequestIID int, approvalRule int, options ...OptionFunc) (*Response, error) {
	project, err := parseID(pid)
	if err != nil {
		return nil, err
	}
	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules/%d", pathEscape(project), mergeRequestIID, approvalRule)

	req, err := s.client.NewRequest("DELETE", u, nil, options)
	if err != nil {
		return nil, err
	}

	return s.client.Do(req, nil)
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"flag"
	"fmt"
	"log"
	"net"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

type timeoutLock struct {
	mutex sync.Mutex
	id    int64 // unique ID of the current lock. Only allow an unlock if the correct id is passed
}

var locksLock sync.RWMutex
var locks = map[string]*timeoutLock{}

func main() {
	var port int
	flag.IntVar(&port, "p", 45625, "port")
	listener, err := net.Listen("tcp", ":"+strconv.Itoa(port))
	if err != nil {
		log.Fatalln("error listening", err)
	}

	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Println("error accepting", err)
			return
		}
		go handleConn(conn)
	}
}

var (
	unlockedResponse    = []byte("UNLOCKED\r\n")
	notUnlockedResponse = []byte("NOT_UNLOCKED\r\n")

	errBadFormat      = []byte("ERROR bad command format\r\n")
	errUnknownCommand = []byte("ERROR unknown command\r\n")
	errLockNotFound   = []byte("ERROR lock not found\r\n")
)

func handleConn(conn net.Conn) {
	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		split := strings.Fields(scanner.Text())
		if len(split) < 3 {
			conn.Write(errBadFormat)
			continue
		}
		cmd := split[0]
		key := split[1]
		switch cmd {
		// LOCK <key> <timeout>
		case "LOCK":
			timeout, err := strconv.Atoi(split[2])

			if err != nil {
				conn.Write(errBadFormat)
				continue
			}
			locksLock.RLock()
			lock, ok := locks[key]
			locksLock.RUnlock()
			if !ok {
				// lock doesn't exist; create it
				locksLock.Lock()
				lock, ok = locks[key]
				if !ok {
					lock = &timeoutLock{}
					locks[key] = lock
				}
				locksLock.Unlock()
			}

			lock.mutex.Lock()
			id := atomic.AddInt64(&lock.id, 1)
			time.AfterFunc(time.Duration(timeout)*time.Millisecond, func() {
				if atomic.CompareAndSwapInt64(&lock.id, id, id+1) {
					lock.mutex.Unlock()
				}
			})
			fmt.Fprintf(conn, "LOCKED %v\r\n", id)

			log.Printf("Request:  %-12s | Key:  %-15s | Timeout: %dms", cmd, key, timeout)
			log.Printf("Response: %-12s | Key:  %-15s | Id: %d", "LOCKED", key, id)

		// UNLOCK <key> <id>
		case "UNLOCK":
			id, err := strconv.ParseInt(split[2], 10, 64)

			if err != nil {
				conn.Write(errBadFormat)
				continue
			}
			locksLock.RLock()
			lock, ok := locks[key]
			locksLock.RUnlock()
			if !ok {
				conn.Write(errLockNotFound)

				log.Printf("Request:  %-12s | Key:  %-15s | Id: %d", cmd, key, id)
				log.Printf("Response: %-12s | Key:  %-15s", "404", key)
				continue
			}
			if atomic.CompareAndSwapInt64(&lock.id, id, id+1) {
				lock.mutex.Unlock()
				conn.Write(unlockedResponse)

				log.Printf("Request:  %-12s | Key:  %-15s | Id: %d", cmd, key, id)
				log.Printf("Response: %-12s | Key:  %-15s | Id: %d", "UNLOCKED", key, id)
			} else {
				conn.Write(notUnlockedResponse)

				log.Printf("Request:  %-12s | Key:  %-15s | Id: %d", cmd, key, id)
				log.Printf("Response: %-12s | Key:  %-15s | Id: %d", "NOT_UNLOCKED", key, id)
			}

		default:
			conn.Write(errUnknownCommand)
			continue
		}
	}
}
<commit_msg>Timeout logging<commit_after>package main

import (
	"bufio"
	"flag"
	"fmt"
	"log"
	"net"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

type timeoutLock struct {
	mutex sync.Mutex
	id    int64 // unique ID of the current lock. Only allow an unlock if the correct id is passed
}

var locksLock sync.RWMutex
var locks = map[string]*timeoutLock{}

func main() {
	var port int
	flag.IntVar(&port, "p", 45625, "port")
	listener, err := net.Listen("tcp", ":"+strconv.Itoa(port))
	if err != nil {
		log.Fatalln("error listening", err)
	}

	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Println("error accepting", err)
			return
		}
		go handleConn(conn)
	}
}

var (
	unlockedResponse    = []byte("UNLOCKED\r\n")
	notUnlockedResponse = []byte("NOT_UNLOCKED\r\n")

	errBadFormat      = []byte("ERROR bad command format\r\n")
	errUnknownCommand = []byte("ERROR unknown command\r\n")
	errLockNotFound   = []byte("ERROR lock not found\r\n")
)

func handleConn(conn net.Conn) {
	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		split := strings.Fields(scanner.Text())
		if len(split) < 3 {
			conn.Write(errBadFormat)
			continue
		}
		cmd := split[0]
		key := split[1]
		switch cmd {
		// LOCK <key> <timeout>
		case "LOCK":
			timeout, err := strconv.Atoi(split[2])

			if err != nil {
				conn.Write(errBadFormat)
				continue
			}
			locksLock.RLock()
			lock, ok := locks[key]
			locksLock.RUnlock()
			if !ok {
				// lock doesn't exist; create it
				locksLock.Lock()
				lock, ok = locks[key]
				if !ok {
					lock = &timeoutLock{}
					locks[key] = lock
				}
				locksLock.Unlock()
			}

			lock.mutex.Lock()
			id := atomic.AddInt64(&lock.id, 1)
			time.AfterFunc(time.Duration(timeout)*time.Millisecond, func() {
				if atomic.CompareAndSwapInt64(&lock.id, id, id+1) {
					lock.mutex.Unlock()
				}
				log.Printf("Timedout: %-12d | Key:  %-15s | Id: %d", timeout, key, id)
			})
			fmt.Fprintf(conn, "LOCKED %v\r\n", id)

			log.Printf("Request:  %-12s | Key:  %-15s | Timeout: %dms", cmd, key, timeout)
			log.Printf("Response: %-12s | Key:  %-15s | Id: %d", "LOCKED", key, id)

		// UNLOCK <key> <id>
		case "UNLOCK":
			id, err := strconv.ParseInt(split[2], 10, 64)

			if err != nil {
				conn.Write(errBadFormat)
				continue
			}
			locksLock.RLock()
			lock, ok := locks[key]
			locksLock.RUnlock()
			if !ok {
				conn.Write(errLockNotFound)

				log.Printf("Request:  %-12s | Key:  %-15s | Id: %d", cmd, key, id)
				log.Printf("Response: %-12s | Key:  %-15s", "404", key)
				continue
			}
			if atomic.CompareAndSwapInt64(&lock.id, id, id+1) {
				lock.mutex.Unlock()
				conn.Write(unlockedResponse)

				log.Printf("Request:  %-12s | Key:  %-15s | Id: %d", cmd, key, id)
				log.Printf("Response: %-12s | Key:  %-15s | Id: %d", "UNLOCKED", key, id)
			} else {
				conn.Write(notUnlockedResponse)

				log.Printf("Request:  %-12s | Key:  %-15s | Id: %d", cmd, key, id)
				log.Printf("Response: %-12s | Key:  %-15s | Id: %d", "NOT_UNLOCKED", key, id)
			}

		default:
			conn.Write(errUnknownCommand)
			continue
		}
	}
}
<|endoftext|>