<commit_before>package auth

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/pkg/browser"
	"golang.org/x/oauth2"

	"github.com/datawire/dlib/dcontext"
	"github.com/datawire/dlib/dgroup"
	"github.com/datawire/dlib/dhttp"
	"github.com/datawire/dlib/dlog"
	"github.com/telepresenceio/telepresence/rpc/v2/connector"
	"github.com/telepresenceio/telepresence/v2/pkg/client"
	"github.com/telepresenceio/telepresence/v2/pkg/client/connector/auth/authdata"
	"github.com/telepresenceio/telepresence/v2/pkg/client/connector/internal/scout"
)

const (
	callbackPath = "/callback"
)

var ErrNotLoggedIn = errors.New("not logged in")

type oauth2Callback struct {
	Code             string
	Error            string
	ErrorDescription string
}

type loginExecutor struct {
	// static

	env              client.Env
	SaveTokenFunc    func(context.Context, *oauth2.Token) error
	SaveUserInfoFunc func(context.Context, *authdata.UserInfo) error
	OpenURLFunc      func(string) error
	stdout           io.Writer
	scout            chan<- scout.ScoutReport

	// stateful

	oauth2ConfigMu sync.RWMutex // locked unless a .Worker is running
	oauth2Config   oauth2.Config

	loginMu               sync.Mutex
	callbacks             chan oauth2Callback
	tokenSource           oauth2.TokenSource
	userInfo              *authdata.UserInfo
	refreshTimer          *time.Timer
	refreshTimerIsStopped bool
}

// LoginExecutor controls the execution of a login flow
type LoginExecutor interface {
	Worker(ctx context.Context) error
	Login(ctx context.Context) error
	Logout(ctx context.Context) error
	GetToken(ctx context.Context) (string, error)
	GetUserInfo(ctx context.Context) (*authdata.UserInfo, error)
}

// NewLoginExecutor returns an instance of LoginExecutor
func NewLoginExecutor(
	env client.Env,
	saveTokenFunc func(context.Context, *oauth2.Token) error,
	saveUserInfoFunc func(context.Context, *authdata.UserInfo) error,
	openURLFunc func(string) error,
	stdout io.Writer,
	scout chan<- scout.ScoutReport,
) LoginExecutor {
	ret := &loginExecutor{
		env:              env,
		SaveTokenFunc:    saveTokenFunc,
		SaveUserInfoFunc: saveUserInfoFunc,
		OpenURLFunc:      openURLFunc,
		stdout:           stdout,
		scout:            scout,

		callbacks:    make(chan oauth2Callback),
		refreshTimer: time.NewTimer(1 * time.Minute),
	}
	ret.oauth2ConfigMu.Lock()
	ret.loginMu.Lock()
	ret.resetRefreshTimer(0)
	return ret
}

// EnsureLoggedIn will check if the user is logged in and if not initiate the login flow.
func EnsureLoggedIn(ctx context.Context, executor LoginExecutor) (connector.LoginResult_Code, error) {
	if token, _ := authdata.LoadTokenFromUserCache(ctx); token != nil {
		return connector.LoginResult_OLD_LOGIN_REUSED, nil
	}

	if err := executor.Login(ctx); err != nil {
		return connector.LoginResult_UNSPECIFIED, err
	}

	return connector.LoginResult_NEW_LOGIN_SUCCEEDED, nil
}

func NewStandardLoginExecutor(env client.Env, stdout io.Writer, scout chan<- scout.ScoutReport) LoginExecutor {
	return NewLoginExecutor(
		env,
		authdata.SaveTokenToUserCache,
		authdata.SaveUserInfoToUserCache,
		browser.OpenURL,
		stdout,
		scout,
	)
}

func (l *loginExecutor) tokenCB(ctx context.Context, tokenInfo *oauth2.Token) error {
	if err := l.SaveTokenFunc(ctx, tokenInfo); err != nil {
		return fmt.Errorf("could not save access token to user cache: %w", err)
	}
	l.resetRefreshTimer(time.Until(tokenInfo.Expiry))
	return nil
}

func (l *loginExecutor) resetRefreshTimer(delta time.Duration) {
	if !l.refreshTimerIsStopped {
		if !l.refreshTimer.Stop() {
			<-l.refreshTimer.C
		}
		l.refreshTimerIsStopped = true
	}
	if delta > 0 {
		l.refreshTimer.Reset(delta)
	}
}

func (l *loginExecutor) Worker(ctx context.Context) error {
	listener, err := net.Listen("tcp", ":0")
	if err != nil {
		return err
	}
	l.oauth2Config = oauth2.Config{
		ClientID:    l.env.LoginClientID,
		RedirectURL: fmt.Sprintf("http://localhost:%d%s", listener.Addr().(*net.TCPAddr).Port, callbackPath),
		Endpoint: oauth2.Endpoint{
			AuthURL:  l.env.LoginAuthURL,
			TokenURL: l.env.LoginTokenURL,
		},
		Scopes: []string{"openid", "profile", "email"},
	}

	l.tokenSource, err = func() (oauth2.TokenSource, error) {
		l.resetRefreshTimer(0)
		tokenInfo, err := authdata.LoadTokenFromUserCache(ctx)
		if err != nil || tokenInfo == nil {
			return nil, err
		}
		l.resetRefreshTimer(time.Until(tokenInfo.Expiry))
		return newTokenSource(ctx, l.oauth2Config, tokenInfo, l.tokenCB), nil
	}()
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	defer l.resetRefreshTimer(0)

	l.userInfo, err = authdata.LoadUserInfoFromUserCache(ctx)
	if err != nil && !os.IsNotExist(err) {
		return err
	}

	l.oauth2ConfigMu.Unlock()
	defer l.oauth2ConfigMu.Lock()
	l.loginMu.Unlock()
	defer l.loginMu.Lock()

	grp := dgroup.NewGroup(ctx, dgroup.GroupConfig{
		EnableWithSoftness: ctx == dcontext.HardContext(ctx),
		ShutdownOnNonError: true,
	})

	grp.Go("server-http", func(ctx context.Context) error {
		sc := dhttp.ServerConfig{
			Handler: http.HandlerFunc(l.httpHandler),
		}
		return sc.Serve(ctx, listener)
	})
	grp.Go("refresh", func(ctx context.Context) error {
		for {
			select {
			case <-l.refreshTimer.C:
				dlog.Infoln(ctx, "refreshing access token...")
				if token, err := l.GetToken(ctx); err != nil {
					dlog.Infof(ctx, "could not refresh assess token: %v", err)
				} else if token != "" {
					dlog.Infof(ctx, "got new access token")
				}
			case <-ctx.Done():
				return nil
			}
		}
	})
	return grp.Wait()
}

// Login tries logging the user by opening a browser window and authenticating against the
// configured (in `l.env`) OAuth2 authorization server using the authorization-code flow.  This
// relies on the .Worker() HTTP server being already running in the background, as it is needed to
// serve the redirection endpoint (called the "callback URL" in this code).  Once the callback URL
// is invoked, this function will receive notification of that on the l.callbacks channel, and will
// invoke the authorization server's token endpoint to get the user's access & refresh tokens and
// persist them with the l.SaveTokenFunc (which would usually write to user cache).  If login
// succeeds, the this function will then try invoking the authorization server's userinfo endpoint
// and persisting it using l.SaveUserInfoFunc (which would usually write to user cache).
func (l *loginExecutor) Login(ctx context.Context) (err error) {
	// Whatever the result is, report it to the terminal and report it to Metriton.
	var token *oauth2.Token
	defer func() {
		switch {
		case err != nil && err != ctx.Err():
			fmt.Fprintln(l.stdout, "Login failure.")
			l.scout <- scout.ScoutReport{
				Action: "login_failure",
				Metadata: map[string]interface{}{
					"error": err.Error(),
				},
			}
		case err != nil && err == ctx.Err():
			fmt.Fprintln(l.stdout, "Login aborted.")
			l.scout <- scout.ScoutReport{
				Action: "login_interrupted",
			}
		default:
			fmt.Fprintln(l.stdout, "Login successful.")
			_ = l.retrieveUserInfo(ctx, token)
			l.scout <- scout.ScoutReport{
				Action: "login_success",
			}
		}
	}()

	// We'll be making use of l.auth2config
	l.oauth2ConfigMu.RLock()
	defer l.oauth2ConfigMu.RUnlock()

	// Only one login action at a time
	l.loginMu.Lock()
	defer l.loginMu.Unlock()

	// create OAuth2 authentication code flow URL
	state := uuid.New().String()
	pkceVerifier, err := NewCodeVerifier()
	if err != nil {
		return err
	}
	url := l.oauth2Config.AuthCodeURL(
		state,
		oauth2.SetAuthURLParam("code_challenge", pkceVerifier.CodeChallengeS256()),
		oauth2.SetAuthURLParam("code_challenge_method", PKCEChallengeMethodS256),
	)

	fmt.Fprintln(l.stdout, "Launching browser authentication flow...")
	if err := l.OpenURLFunc(url); err != nil {
		fmt.Fprintf(l.stdout, "Could not open browser, please access this URL: %v\n", url)
	}

	// wait for callback completion or interruption
	select {
	case callback, ok := <-l.callbacks:
		if !ok {
			return errors.New("connector shutting down")
		}
		if callback.Error != "" {
			return fmt.Errorf("%v error returned on OAuth2 callback: %v", callback.Error, callback.ErrorDescription)
		}

		// retrieve access token from callback code
		token, err = l.oauth2Config.Exchange(
			ctx,
			callback.Code,
			oauth2.SetAuthURLParam("code_verifier", pkceVerifier.String()),
		)
		if err != nil {
			return fmt.Errorf("error while exchanging code for token: %w", err)
		}

		l.tokenSource = newTokenSource(ctx, l.oauth2Config, token, l.tokenCB)
		if err := l.tokenCB(ctx, token); err != nil {
			return err
		}

		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func (l *loginExecutor) Logout(ctx context.Context) error {
	l.loginMu.Lock()
	defer l.loginMu.Unlock()

	if l.tokenSource == nil {
		return ErrNotLoggedIn
	}
	l.resetRefreshTimer(0)
	l.tokenSource = nil
	l.userInfo = nil
	_ = authdata.DeleteTokenFromUserCache(ctx)
	_ = authdata.DeleteUserInfoFromUserCache(ctx)

	return nil
}

func (l *loginExecutor) GetToken(ctx context.Context) (string, error) {
	l.loginMu.Lock()
	defer l.loginMu.Unlock()

	if l.tokenSource == nil {
		return "", ErrNotLoggedIn
	} else if tokenInfo, err := l.tokenSource.Token(); err != nil {
		return "", err
	} else {
		return tokenInfo.AccessToken, nil
	}
}

func (l *loginExecutor) GetUserInfo(ctx context.Context) (*authdata.UserInfo, error) {
	l.loginMu.Lock()
	defer l.loginMu.Unlock()

	if l.userInfo == nil {
		return nil, ErrNotLoggedIn
	}
	return l.userInfo, nil
}

func (l *loginExecutor) retrieveUserInfo(ctx context.Context, token *oauth2.Token) error {
	var userInfo authdata.UserInfo
	req, err := http.NewRequest("GET", l.env.UserInfoURL, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status %v from user info endpoint", resp.StatusCode)
	}
	content, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	err = json.Unmarshal(content, &userInfo)
	if err != nil {
		return err
	}
	l.userInfo = &userInfo
	return l.SaveUserInfoFunc(ctx, &userInfo)
}

func (l *loginExecutor) httpHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != callbackPath {
		http.NotFound(w, r)
		return
	}
	query := r.URL.Query()
	code := query.Get("code")
	errorName := query.Get("error")
	errorDescription := query.Get("error_description")

	var sb strings.Builder
	sb.WriteString("<!DOCTYPE html><title>Authentication Successful</title>")
	if errorName == "" && code != "" {
		w.Header().Set("Location", l.env.LoginCompletionURL)
		w.WriteHeader(http.StatusTemporaryRedirect)
		sb.WriteString("
Authentication Successful
")
		sb.WriteString("
You can now close this tab and resume on the CLI.
")
	} else {
		sb.WriteString("
Authentication Error
")
		sb.WriteString(fmt.Sprintf("
%s: %s
", errorName, errorDescription))
		w.WriteHeader(http.StatusInternalServerError)
	}
	sb.WriteString("")

	w.Header().Set("Content-Type", "text/html")
	if _, err := io.WriteString(w, sb.String()); err != nil {
		dlog.Errorf(r.Context(), "Error writing callback response body: %v", err)
	}

	resp := oauth2Callback{
		Code:             code,
		Error:            errorName,
		ErrorDescription: errorDescription,
	}
	// Only send the resp if there's still a listener waiting for it.  The user might have hit
	// Ctrl-C and hung up!
	select {
	case l.callbacks <- resp:
	default:
	}
}
<commit_msg>pkg/client/connector/auth: Add comments, fix a race condition noticed when writing the comments<commit_after>package auth

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/pkg/browser"
	"golang.org/x/oauth2"

	"github.com/datawire/dlib/dcontext"
	"github.com/datawire/dlib/dgroup"
	"github.com/datawire/dlib/dhttp"
	"github.com/datawire/dlib/dlog"
	"github.com/telepresenceio/telepresence/rpc/v2/connector"
	"github.com/telepresenceio/telepresence/v2/pkg/client"
	"github.com/telepresenceio/telepresence/v2/pkg/client/connector/auth/authdata"
	"github.com/telepresenceio/telepresence/v2/pkg/client/connector/internal/scout"
)

const (
	callbackPath = "/callback"
)

var ErrNotLoggedIn = errors.New("not logged in")

type oauth2Callback struct {
	Code             string
	Error            string
	ErrorDescription string
}

type loginExecutor struct {
	// static

	env              client.Env
	SaveTokenFunc    func(context.Context, *oauth2.Token) error
	SaveUserInfoFunc func(context.Context, *authdata.UserInfo) error
	OpenURLFunc      func(string) error
	stdout           io.Writer
	scout            chan<- scout.ScoutReport

	// stateful

	oauth2ConfigMu sync.RWMutex // locked unless a .Worker is running
	oauth2Config   oauth2.Config

	loginMu               sync.Mutex
	callbacks             chan oauth2Callback
	tokenSource           oauth2.TokenSource
	userInfo              *authdata.UserInfo
	refreshTimer          *time.Timer
	refreshTimerIsStopped bool
	refreshTimerReset     chan time.Duration
}

// LoginExecutor controls the execution of a login flow
type LoginExecutor interface {
	Worker(ctx context.Context) error
	Login(ctx context.Context) error
	Logout(ctx context.Context) error
	GetToken(ctx context.Context) (string, error)
	GetUserInfo(ctx context.Context) (*authdata.UserInfo, error)
}

// NewLoginExecutor returns an instance of LoginExecutor
func NewLoginExecutor(
	env client.Env,
	saveTokenFunc func(context.Context, *oauth2.Token) error,
	saveUserInfoFunc func(context.Context, *authdata.UserInfo) error,
	openURLFunc func(string) error,
	stdout io.Writer,
	scout chan<- scout.ScoutReport,
) LoginExecutor {
	ret := &loginExecutor{
		env:              env,
		SaveTokenFunc:    saveTokenFunc,
		SaveUserInfoFunc: saveUserInfoFunc,
		OpenURLFunc:      openURLFunc,
		stdout:           stdout,
		scout:            scout,

		callbacks: make(chan oauth2Callback),
		// AFAICT, it's not possible to create a timer in a stopped state.  So we create it
		// in a running state with 1 minute left, and then immediately stop it below with
		// resetRefreshTimerUnlocked.
		refreshTimer:      time.NewTimer(1 * time.Minute),
		refreshTimerReset: make(chan time.Duration),
	}
	ret.oauth2ConfigMu.Lock()
	ret.loginMu.Lock()
	ret.resetRefreshTimerUnlocked(0)
	return ret
}

// EnsureLoggedIn will check if the user is logged in and if not initiate the login flow.
func EnsureLoggedIn(ctx context.Context, executor LoginExecutor) (connector.LoginResult_Code, error) {
	if token, _ := authdata.LoadTokenFromUserCache(ctx); token != nil {
		return connector.LoginResult_OLD_LOGIN_REUSED, nil
	}

	if err := executor.Login(ctx); err != nil {
		return connector.LoginResult_UNSPECIFIED, err
	}

	return connector.LoginResult_NEW_LOGIN_SUCCEEDED, nil
}

func NewStandardLoginExecutor(env client.Env, stdout io.Writer, scout chan<- scout.ScoutReport) LoginExecutor {
	return NewLoginExecutor(
		env,
		authdata.SaveTokenToUserCache,
		authdata.SaveUserInfoToUserCache,
		browser.OpenURL,
		stdout,
		scout,
	)
}

func (l *loginExecutor) tokenCB(ctx context.Context, tokenInfo *oauth2.Token) error {
	if err := l.SaveTokenFunc(ctx, tokenInfo); err != nil {
		return fmt.Errorf("could not save access token to user cache: %w", err)
	}
	l.resetRefreshTimer(time.Until(tokenInfo.Expiry))
	return nil
}

// resetRefreshTimer resets the timer to have `delta` time left on it.  If `delta` is <= 0, then it
// stops the timer.  It is safe to call resetRefreshTimer(0) on an already-stopped timer.  May only
// be called while the "refresh" goroutine is running.
func (l *loginExecutor) resetRefreshTimer(delta time.Duration) {
	// We pass this along to the "refresh" goroutine to call .Stop() and .Reset(), because the
	// time.Timer godoc tells us that this "cannot be done concurrent to other receives from the
	// Timer's channel or other calls to the Timer's Stop method."; and without doing it in the
	// refresh goroutine's main select loop, it'd be impossible to guard against concurrent
	// receives.
	l.refreshTimerReset <- delta
}

// resetRefreshTimerUnlocked is like resetRefreshTimer, but you need to be careful about not calling
// it concurrent to the "refresh" goroutine or other calls to resetRefreshTimerUnlocked (what that
// means at the moment: only call this from NewLoginExecutor() or .Worker()).
func (l *loginExecutor) resetRefreshTimerUnlocked(delta time.Duration) {
	// The timer must be stopped before we reset it.  We have to track l.refreshTimerIsStopped
	// because the <-l.refreshTimer.C receive will hang on subsequent calls (even though we're
	// checking the return value of .Stop()!).
	if !l.refreshTimerIsStopped {
		if !l.refreshTimer.Stop() {
			<-l.refreshTimer.C
		}
		l.refreshTimerIsStopped = true
	}
	// Reset the timer if delta > 0.  Leave it stopped if delta <= 0.
	if delta > 0 {
		l.refreshTimer.Reset(delta)
	}
}

func (l *loginExecutor) Worker(ctx context.Context) error {
	listener, err := net.Listen("tcp", ":0")
	if err != nil {
		return err
	}
	l.oauth2Config = oauth2.Config{
		ClientID:    l.env.LoginClientID,
		RedirectURL: fmt.Sprintf("http://localhost:%d%s", listener.Addr().(*net.TCPAddr).Port, callbackPath),
		Endpoint: oauth2.Endpoint{
			AuthURL:  l.env.LoginAuthURL,
			TokenURL: l.env.LoginTokenURL,
		},
		Scopes: []string{"openid", "profile", "email"},
	}

	l.tokenSource, err = func() (oauth2.TokenSource, error) {
		l.resetRefreshTimerUnlocked(0)
		tokenInfo, err := authdata.LoadTokenFromUserCache(ctx)
		if err != nil || tokenInfo == nil {
			return nil, err
		}
		l.resetRefreshTimerUnlocked(time.Until(tokenInfo.Expiry))
		return newTokenSource(ctx, l.oauth2Config, tokenInfo, l.tokenCB), nil
	}()
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	defer l.resetRefreshTimerUnlocked(0)

	l.userInfo, err = authdata.LoadUserInfoFromUserCache(ctx)
	if err != nil && !os.IsNotExist(err) {
		return err
	}

	l.oauth2ConfigMu.Unlock()
	defer l.oauth2ConfigMu.Lock()
	l.loginMu.Unlock()
	defer l.loginMu.Lock()

	grp := dgroup.NewGroup(ctx, dgroup.GroupConfig{
		EnableWithSoftness: ctx == dcontext.HardContext(ctx),
		ShutdownOnNonError: true,
	})

	grp.Go("server-http", func(ctx context.Context) error {
		sc := dhttp.ServerConfig{
			Handler: http.HandlerFunc(l.httpHandler),
		}
		return sc.Serve(ctx, listener)
	})
	grp.Go("refresh", func(ctx context.Context) error {
		for {
			select {
			case <-l.refreshTimer.C:
				dlog.Infoln(ctx, "refreshing access token...")
				if token, err := l.GetToken(ctx); err != nil {
					dlog.Infof(ctx, "could not refresh assess token: %v", err)
				} else if token != "" {
					dlog.Infof(ctx, "got new access token")
				}
			case delta := <-l.refreshTimerReset:
				l.resetRefreshTimerUnlocked(delta)
			case <-ctx.Done():
				return nil
			}
		}
	})
	return grp.Wait()
}

// Login tries logging the user by opening a browser window and authenticating against the
// configured (in `l.env`) OAuth2 authorization server using the authorization-code flow.  This
// relies on the .Worker() HTTP server being already running in the background, as it is needed to
// serve the redirection endpoint (called the "callback URL" in this code).  Once the callback URL
// is invoked, this function will receive notification of that on the l.callbacks channel, and will
// invoke the authorization server's token endpoint to get the user's access & refresh tokens and
// persist them with the l.SaveTokenFunc (which would usually write to user cache).  If login
// succeeds, the this function will then try invoking the authorization server's userinfo endpoint
// and persisting it using l.SaveUserInfoFunc (which would usually write to user cache).
func (l *loginExecutor) Login(ctx context.Context) (err error) {
	// Whatever the result is, report it to the terminal and report it to Metriton.
	var token *oauth2.Token
	defer func() {
		switch {
		case err != nil && err != ctx.Err():
			fmt.Fprintln(l.stdout, "Login failure.")
			l.scout <- scout.ScoutReport{
				Action: "login_failure",
				Metadata: map[string]interface{}{
					"error": err.Error(),
				},
			}
		case err != nil && err == ctx.Err():
			fmt.Fprintln(l.stdout, "Login aborted.")
			l.scout <- scout.ScoutReport{
				Action: "login_interrupted",
			}
		default:
			fmt.Fprintln(l.stdout, "Login successful.")
			_ = l.retrieveUserInfo(ctx, token)
			l.scout <- scout.ScoutReport{
				Action: "login_success",
			}
		}
	}()

	// We'll be making use of l.auth2config
	l.oauth2ConfigMu.RLock()
	defer l.oauth2ConfigMu.RUnlock()

	// Only one login action at a time
	l.loginMu.Lock()
	defer l.loginMu.Unlock()

	// create OAuth2 authentication code flow URL
	state := uuid.New().String()
	pkceVerifier, err := NewCodeVerifier()
	if err != nil {
		return err
	}
	url := l.oauth2Config.AuthCodeURL(
		state,
		oauth2.SetAuthURLParam("code_challenge", pkceVerifier.CodeChallengeS256()),
		oauth2.SetAuthURLParam("code_challenge_method", PKCEChallengeMethodS256),
	)

	fmt.Fprintln(l.stdout, "Launching browser authentication flow...")
	if err := l.OpenURLFunc(url); err != nil {
		fmt.Fprintf(l.stdout, "Could not open browser, please access this URL: %v\n", url)
	}

	// wait for callback completion or interruption
	select {
	case callback, ok := <-l.callbacks:
		if !ok {
			return errors.New("connector shutting down")
		}
		if callback.Error != "" {
			return fmt.Errorf("%v error returned on OAuth2 callback: %v", callback.Error, callback.ErrorDescription)
		}

		// retrieve access token from callback code
		token, err = l.oauth2Config.Exchange(
			ctx,
			callback.Code,
			oauth2.SetAuthURLParam("code_verifier", pkceVerifier.String()),
		)
		if err != nil {
			return fmt.Errorf("error while exchanging code for token: %w", err)
		}

		l.tokenSource = newTokenSource(ctx, l.oauth2Config, token, l.tokenCB)
		if err := l.tokenCB(ctx, token); err != nil {
			return err
		}

		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func (l *loginExecutor) Logout(ctx context.Context) error {
	l.loginMu.Lock()
	defer l.loginMu.Unlock()

	if l.tokenSource == nil {
		return ErrNotLoggedIn
	}
	l.resetRefreshTimer(0)
	l.tokenSource = nil
	l.userInfo = nil
	_ = authdata.DeleteTokenFromUserCache(ctx)
	_ = authdata.DeleteUserInfoFromUserCache(ctx)

	return nil
}

func (l *loginExecutor) GetToken(ctx context.Context) (string, error) {
	l.loginMu.Lock()
	defer l.loginMu.Unlock()

	if l.tokenSource == nil {
		return "", ErrNotLoggedIn
	} else if tokenInfo, err := l.tokenSource.Token(); err != nil {
		return "", err
	} else {
		return tokenInfo.AccessToken, nil
	}
}

func (l *loginExecutor) GetUserInfo(ctx context.Context) (*authdata.UserInfo, error) {
	l.loginMu.Lock()
	defer l.loginMu.Unlock()

	if l.userInfo == nil {
		return nil, ErrNotLoggedIn
	}
	return l.userInfo, nil
}

func (l *loginExecutor) retrieveUserInfo(ctx context.Context, token *oauth2.Token) error {
	var userInfo authdata.UserInfo
	req, err := http.NewRequest("GET", l.env.UserInfoURL, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status %v from user info endpoint", resp.StatusCode)
	}
	content, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	err = json.Unmarshal(content, &userInfo)
	if err != nil {
		return err
	}
	l.userInfo = &userInfo
	return l.SaveUserInfoFunc(ctx, &userInfo)
}

func (l *loginExecutor) httpHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != callbackPath {
		http.NotFound(w, r)
		return
	}
	query := r.URL.Query()
	code := query.Get("code")
	errorName := query.Get("error")
	errorDescription := query.Get("error_description")

	var sb strings.Builder
	sb.WriteString("<!DOCTYPE html><title>Authentication Successful</title>")
	if errorName == "" && code != "" {
		w.Header().Set("Location", l.env.LoginCompletionURL)
		w.WriteHeader(http.StatusTemporaryRedirect)
		sb.WriteString("
Authentication Successful
")
		sb.WriteString("
You can now close this tab and resume on the CLI.
")
	} else {
		sb.WriteString("
Authentication Error
")
		sb.WriteString(fmt.Sprintf("
%s: %s
", errorName, errorDescription))
		w.WriteHeader(http.StatusInternalServerError)
	}
	sb.WriteString("")

	w.Header().Set("Content-Type", "text/html")
	if _, err := io.WriteString(w, sb.String()); err != nil {
		dlog.Errorf(r.Context(), "Error writing callback response body: %v", err)
	}

	resp := oauth2Callback{
		Code:             code,
		Error:            errorName,
		ErrorDescription: errorDescription,
	}
	// Only send the resp if there's still a listener waiting for it.  The user might have hit
	// Ctrl-C and hung up!
	select {
	case l.callbacks <- resp:
	default:
	}
}
<|endoftext|>


<commit_before>package controller

import (
	"fmt"
	"github.com/golang/glog"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/tools/cache"
	api "managed-certs-gke/pkg/apis/cloud.google.com/v1alpha1"
	"time"
)

func (c *McertController) Run(stopChannel <-chan struct{}, errors chan<- error) {
	defer c.queue.ShutDown()

	err := c.initializeState()
	if err != nil {
		errors <- fmt.Errorf("Cnuld not intialize state: %v", err)
		return
	}

	go wait.Until(c.runWorker, time.Second, stopChannel)
	go wait.Until(c.synchronizeAllMcerts, time.Minute, stopChannel)

	<-stopChannel
}

func (c *McertController) initializeState() error {
	mcerts, err := c.lister.List(labels.Everything())
	if err != nil {
		return err
	}

	for _, mcert := range mcerts {
		c.state.Put(mcert.ObjectMeta.Name, mcert.Status.CertificateName)
	}

	return nil
}

func (c *McertController) enqueue(obj interface{}) {
	if key, err := cache.MetaNamespaceKeyFunc(obj); err != nil {
		runtime.HandleError(err)
	} else {
		c.queue.AddRateLimited(key)
	}
}

func (c *McertController) getAllMcertsInCluster() (result map[string]*api.ManagedCertificate, err error) {
	mcerts, err := c.lister.List(labels.Everything())
	if err != nil {
		return nil, err
	}

	result = make(map[string]*api.ManagedCertificate, len(mcerts))
	for _, mcert := range mcerts {
		result[mcert.ObjectMeta.Name] = mcert
	}

	return
}

func (c *McertController) deleteObsoleteMcertsFromState(allMcertsInCluster map[string]*api.ManagedCertificate) {
	allKnownMcerts := c.state.GetAllManagedCertificates()
	for _, knownMcert := range allKnownMcerts {
		if _, exists := allMcertsInCluster[knownMcert]; !exists {
			// A managed certificate exists in state, but does not exist as a custom object in cluster, probably was deleted by the user - delete it from the state.
			c.state.Delete(knownMcert)
			glog.Infof("Deleted %s managed certificate from state, because such custom object does not exist in the cluster (any more?)", knownMcert)
		}
	}
}

func (c* McertController) deleteObsoleteSslCertificates() error {
	allKnownSslCerts := c.state.GetAllSslCertificates()
	allKnownSslCertsSet := make(map[string]bool, len(allKnownSslCerts))

	for _, knownSslCert := range allKnownSslCerts {
		allKnownSslCertsSet[knownSslCert] = true
	}

	sslCerts, err := c.sslClient.List()
	if err != nil {
		return err
	}

	for _, sslCert := range sslCerts.Items {
		if known, exists := allKnownSslCertsSet[sslCert.Name]; !exists || !known {
			c.sslClient.Delete(sslCert.Name)
			glog.Infof("Deleted %s SslCertificate resource, because there is no such ssl certificate in state", sslCert.Name)
		}
	}

	return nil
}

func (c *McertController) synchronizeAllMcerts() {
	allMcertsInCluster, err := c.getAllMcertsInCluster()
	if err != nil {
		runtime.HandleError(err)
		return
	}

	c.deleteObsoleteMcertsFromState(allMcertsInCluster)

	err = c.deleteObsoleteSslCertificates()
	if err != nil {
		runtime.HandleError(err)
		return
	}

	for _, mcert := range allMcertsInCluster {
		c.enqueue(mcert)
	}
}
<commit_msg>add log on initialization<commit_after>package controller

import (
	"fmt"
	"github.com/golang/glog"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/tools/cache"
	api "managed-certs-gke/pkg/apis/cloud.google.com/v1alpha1"
	"time"
)

func (c *McertController) Run(stopChannel <-chan struct{}, errors chan<- error) {
	defer c.queue.ShutDown()

	err := c.initializeState()
	if err != nil {
		errors <- fmt.Errorf("Cnuld not intialize state: %v", err)
		return
	}

	go wait.Until(c.runWorker, time.Second, stopChannel)
	go wait.Until(c.synchronizeAllMcerts, time.Minute, stopChannel)

	<-stopChannel
}

func (c *McertController) initializeState() error {
	mcerts, err := c.lister.List(labels.Everything())
	if err != nil {
		return err
	}

	for _, mcert := range mcerts {
		glog.Infof("Initializing state, map managed certificate %s to ssl certificate %s", mcert.ObjectMeta.Name, mcert.Status.CertificateName)
		c.state.Put(mcert.ObjectMeta.Name, mcert.Status.CertificateName)
	}

	return nil
}

func (c *McertController) enqueue(obj interface{}) {
	if key, err := cache.MetaNamespaceKeyFunc(obj); err != nil {
		runtime.HandleError(err)
	} else {
		c.queue.AddRateLimited(key)
	}
}

func (c *McertController) getAllMcertsInCluster() (result map[string]*api.ManagedCertificate, err error) {
	mcerts, err := c.lister.List(labels.Everything())
	if err != nil {
		return nil, err
	}

	result = make(map[string]*api.ManagedCertificate, len(mcerts))
	for _, mcert := range mcerts {
		result[mcert.ObjectMeta.Name] = mcert
	}

	return
}

func (c *McertController) deleteObsoleteMcertsFromState(allMcertsInCluster map[string]*api.ManagedCertificate) {
	allKnownMcerts := c.state.GetAllManagedCertificates()
	for _, knownMcert := range allKnownMcerts {
		if _, exists := allMcertsInCluster[knownMcert]; !exists {
			// A managed certificate exists in state, but does not exist as a custom object in cluster, probably was deleted by the user - delete it from the state.
			c.state.Delete(knownMcert)
			glog.Infof("Deleted %s managed certificate from state, because such custom object does not exist in the cluster (any more?)", knownMcert)
		}
	}
}

func (c* McertController) deleteObsoleteSslCertificates() error {
	allKnownSslCerts := c.state.GetAllSslCertificates()
	allKnownSslCertsSet := make(map[string]bool, len(allKnownSslCerts))

	for _, knownSslCert := range allKnownSslCerts {
		allKnownSslCertsSet[knownSslCert] = true
	}

	sslCerts, err := c.sslClient.List()
	if err != nil {
		return err
	}

	for _, sslCert := range sslCerts.Items {
		if known, exists := allKnownSslCertsSet[sslCert.Name]; !exists || !known {
			c.sslClient.Delete(sslCert.Name)
			glog.Infof("Deleted %s SslCertificate resource, because there is no such ssl certificate in state", sslCert.Name)
		}
	}

	return nil
}

func (c *McertController) synchronizeAllMcerts() {
	allMcertsInCluster, err := c.getAllMcertsInCluster()
	if err != nil {
		runtime.HandleError(err)
		return
	}

	c.deleteObsoleteMcertsFromState(allMcertsInCluster)

	err = c.deleteObsoleteSslCertificates()
	if err != nil {
		runtime.HandleError(err)
		return
	}

	for _, mcert := range allMcertsInCluster {
		c.enqueue(mcert)
	}
}
<|endoftext|>


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

package server

import (
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"fmt"
	"strings"
	"testing"

	"google.golang.org/grpc/credentials"
)

func TestListAuthChecker(t *testing.T) {
	testCases := []struct {
		name         string
		mode         AuthListMode
		authInfo     credentials.AuthInfo
		extractIDsFn func(exts []pkix.Extension) ([]string, error)
		err          string
		remove       bool // Remove the added entry
		set          bool // Use set to add the entry
	}{
		{
			name:     "nil",
			authInfo: nil,
			err:      "denying by default",
		},
		{
			name:     "non-tlsinfo",
			authInfo: &nonTLSInfo{},
			err:      "unable to extract TLS info",
		},
		{
			name:     "empty tlsinfo",
			mode:     AuthWhiteList,
			authInfo: credentials.TLSInfo{},
			err:      "no allowed identity found in peer's authentication info",
		},
		{
			name: "empty cert chain",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},

			err: "no allowed identity found in peer's authentication info",
		},
		{
			name: "error extracting ids",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return nil, fmt.Errorf("error extracting ids")
			},
			err: "no allowed identity found in peer's authentication info",
		},
		{
			name: "id mismatch",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{"bar"}, nil
			},
			err: "no allowed identity found in peer's authentication info",
		},
		{
			name: "success",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{"foo"}, nil
			},
		},
		{
			name: "removed",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{"foo"}, nil
			},
			remove: true,
			err:    "no allowed identity found in peer's authentication info",
		},
		{
			name: "blacklist allow",
			mode: AuthBlackList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{}, nil
			},
		},
		{
			name: "blacklist block",
			mode: AuthBlackList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{"foo"}, nil
			},
			err: "id is blacklisted: foo",
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {

			c := NewListAuthChecker()
			c.SetMode(testCase.mode)
			if testCase.extractIDsFn != nil {
				c.extractIDsFn = testCase.extractIDsFn
			}

			if testCase.set {
				c.Set("foo")
			} else {
				c.Add("foo")
			}

			if testCase.remove {
				c.Remove("foo")
			}

			err := c.Check(testCase.authInfo)
			if err != nil {
				if testCase.err == "" {
					t.Fatalf("Unexpected error: %v", err)
				} else if !strings.HasPrefix(err.Error(), testCase.err) {
					t.Fatalf("Error mismatch: got:%v, expected:%s", err, testCase.err)
				}
			} else if testCase.err != "" {
				t.Fatalf("Expected error not found: %s", testCase.err)
			}
		})
	}
}

type nonTLSInfo struct {
}

var _ credentials.AuthInfo = &nonTLSInfo{}

func (a *nonTLSInfo) AuthType() string {
	return "non-tls"
}

type authInfo struct {
	credentials.TLSInfo
}

var _ credentials.AuthInfo = &authInfo{}

func (a *authInfo) AuthType() string {
	return ""
}

func TestListAuthChecker_Allowed(t *testing.T) {
	cases := []struct {
		mode   AuthListMode
		id     string
		testid string
		expect bool
	}{
		{mode: AuthBlackList, testid: "foo", expect: true},
		{mode: AuthBlackList, id: "foo", testid: "foo", expect: false},
		{mode: AuthBlackList, id: "foo", testid: "bar", expect: true},
		{mode: AuthWhiteList, testid: "foo", expect: false},
		{mode: AuthWhiteList, id: "foo", testid: "foo", expect: true},
		{mode: AuthWhiteList, id: "foo", testid: "bar", expect: false},
	}

	for i, c := range cases {
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			checker := NewListAuthChecker()
			if c.id != "" {
				checker.Set(c.id)
			}
			checker.SetMode(c.mode)

			result := checker.Allowed(c.testid)
			if result != c.expect {
				t.Fatalf("Mismatch: Got:%v, Wanted:%v", result, c.expect)
			}
		})
	}
}

func TestListAuthChecker_String(t *testing.T) {
	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("Panic detected: %v", r)
		}
	}()

	c := NewListAuthChecker()
	c.SetMode(AuthBlackList)

	c.Set("1", "2", "3")

	// Make sure it doesn't crash
	_ = c.String()

	c.SetMode(AuthWhiteList)

	// Make sure it doesn't crash
	_ = c.String()
}
<commit_msg>increase code coverage of pkg/mcp/server/listchecker.go (#10579)<commit_after>//  Copyright 2018 Istio Authors
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

package server

import (
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"fmt"
	"strings"
	"testing"

	"google.golang.org/grpc/credentials"
)

func TestListAuthChecker(t *testing.T) {
	testCases := []struct {
		name         string
		mode         AuthListMode
		authInfo     credentials.AuthInfo
		extractIDsFn func(exts []pkix.Extension) ([]string, error)
		err          string
		remove       bool // Remove the added entry
		set          bool // Use set to add the entry
		ids          []string
		allowed      []string
	}{
		{
			name:     "nil",
			authInfo: nil,
			err:      "denying by default",
			ids:      []string{"foo"},
		},
		{
			name:     "non-tlsinfo",
			authInfo: &nonTLSInfo{},
			err:      "unable to extract TLS info",
			ids:      []string{"foo"},
		},
		{
			name:     "empty tlsinfo",
			mode:     AuthWhiteList,
			authInfo: credentials.TLSInfo{},
			err:      "no allowed identity found in peer's authentication info",
			ids:      []string{"foo"},
		},
		{
			name: "empty cert chain",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},

			err: "no allowed identity found in peer's authentication info",
			ids: []string{"foo"},
		},
		{
			name: "error extracting ids",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return nil, fmt.Errorf("error extracting ids")
			},
			err: "no allowed identity found in peer's authentication info",
			ids: []string{"foo"},
		},
		{
			name: "id mismatch",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{"bar"}, nil
			},
			err: "no allowed identity found in peer's authentication info",
			ids: []string{"foo"},
		},
		{
			name: "success",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{"foo"}, nil
			},
			ids:     []string{"foo"},
			allowed: []string{"foo"},
		},
		{
			name: "success with Set()",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{"foo", "bar"}, nil
			},
			ids:     []string{"foo", "bar"},
			allowed: []string{"foo", "bar"},
		},
		{
			name: "removed",
			mode: AuthWhiteList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{"foo"}, nil
			},
			remove: true,
			err:    "no allowed identity found in peer's authentication info",
			ids:    []string{"foo"},
		},
		{
			name: "blacklist allow",
			mode: AuthBlackList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{}, nil
			},
			allowed: []string{"foo", "bar", "baz"},
		},
		{
			name: "blacklist block",
			mode: AuthBlackList,
			authInfo: credentials.TLSInfo{
				State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{{}}}},
			},
			extractIDsFn: func(exts []pkix.Extension) ([]string, error) {
				return []string{"foo"}, nil
			},
			err:     "id is blacklisted: foo",
			ids:     []string{"foo"},
			allowed: []string{"bar", "baz"},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {

			c := NewListAuthChecker()
			c.SetMode(testCase.mode)
			if testCase.extractIDsFn != nil {
				c.extractIDsFn = testCase.extractIDsFn
			}

			switch len(testCase.ids) {
			case 1:
				c.Add(testCase.ids[0])
			default:
				c.Set(testCase.ids...)
			}

			if testCase.remove {
				c.Remove("foo")
			}

			err := c.Check(testCase.authInfo)
			if err != nil {
				if testCase.err == "" {
					t.Fatalf("Unexpected error: %v", err)
				} else if !strings.HasPrefix(err.Error(), testCase.err) {
					t.Fatalf("Error mismatch: got:%v, expected:%s", err, testCase.err)
				}
			} else if testCase.err != "" {
				t.Fatalf("Expected error not found: %s", testCase.err)
			}

			for _, id := range testCase.allowed {
				if testCase.mode == AuthWhiteList {
					if !c.Allowed(id) {
						t.Fatalf("Allowed(%v) failed", id)
					}
				}
				//} else {
				//	if c.Allowed(id) {
				//		t.Fatalf("Allowed(%v) failed", id)
				//	}
				//}

			}

			fmt.Println(c)
		})
	}
}

type nonTLSInfo struct {
}

var _ credentials.AuthInfo = &nonTLSInfo{}

func (a *nonTLSInfo) AuthType() string {
	return "non-tls"
}

type authInfo struct {
	credentials.TLSInfo
}

var _ credentials.AuthInfo = &authInfo{}

func (a *authInfo) AuthType() string {
	return ""
}

func TestListAuthChecker_Allowed(t *testing.T) {
	cases := []struct {
		mode   AuthListMode
		id     string
		testid string
		expect bool
	}{
		{mode: AuthBlackList, testid: "foo", expect: true},
		{mode: AuthBlackList, id: "foo", testid: "foo", expect: false},
		{mode: AuthBlackList, id: "foo", testid: "bar", expect: true},
		{mode: AuthWhiteList, testid: "foo", expect: false},
		{mode: AuthWhiteList, id: "foo", testid: "foo", expect: true},
		{mode: AuthWhiteList, id: "foo", testid: "bar", expect: false},
	}

	for i, c := range cases {
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			checker := NewListAuthChecker()
			if c.id != "" {
				checker.Set(c.id)
			}
			checker.SetMode(c.mode)

			result := checker.Allowed(c.testid)
			if result != c.expect {
				t.Fatalf("Mismatch: Got:%v, Wanted:%v", result, c.expect)
			}
		})
	}
}

func TestListAuthChecker_String(t *testing.T) {
	defer func() {
		if r := recover(); r != nil {
			t.Fatalf("Panic detected: %v", r)
		}
	}()

	c := NewListAuthChecker()
	c.SetMode(AuthBlackList)

	c.Set("1", "2", "3")

	// Make sure it doesn't crash
	_ = c.String()

	c.SetMode(AuthWhiteList)

	// Make sure it doesn't crash
	_ = c.String()
}
<|endoftext|>


<commit_before>/*
Copyright 2018 The Kubernetes Authors.

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

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

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

package globalflag

import (
	"flag"
	"fmt"
	"os"
	"strings"

	"github.com/spf13/pflag"

	"k8s.io/apiserver/pkg/util/logs"
)

// AddGlobalFlags explicitly registers flags that libraries (klog, verflag, etc.) register
// against the global flagsets from "flag" and "github.com/spf13/pflag".
// We do this in order to prevent unwanted flags from leaking into the component's flagset.
func AddGlobalFlags(fs *pflag.FlagSet, name string) {
	addGlogFlags(fs)
	logs.AddFlags(fs)

	fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", name))
}

// addGlogFlags explicitly registers flags that klog libraries(k8s.io/klog) register.
func addGlogFlags(fs *pflag.FlagSet) {
	// lookup flags in global flag set and re-register the values with our flagset
	global := flag.CommandLine
	local := pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)

	register(global, local, "logtostderr")
	register(global, local, "alsologtostderr")
	register(global, local, "v")
	register(global, local, "skip_headers")
	register(global, local, "stderrthreshold")
	register(global, local, "vmodule")
	register(global, local, "log_backtrace_at")
	register(global, local, "log_dir")
	register(global, local, "log_file")

	fs.AddFlagSet(local)
}

// normalize replaces underscores with hyphens
// we should always use hyphens instead of underscores when registering component flags
func normalize(s string) string {
	return strings.Replace(s, "_", "-", -1)
}

// register adds a flag to local that targets the Value associated with the Flag named globalName in global
func register(global *flag.FlagSet, local *pflag.FlagSet, globalName string) {
	if f := global.Lookup(globalName); f != nil {
		pflagFlag := pflag.PFlagFromGoFlag(f)
		pflagFlag.Name = normalize(pflagFlag.Name)
		local.AddFlag(pflagFlag)
	} else {
		panic(fmt.Sprintf("failed to find flag in global flagset (flag): %s", globalName))
	}
}
<commit_msg>*-controller-manager: fix missing global flags for --help<commit_after>/*
Copyright 2018 The Kubernetes Authors.

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

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

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

package globalflag

import (
	"flag"
	"fmt"
	"strings"

	"github.com/spf13/pflag"

	"k8s.io/apiserver/pkg/util/logs"
)

// AddGlobalFlags explicitly registers flags that libraries (klog, verflag, etc.) register
// against the global flagsets from "flag" and "k8s.io/klog".
// We do this in order to prevent unwanted flags from leaking into the component's flagset.
func AddGlobalFlags(fs *pflag.FlagSet, name string) {
	addGlogFlags(fs)
	logs.AddFlags(fs)

	fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", name))
}

// addGlogFlags explicitly registers flags that klog libraries(k8s.io/klog) register.
func addGlogFlags(fs *pflag.FlagSet) {
	// lookup flags of klog libraries in global flag set and re-register the values with our flagset
	Register(fs, "logtostderr")
	Register(fs, "alsologtostderr")
	Register(fs, "v")
	Register(fs, "skip_headers")
	Register(fs, "stderrthreshold")
	Register(fs, "vmodule")
	Register(fs, "log_backtrace_at")
	Register(fs, "log_dir")
	Register(fs, "log_file")
}

// normalize replaces underscores with hyphens
// we should always use hyphens instead of underscores when registering component flags
func normalize(s string) string {
	return strings.Replace(s, "_", "-", -1)
}

// Register adds a flag to local that targets the Value associated with the Flag named globalName in flag.CommandLine.
func Register(local *pflag.FlagSet, globalName string) {
	if f := flag.CommandLine.Lookup(globalName); f != nil {
		pflagFlag := pflag.PFlagFromGoFlag(f)
		pflagFlag.Name = normalize(pflagFlag.Name)
		local.AddFlag(pflagFlag)
	} else {
		panic(fmt.Sprintf("failed to find flag in global flagset (flag): %s", globalName))
	}
}
<|endoftext|>


<commit_before>package device

import (
	"github.com/lxc/lxd/lxd/device/config"
)

// InstanceIdentifier is an interface that allows us to identify an Instance and its properties.
// It is intended that this interface be entirely comprised of functions which cannot be blocking
// independent of when they're called in the instance lifecycle.
type InstanceIdentifier interface {
	Name() string
	Type() string
	Project() string
	DevicesPath() string
	ExpandedConfig() map[string]string
	ExpandedDevices() config.Devices
}
<commit_msg>device/instance/id: Adds LogPath() to instance identifier interface<commit_after>package device

import (
	"github.com/lxc/lxd/lxd/device/config"
)

// InstanceIdentifier is an interface that allows us to identify an Instance and its properties.
// It is intended that this interface be entirely comprised of functions which cannot be blocking
// independent of when they're called in the instance lifecycle.
type InstanceIdentifier interface {
	Name() string
	Type() string
	Project() string
	DevicesPath() string
	LogPath() string
	ExpandedConfig() map[string]string
	ExpandedDevices() config.Devices
}
<|endoftext|>


<commit_before>//go:build linux && cgo
// +build linux,cgo

package drivers

import (
	"fmt"
	"os"
	"unsafe"

	"github.com/pkg/errors"

	// Used by cgo
	_ "github.com/lxc/lxd/lxd/include"
)

/*
#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif
#define _FILE_OFFSET_BITS 64
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <linux/loop.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "../../include/macro.h"
#include "../../include/memory_utils.h"

#define LXD_MAXPATH 4096
#define LXD_NUMSTRLEN64 21
#define LXD_MAX_LOOP_PATHLEN (2 * sizeof("loop/")) + LXD_NUMSTRLEN64 + sizeof("backing_file") + 1

// If a loop file is already associated with a loop device, find it.
// This looks at "/sys/block" to avoid having to parse all of "/dev". Also, this
// allows to retrieve the full name of the backing file even if
// strlen(backing file) > LO_NAME_SIZE.
static int find_associated_loop_device(const char *loop_file,
				       char *loop_dev_name)
{
	__do_closedir DIR *dir = NULL;
	char looppath[LXD_MAX_LOOP_PATHLEN];
	char buf[LXD_MAXPATH];
	struct dirent *dp;

	dir = opendir("/sys/block");
	if (!dir)
		return -1;

	while ((dp = readdir(dir))) {
		__do_close int loop_path_fd = -EBADF;
		int ret;
		size_t totlen;
		struct stat fstatbuf;
		int dfd = -1;

		if (!dp)
			break;

		if (strncmp(dp->d_name, "loop", 4))
			continue;

		dfd = dirfd(dir);
		if (dfd < 0)
			continue;

		ret = snprintf(looppath, sizeof(looppath), "%s/loop/backing_file", dp->d_name);
		if (ret < 0 || (size_t)ret >= sizeof(looppath))
			continue;

		ret = fstatat(dfd, looppath, &fstatbuf, 0);
		if (ret < 0)
			continue;

		loop_path_fd = openat(dfd, looppath, O_RDONLY | O_CLOEXEC, 0);
		if (ret < 0)
			continue;

		// Clear buffer.
		memset(buf, 0, sizeof(buf));
		ret = read(loop_path_fd, buf, sizeof(buf));
		if (ret < 0)
			continue;

		totlen = strlen(buf);

		// Trim newlines.
		while ((totlen > 0) && (buf[totlen - 1] == '\n'))
			buf[--totlen] = '\0';

		if (strcmp(buf, loop_file))
			continue;

		// Create path to loop device.
		ret = snprintf(loop_dev_name, LO_NAME_SIZE, "/dev/%s",
			       dp->d_name);
		if (ret < 0 || ret >= LO_NAME_SIZE)
			continue;

		// Open fd to loop device.
		return open(loop_dev_name, O_RDWR | O_CLOEXEC);
	}

	return -1;
}

static int get_unused_loop_dev_legacy(char *loop_name)
{
	__do_closedir DIR *dir = NULL;
	struct dirent *dp;
	struct loop_info64 lo64;

	dir = opendir("/dev");
	if (!dir)
		return -1;

	while ((dp = readdir(dir))) {
		__do_close int dfd = -EBADF, fd = -EBADF;
		int ret;

		if (!dp)
			break;

		if (strncmp(dp->d_name, "loop", 4) != 0)
			continue;

		dfd = dirfd(dir);
		if (dfd < 0)
			continue;

		fd = openat(dfd, dp->d_name, O_RDWR);
		if (fd < 0)
			continue;

		ret = ioctl(fd, LOOP_GET_STATUS64, &lo64);
		if (ret < 0)
			if (ioctl(fd, LOOP_GET_STATUS64, &lo64) == 0 || errno != ENXIO)
				continue;

		ret = snprintf(loop_name, LO_NAME_SIZE, "/dev/%s", dp->d_name);
		if (ret < 0 || ret >= LO_NAME_SIZE)
			continue;

		return move_fd(fd);
	}

	return -1;
}

static int get_unused_loop_dev(char *name_loop)
{
	__do_close int fd_ctl = -EBADF;
	int loop_nr, ret;

	fd_ctl = open("/dev/loop-control", O_RDWR | O_CLOEXEC);
	if (fd_ctl < 0)
		return -ENODEV;

	loop_nr = ioctl(fd_ctl, LOOP_CTL_GET_FREE);
	if (loop_nr < 0)
		return -1;

	ret = snprintf(name_loop, LO_NAME_SIZE, "/dev/loop%d", loop_nr);
	if (ret < 0 || ret >= LO_NAME_SIZE)
		return -1;

	return open(name_loop, O_RDWR | O_CLOEXEC);
}

static int prepare_loop_dev(const char *source, char *loop_dev, int flags)
{
	__do_close int fd_img = -EBADF, fd_loop = -EBADF;
	int ret;
	struct loop_info64 lo64;

	fd_loop = get_unused_loop_dev(loop_dev);
	if (fd_loop < 0) {
		if (fd_loop == -ENODEV)
			fd_loop = get_unused_loop_dev_legacy(loop_dev);
		else
			return -1;
	}

	fd_img = open(source, O_RDWR | O_CLOEXEC);
	if (fd_img < 0)
		return -1;

	ret = ioctl(fd_loop, LOOP_SET_FD, fd_img);
	if (ret < 0)
		return -1;

	memset(&lo64, 0, sizeof(lo64));
	lo64.lo_flags = flags;

	ret = ioctl(fd_loop, LOOP_SET_STATUS64, &lo64);
	if (ret < 0)
		return -1;

	return move_fd(fd_loop);
}

static inline int prepare_loop_dev_retry(const char *source, char *loop_dev, int flags)
{
	int ret;
	unsigned int idx = 0;

	do {
		ret = prepare_loop_dev(source, loop_dev, flags);
		idx++;
	} while (ret < 0 && errno == EBUSY && idx < 30);

	return ret;
}

// Note that this does not guarantee to clear the loop device in time so that
// find_associated_loop_device() will not report that there still is a
// configured device (udev and so on...). So don't call
// find_associated_loop_device() after having called
// set_autoclear_loop_device().
int set_autoclear_loop_device(int fd_loop)
{
	struct loop_info64 lo64;

	memset(&lo64, 0, sizeof(lo64));
	lo64.lo_flags = LO_FLAGS_AUTOCLEAR;
	errno = 0;
	return ioctl(fd_loop, LOOP_SET_STATUS64, &lo64);
}

// Directly release the loop device
int free_loop_device(int fd_loop)
{
	return ioctl(fd_loop, LOOP_CLR_FD);
}

// Unset the LO_FLAGS_AUTOCLEAR flag on the given loop device file descriptor.
int unset_autoclear_loop_device(int fd_loop)
{
	int ret;
	struct loop_info64 lo64;

	errno = 0;
	ret = ioctl(fd_loop, LOOP_GET_STATUS64, &lo64);
	if (ret < 0)
		return -1;

	if ((lo64.lo_flags & LO_FLAGS_AUTOCLEAR) == 0)
		return 0;

	lo64.lo_flags &= ~LO_FLAGS_AUTOCLEAR;
	errno = 0;
	return ioctl(fd_loop, LOOP_SET_STATUS64, &lo64);
}
*/
import "C"

// LoFlagsAutoclear determines whether the loop device will autodestruct on last
// close.
const LoFlagsAutoclear int = C.LO_FLAGS_AUTOCLEAR

// PrepareLoopDev detects and sets up a loop device for source. It returns an
// open file descriptor to the free loop device and the path of the free loop
// device. It's the callers responsibility to close the open file descriptor.
func PrepareLoopDev(source string, flags int) (*os.File, error) {
	cLoopDev := C.malloc(C.size_t(C.LO_NAME_SIZE))
	if cLoopDev == nil {
		return nil, fmt.Errorf("Failed to allocate memory in C")
	}
	defer C.free(cLoopDev)

	cSource := C.CString(source)
	defer C.free(unsafe.Pointer(cSource))
	loopFd, _ := C.find_associated_loop_device(cSource, (*C.char)(cLoopDev))
	if loopFd >= 0 {
		return os.NewFile(uintptr(loopFd), C.GoString((*C.char)(cLoopDev))), nil
	}

	loopFd, err := C.prepare_loop_dev_retry(cSource, (*C.char)(cLoopDev), C.int(flags))
	if loopFd < 0 {
		if err != nil {
			return nil, errors.Wrapf(err, "Failed to prepare loop device for %q", source)
		}

		return nil, fmt.Errorf("Failed to prepare loop device for %q", source)
	}

	return os.NewFile(uintptr(loopFd), C.GoString((*C.char)(cLoopDev))), nil
}

// releaseLoopDev releases the loop dev assigned to the provided file.
func releaseLoopDev(source string) error {
	cLoopDev := C.malloc(C.size_t(C.LO_NAME_SIZE))
	if cLoopDev == nil {
		return fmt.Errorf("Failed to allocate memory in C")
	}
	defer C.free(cLoopDev)

	cSource := C.CString(source)
	loopFd, err := C.find_associated_loop_device(cSource, (*C.char)(cLoopDev))
	if err != nil {
		return err
	}

	// Prepare a Go file and defer close on the loop device.
	fd := os.NewFile(uintptr(loopFd), C.GoString((*C.char)(cLoopDev)))
	defer fd.Close()

	if loopFd >= 0 {
		_, err := C.free_loop_device(C.int(loopFd))
		if err != nil {
			return err
		}
	}

	return nil
}

// SetAutoclearOnLoopDev enables autodestruction of the provided loopback device.
func SetAutoclearOnLoopDev(loopFd int) error {
	ret, err := C.set_autoclear_loop_device(C.int(loopFd))
	if ret < 0 {
		if err != nil {
			return err
		}
		return fmt.Errorf("Failed to set LO_FLAGS_AUTOCLEAR")
	}

	return nil
}

// UnsetAutoclearOnLoopDev disables autodestruction of the provided loopback device.
func UnsetAutoclearOnLoopDev(loopFd int) error {
	ret, err := C.unset_autoclear_loop_device(C.int(loopFd))
	if ret < 0 {
		if err != nil {
			return err
		}
		return fmt.Errorf("Failed to unset LO_FLAGS_AUTOCLEAR")
	}

	return nil
}
<commit_msg>lxd/storage: Add const LO_FLAGS_DIRECT_IO flag<commit_after>//go:build linux && cgo
// +build linux,cgo

package drivers

import (
	"fmt"
	"os"
	"unsafe"

	"github.com/pkg/errors"

	// Used by cgo
	_ "github.com/lxc/lxd/lxd/include"
)

/*
#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif
#define _FILE_OFFSET_BITS 64
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <linux/loop.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "../../include/macro.h"
#include "../../include/memory_utils.h"

#define LXD_MAXPATH 4096
#define LXD_NUMSTRLEN64 21
#define LXD_MAX_LOOP_PATHLEN (2 * sizeof("loop/")) + LXD_NUMSTRLEN64 + sizeof("backing_file") + 1

// If a loop file is already associated with a loop device, find it.
// This looks at "/sys/block" to avoid having to parse all of "/dev". Also, this
// allows to retrieve the full name of the backing file even if
// strlen(backing file) > LO_NAME_SIZE.
static int find_associated_loop_device(const char *loop_file,
				       char *loop_dev_name)
{
	__do_closedir DIR *dir = NULL;
	char looppath[LXD_MAX_LOOP_PATHLEN];
	char buf[LXD_MAXPATH];
	struct dirent *dp;

	dir = opendir("/sys/block");
	if (!dir)
		return -1;

	while ((dp = readdir(dir))) {
		__do_close int loop_path_fd = -EBADF;
		int ret;
		size_t totlen;
		struct stat fstatbuf;
		int dfd = -1;

		if (!dp)
			break;

		if (strncmp(dp->d_name, "loop", 4))
			continue;

		dfd = dirfd(dir);
		if (dfd < 0)
			continue;

		ret = snprintf(looppath, sizeof(looppath), "%s/loop/backing_file", dp->d_name);
		if (ret < 0 || (size_t)ret >= sizeof(looppath))
			continue;

		ret = fstatat(dfd, looppath, &fstatbuf, 0);
		if (ret < 0)
			continue;

		loop_path_fd = openat(dfd, looppath, O_RDONLY | O_CLOEXEC, 0);
		if (ret < 0)
			continue;

		// Clear buffer.
		memset(buf, 0, sizeof(buf));
		ret = read(loop_path_fd, buf, sizeof(buf));
		if (ret < 0)
			continue;

		totlen = strlen(buf);

		// Trim newlines.
		while ((totlen > 0) && (buf[totlen - 1] == '\n'))
			buf[--totlen] = '\0';

		if (strcmp(buf, loop_file))
			continue;

		// Create path to loop device.
		ret = snprintf(loop_dev_name, LO_NAME_SIZE, "/dev/%s",
			       dp->d_name);
		if (ret < 0 || ret >= LO_NAME_SIZE)
			continue;

		// Open fd to loop device.
		return open(loop_dev_name, O_RDWR | O_CLOEXEC);
	}

	return -1;
}

static int get_unused_loop_dev_legacy(char *loop_name)
{
	__do_closedir DIR *dir = NULL;
	struct dirent *dp;
	struct loop_info64 lo64;

	dir = opendir("/dev");
	if (!dir)
		return -1;

	while ((dp = readdir(dir))) {
		__do_close int dfd = -EBADF, fd = -EBADF;
		int ret;

		if (!dp)
			break;

		if (strncmp(dp->d_name, "loop", 4) != 0)
			continue;

		dfd = dirfd(dir);
		if (dfd < 0)
			continue;

		fd = openat(dfd, dp->d_name, O_RDWR);
		if (fd < 0)
			continue;

		ret = ioctl(fd, LOOP_GET_STATUS64, &lo64);
		if (ret < 0)
			if (ioctl(fd, LOOP_GET_STATUS64, &lo64) == 0 || errno != ENXIO)
				continue;

		ret = snprintf(loop_name, LO_NAME_SIZE, "/dev/%s", dp->d_name);
		if (ret < 0 || ret >= LO_NAME_SIZE)
			continue;

		return move_fd(fd);
	}

	return -1;
}

static int get_unused_loop_dev(char *name_loop)
{
	__do_close int fd_ctl = -EBADF;
	int loop_nr, ret;

	fd_ctl = open("/dev/loop-control", O_RDWR | O_CLOEXEC);
	if (fd_ctl < 0)
		return -ENODEV;

	loop_nr = ioctl(fd_ctl, LOOP_CTL_GET_FREE);
	if (loop_nr < 0)
		return -1;

	ret = snprintf(name_loop, LO_NAME_SIZE, "/dev/loop%d", loop_nr);
	if (ret < 0 || ret >= LO_NAME_SIZE)
		return -1;

	return open(name_loop, O_RDWR | O_CLOEXEC);
}

static int prepare_loop_dev(const char *source, char *loop_dev, int flags)
{
	__do_close int fd_img = -EBADF, fd_loop = -EBADF;
	int ret;
	struct loop_info64 lo64;

	fd_loop = get_unused_loop_dev(loop_dev);
	if (fd_loop < 0) {
		if (fd_loop == -ENODEV)
			fd_loop = get_unused_loop_dev_legacy(loop_dev);
		else
			return -1;
	}

	fd_img = open(source, O_RDWR | O_CLOEXEC);
	if (fd_img < 0)
		return -1;

	ret = ioctl(fd_loop, LOOP_SET_FD, fd_img);
	if (ret < 0)
		return -1;

	memset(&lo64, 0, sizeof(lo64));
	lo64.lo_flags = flags;

	ret = ioctl(fd_loop, LOOP_SET_STATUS64, &lo64);
	if (ret < 0)
		return -1;

	return move_fd(fd_loop);
}

static inline int prepare_loop_dev_retry(const char *source, char *loop_dev, int flags)
{
	int ret;
	unsigned int idx = 0;

	do {
		ret = prepare_loop_dev(source, loop_dev, flags);
		idx++;
	} while (ret < 0 && errno == EBUSY && idx < 30);

	return ret;
}

// Note that this does not guarantee to clear the loop device in time so that
// find_associated_loop_device() will not report that there still is a
// configured device (udev and so on...). So don't call
// find_associated_loop_device() after having called
// set_autoclear_loop_device().
int set_autoclear_loop_device(int fd_loop)
{
	struct loop_info64 lo64;

	memset(&lo64, 0, sizeof(lo64));
	lo64.lo_flags = LO_FLAGS_AUTOCLEAR;
	errno = 0;
	return ioctl(fd_loop, LOOP_SET_STATUS64, &lo64);
}

// Directly release the loop device
int free_loop_device(int fd_loop)
{
	return ioctl(fd_loop, LOOP_CLR_FD);
}

// Unset the LO_FLAGS_AUTOCLEAR flag on the given loop device file descriptor.
int unset_autoclear_loop_device(int fd_loop)
{
	int ret;
	struct loop_info64 lo64;

	errno = 0;
	ret = ioctl(fd_loop, LOOP_GET_STATUS64, &lo64);
	if (ret < 0)
		return -1;

	if ((lo64.lo_flags & LO_FLAGS_AUTOCLEAR) == 0)
		return 0;

	lo64.lo_flags &= ~LO_FLAGS_AUTOCLEAR;
	errno = 0;
	return ioctl(fd_loop, LOOP_SET_STATUS64, &lo64);
}
*/
import "C"

// LoFlagsAutoclear determines whether the loop device will autodestruct on last
// close.
const LoFlagsAutoclear int = C.LO_FLAGS_AUTOCLEAR

// LoFlagsDirectIO determines whether the loop device will use Direct IO with the
// backing file.
const LoFlagsDirectIO int = C.LO_FLAGS_DIRECT_IO

// PrepareLoopDev detects and sets up a loop device for source. It returns an
// open file descriptor to the free loop device and the path of the free loop
// device. It's the callers responsibility to close the open file descriptor.
func PrepareLoopDev(source string, flags int) (*os.File, error) {
	cLoopDev := C.malloc(C.size_t(C.LO_NAME_SIZE))
	if cLoopDev == nil {
		return nil, fmt.Errorf("Failed to allocate memory in C")
	}
	defer C.free(cLoopDev)

	cSource := C.CString(source)
	defer C.free(unsafe.Pointer(cSource))
	loopFd, _ := C.find_associated_loop_device(cSource, (*C.char)(cLoopDev))
	if loopFd >= 0 {
		return os.NewFile(uintptr(loopFd), C.GoString((*C.char)(cLoopDev))), nil
	}

	loopFd, err := C.prepare_loop_dev_retry(cSource, (*C.char)(cLoopDev), C.int(flags))
	if loopFd < 0 {
		if err != nil {
			return nil, errors.Wrapf(err, "Failed to prepare loop device for %q", source)
		}

		return nil, fmt.Errorf("Failed to prepare loop device for %q", source)
	}

	return os.NewFile(uintptr(loopFd), C.GoString((*C.char)(cLoopDev))), nil
}

// releaseLoopDev releases the loop dev assigned to the provided file.
func releaseLoopDev(source string) error {
	cLoopDev := C.malloc(C.size_t(C.LO_NAME_SIZE))
	if cLoopDev == nil {
		return fmt.Errorf("Failed to allocate memory in C")
	}
	defer C.free(cLoopDev)

	cSource := C.CString(source)
	loopFd, err := C.find_associated_loop_device(cSource, (*C.char)(cLoopDev))
	if err != nil {
		return err
	}

	// Prepare a Go file and defer close on the loop device.
	fd := os.NewFile(uintptr(loopFd), C.GoString((*C.char)(cLoopDev)))
	defer fd.Close()

	if loopFd >= 0 {
		_, err := C.free_loop_device(C.int(loopFd))
		if err != nil {
			return err
		}
	}

	return nil
}

// SetAutoclearOnLoopDev enables autodestruction of the provided loopback device.
func SetAutoclearOnLoopDev(loopFd int) error {
	ret, err := C.set_autoclear_loop_device(C.int(loopFd))
	if ret < 0 {
		if err != nil {
			return err
		}
		return fmt.Errorf("Failed to set LO_FLAGS_AUTOCLEAR")
	}

	return nil
}

// UnsetAutoclearOnLoopDev disables autodestruction of the provided loopback device.
func UnsetAutoclearOnLoopDev(loopFd int) error {
	ret, err := C.unset_autoclear_loop_device(C.int(loopFd))
	if ret < 0 {
		if err != nil {
			return err
		}
		return fmt.Errorf("Failed to unset LO_FLAGS_AUTOCLEAR")
	}

	return nil
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"log"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"sync"
	"syscall"

	"github.com/joho/godotenv"
	"gopkg.in/Shopify/sarama.v1"
)

var (
	// Brokers the kafka broker connection string
	Brokers *string
	// MaxQueue max number of queue
	MaxQueue *string
	// MaxWorker max number of workers
	MaxWorker *string
	// Verbose use to turn on Sarama logging
	Verbose *bool
	// signals we want to gracefully shutdown
	// when it receives a SIGTERM or SIGINT
	signals = make(chan os.Signal, 1)
	done    = make(chan bool, 1)
)

// Job represents the job to be run
type Job struct {
	Payload Payload
}

// Payload the coming data payload
type Payload struct {
}

// JobQueue a buffered channel that we can send work requests on.
var JobQueue chan Job

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file. Rename sample.env file to .env")
	}

	Brokers = flag.String("brokers", os.Getenv("KAFKA_PEERS"), "The Kafka brokers to connect to, as a comma separated list")
	MaxQueue = flag.String("max-queue", os.Getenv("MAX_QUEUE"), "The maximum queues")
	MaxWorker = flag.String("max-worker", os.Getenv("MAX_WORKER"), "The maximum workers")
	Verbose = flag.Bool("verbose", false, "Turn on Sarama logging")
	flag.Parse()

	if *Verbose {
		sarama.Logger = log.New(os.Stdout, "[sarama] ", log.LstdFlags)
	}
	if *Brokers == "" {
		flag.PrintDefaults()
		os.Exit(1)
	}
	if *MaxWorker == "" {
		flag.PrintDefaults()
		os.Exit(1)
	}
	if *MaxQueue == "" {
		flag.PrintDefaults()
		os.Exit(1)
	}
	maxWorker, err := strconv.Atoi(*MaxWorker)
	if err != nil {
		log.Printf("Invalid MAX_WORKERS value: %s", err)
	}
	maxQueue, err := strconv.Atoi(*MaxWorker)
	if err != nil {
		log.Printf("Invalid MAX_WORKERS value: %s", err)
	}

	brokerList := strings.Split(*Brokers, ",")
	log.Printf("Kafka Brokers: %s", strings.Join(brokerList, ", "))
	log.Printf("Max Worker: %s", *MaxWorker)

	var wg sync.WaitGroup
	wg.Add(maxWorker)

	JobQueue = make(chan Job, maxQueue)
	dispatcher := NewDispatcher(maxWorker)
	dispatcher.Run()

	// Notify when receive SIGINT or SIGTERM
	// kill -SIGINT <PID> or Ctrl+c
	// kill -SIGTERM <PID>
	signal.Notify(signals,
		syscall.SIGINT,
		syscall.SIGTERM)

	go func() {
		for {
			select {
			case <-signals:
				log.Println("Graceful shutting down...")
				w := Worker{}
				for i := 0; i < maxWorker; i++ {
					w.Stop(&wg)
				}
				done <- true
			}
		}
	}()

	// Exiting
	<-done
	wg.Wait()
	log.Println("Successfully shutdown")
	os.Exit(0)
}
<commit_msg>update shut down log string<commit_after>package main

import (
	"flag"
	"log"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"sync"
	"syscall"

	"github.com/joho/godotenv"
	"gopkg.in/Shopify/sarama.v1"
)

var (
	// Brokers the kafka broker connection string
	Brokers *string
	// MaxQueue max number of queue
	MaxQueue *string
	// MaxWorker max number of workers
	MaxWorker *string
	// Verbose use to turn on Sarama logging
	Verbose *bool
	// signals we want to gracefully shutdown
	// when it receives a SIGTERM or SIGINT
	signals = make(chan os.Signal, 1)
	done    = make(chan bool, 1)
)

// Job represents the job to be run
type Job struct {
	Payload Payload
}

// Payload the coming data payload
type Payload struct {
}

// JobQueue a buffered channel that we can send work requests on.
var JobQueue chan Job

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file. Rename sample.env file to .env")
	}

	Brokers = flag.String("brokers", os.Getenv("KAFKA_PEERS"), "The Kafka brokers to connect to, as a comma separated list")
	MaxQueue = flag.String("max-queue", os.Getenv("MAX_QUEUE"), "The maximum queues")
	MaxWorker = flag.String("max-worker", os.Getenv("MAX_WORKER"), "The maximum workers")
	Verbose = flag.Bool("verbose", false, "Turn on Sarama logging")
	flag.Parse()

	if *Verbose {
		sarama.Logger = log.New(os.Stdout, "[sarama] ", log.LstdFlags)
	}
	if *Brokers == "" {
		flag.PrintDefaults()
		os.Exit(1)
	}
	if *MaxWorker == "" {
		flag.PrintDefaults()
		os.Exit(1)
	}
	if *MaxQueue == "" {
		flag.PrintDefaults()
		os.Exit(1)
	}
	maxWorker, err := strconv.Atoi(*MaxWorker)
	if err != nil {
		log.Printf("Invalid MAX_WORKERS value: %s", err)
	}
	maxQueue, err := strconv.Atoi(*MaxWorker)
	if err != nil {
		log.Printf("Invalid MAX_WORKERS value: %s", err)
	}

	brokerList := strings.Split(*Brokers, ",")
	log.Printf("Kafka Brokers: %s", strings.Join(brokerList, ", "))
	log.Printf("Max Worker: %s", *MaxWorker)

	var wg sync.WaitGroup
	wg.Add(maxWorker)

	JobQueue = make(chan Job, maxQueue)
	dispatcher := NewDispatcher(maxWorker)
	dispatcher.Run()

	// Notify when receive SIGINT or SIGTERM
	// kill -SIGINT <PID> or Ctrl+c
	// kill -SIGTERM <PID>
	signal.Notify(signals,
		syscall.SIGINT,
		syscall.SIGTERM)

	go func() {
		for {
			select {
			case <-signals:
				log.Println("Graceful shutting down...")
				w := Worker{}
				for i := 0; i < maxWorker; i++ {
					w.Stop(&wg)
				}
				done <- true
			}
		}
	}()

	// Exiting
	<-done
	wg.Wait()
	log.Println("Shut down completed")
	os.Exit(0)
}
<|endoftext|>


<commit_before>package fix

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/BurntSushi/toml"
)

//YesToAll will be used by the command to skip the questions
var YesToAll bool

var replace = map[string]string{
	"github.com/markbates/pop":                     "github.com/gobuffalo/pop",
	"github.com/markbates/validate":                "github.com/gobuffalo/validate",
	"github.com/satori/go.uuid":                    "github.com/gobuffalo/uuid",
	"github.com/markbates/willie":                  "github.com/gobuffalo/httptest",
	"github.com/shurcooL/github_flavored_markdown": "github.com/gobuffalo/github_flavored_markdown",
	"github.com/gobuffalo/buffalo-plugins":         "github.com/gobuffalo/buffalo/plugins",
}

var ic = ImportConverter{
	Data: replace,
}

var mr = MiddlewareTransformer{
	PackagesReplacement: map[string]string{
		"github.com/gobuffalo/buffalo/middleware/basicauth": "github.com/gobuffalo/mw-basicauth",
		"github.com/gobuffalo/buffalo/middleware/csrf":      "github.com/gobuffalo/mw-csrf",
		"github.com/gobuffalo/buffalo/middleware/i18n":      "github.com/gobuffalo/mw-i18n",
		"github.com/gobuffalo/buffalo/middleware/ssl":       "github.com/gobuffalo/mw-forcessl",
		"github.com/gobuffalo/buffalo/middleware/tokenauth": "github.com/gobuffalo/mw-tokenauth",
	},

	Aliases: map[string]string{
		"github.com/gobuffalo/mw-basicauth":   "basicauth",
		"github.com/gobuffalo/mw-csrf":        "csrf",
		"github.com/gobuffalo/mw-i18n":        "i18n",
		"github.com/gobuffalo/mw-forcessl":    "forcessl",
		"github.com/gobuffalo/mw-tokenauth":   "tokenauth",
		"github.com/gobuffalo/mw-paramlogger": "paramlogger",
		"github.com/gobuffalo/mw-contenttype": "contenttype",
	},
}

var checks = []Check{
	PackrClean,
	ic.Process,
	mr.transformPackages,
	WebpackCheck,
	PackageJSONCheck,
	AddPackageJSONScripts,
	DepEnsure,
	installTools,
	DeprecrationsCheck,
	fixDocker,
	encodeApp,
	Plugins,
}

func encodeApp(r *Runner) error {
	p := filepath.Join("config", "buffalo-app.toml")
	if _, err := os.Stat(p); err == nil {
		return nil
	}
	os.MkdirAll(filepath.Dir(p), 0755)
	f, err := os.Create(p)
	if err != nil {
		return err
	}
	if err := toml.NewEncoder(f).Encode(r.App); err != nil {
		return err
	}
	return nil
}

func ask(q string) bool {
	if YesToAll {
		return true
	}

	fmt.Printf("? %s [y/n]\n", q)

	reader := bufio.NewReader(os.Stdin)
	text, _ := reader.ReadString('\n')

	text = strings.ToLower(strings.TrimSpace(text))
	return text == "y" || text == "yes"
}
<commit_msg>Fix uuid and nulls packages in buffalo fix (#1732)<commit_after>package fix

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/BurntSushi/toml"
)

// YesToAll will be used by the command to skip the questions
var YesToAll bool

var replace = map[string]string{
	"github.com/markbates/pop":                     "github.com/gobuffalo/pop",
	"github.com/markbates/validate":                "github.com/gobuffalo/validate",
	"github.com/satori/go.uuid":                    "github.com/gofrs/uuid",
	"github.com/markbates/willie":                  "github.com/gobuffalo/httptest",
	"github.com/shurcooL/github_flavored_markdown": "github.com/gobuffalo/github_flavored_markdown",
	"github.com/gobuffalo/buffalo-plugins":         "github.com/gobuffalo/buffalo/plugins",
	"github.com/gobuffalo/uuid":                    "github.com/gofrs/uuid",
	"github.com/gobuffalo/pop/nulls":               "github.com/gobuffalo/nulls",
}

var ic = ImportConverter{
	Data: replace,
}

var mr = MiddlewareTransformer{
	PackagesReplacement: map[string]string{
		"github.com/gobuffalo/buffalo/middleware/basicauth": "github.com/gobuffalo/mw-basicauth",
		"github.com/gobuffalo/buffalo/middleware/csrf":      "github.com/gobuffalo/mw-csrf",
		"github.com/gobuffalo/buffalo/middleware/i18n":      "github.com/gobuffalo/mw-i18n",
		"github.com/gobuffalo/buffalo/middleware/ssl":       "github.com/gobuffalo/mw-forcessl",
		"github.com/gobuffalo/buffalo/middleware/tokenauth": "github.com/gobuffalo/mw-tokenauth",
	},

	Aliases: map[string]string{
		"github.com/gobuffalo/mw-basicauth":   "basicauth",
		"github.com/gobuffalo/mw-csrf":        "csrf",
		"github.com/gobuffalo/mw-i18n":        "i18n",
		"github.com/gobuffalo/mw-forcessl":    "forcessl",
		"github.com/gobuffalo/mw-tokenauth":   "tokenauth",
		"github.com/gobuffalo/mw-paramlogger": "paramlogger",
		"github.com/gobuffalo/mw-contenttype": "contenttype",
	},
}

var checks = []Check{
	PackrClean,
	ic.Process,
	mr.transformPackages,
	WebpackCheck,
	PackageJSONCheck,
	AddPackageJSONScripts,
	DepEnsure,
	installTools,
	DeprecrationsCheck,
	fixDocker,
	encodeApp,
	Plugins,
}

func encodeApp(r *Runner) error {
	p := filepath.Join("config", "buffalo-app.toml")
	if _, err := os.Stat(p); err == nil {
		return nil
	}
	os.MkdirAll(filepath.Dir(p), 0755)
	f, err := os.Create(p)
	if err != nil {
		return err
	}
	if err := toml.NewEncoder(f).Encode(r.App); err != nil {
		return err
	}
	return nil
}

func ask(q string) bool {
	if YesToAll {
		return true
	}

	fmt.Printf("? %s [y/n]\n", q)

	reader := bufio.NewReader(os.Stdin)
	text, _ := reader.ReadString('\n')

	text = strings.ToLower(strings.TrimSpace(text))
	return text == "y" || text == "yes"
}
<|endoftext|>


<commit_before>package docker

import (
	"fmt"
	"time"

	"strconv"

	"github.com/fsouza/go-dockerclient"
	"github.com/ninech/reception/common"
)

type Container struct {
	ID              string
	Name            string
	PublicPorts     []docker.APIPort
	Project         string
	Service         string
	ContainerNumber string
	HttpPort        int64
	IsMain          bool
}

type Client struct {
	Endpoint     string
	HostMap      *common.HostToHostMap
	containerMap map[string]Container
}

// private_port.service.project.docker:public_port
// service.project.docker:public_port
// project.docker:port // reception.main | app | 80 | 8080

func (client *Client) Launch() error {
	if client.Endpoint == "" {
		client.Endpoint = "unix:///var/run/docker.sock"
	}

	dockerClient, err := docker.NewClient(client.Endpoint)
	if err != nil {
		return err
	}

	containers, err := dockerClient.ListContainers(docker.ListContainersOptions{All: false})
	if err != nil {
		return err
	}

	buildContainerMap(containers, client)

	if len(client.containerMap) == 0 {
		return nil
	}

	/*
		Start of building Hostname Map
	*/
	var mainProjectContainer, appProjectContainer *Container

	client.HostMap.Lock()
	defer client.HostMap.Unlock()
	for _, container := range client.containerMap {
		if container.IsMain {
			if mainProjectContainer == nil {
				mainProjectContainer = &container
			} else {
				//TODO write warning: more than 1 "main"
			}
		}

		if container.Name == "app" && appProjectContainer == nil {
			appProjectContainer = &container
		}

		mainServiceHostSet := false
		for _, port := range container.PublicPorts {
			// private_port.service.project.docker:public_port
			backendHostname := fmt.Sprintf("%v:%v", port.IP, port.PublicPort)

			frontendPortHostname := fmt.Sprintf(
				"%v.%v.%v.docker:%v",
				port.PrivatePort,
				container.Service,
				container.Project,
				port.PublicPort)
			client.HostMap.M[frontendPortHostname] = backendHostname

			// service.project.docker:public_port
			if !mainServiceHostSet {
				switch port.PrivatePort {
				case container.HttpPort:
					// this is the designated http port set by the user
				case 80, 8080, 3000:
					// these are common http ports
				default:
					continue
				}

				frontendServiceHostname := fmt.Sprintf(
					"%v.%v.docker:%v",
					container.Service,
					container.Project,
					port.PublicPort)

				client.HostMap.M[frontendServiceHostname] = backendHostname
				mainServiceHostSet = true
			}
		}

		if mainProjectContainer == nil {
			if appProjectContainer != nil {
				mainProjectContainer = appProjectContainer
			} else {
				return nil
			}
		}

		// project.docker:public_port
		var theMainPort, aCommonHttpPort *docker.APIPort
		lowestPort := &mainProjectContainer.PublicPorts[0]
		for _, port := range mainProjectContainer.PublicPorts {
			if port.PrivatePort < lowestPort.PrivatePort {
				lowestPort = &port
			}

			switch port.PrivatePort {
			case container.HttpPort:
				theMainPort = &port
				break
			case 80, 8080, 3000:
				aCommonHttpPort = &port
			}
		}

		var thePort *docker.APIPort
		if theMainPort != nil {
			thePort = theMainPort
		} else if aCommonHttpPort != nil {
			thePort = aCommonHttpPort
		} else {
			thePort = lowestPort
		}

		frontendProjectHostname := fmt.Sprintf(
			"%v.%v.docker:%v",
			container.Service,
			container.Project,
			thePort.PublicPort)
		backendHostname := fmt.Sprintf("%v:%v", thePort.IP, thePort.PublicPort)
		client.HostMap.M[frontendProjectHostname] = backendHostname
	}

	listener := make(chan *docker.APIEvents)
	err = dockerClient.AddEventListener(listener)
	if err != nil {
		return err
	}

	defer func() {
		err = dockerClient.RemoveEventListener(listener)
		if err != nil {
			fmt.Println(err)
		}
	}()

	timeout := time.After(30 * time.Second)

	for {
		select {
		case event := <-listener:
			handleEvent(event)
		case <-timeout:
			return nil
		}
	}
	return nil
}

func buildContainerMap(containers []docker.APIContainers, client *Client) {
	for _, container := range containers {
		if !hasPublicTcpPorts(container.Ports) {
			continue
		}

		_, ok := container.Labels["reception.main"]
		httpPort, err := strconv.ParseInt(container.Labels["reception.http-port"], 10, 64)
		if err != nil {
			httpPort = 8080
		}

		client.containerMap[container.ID] = Container{
			ID:              container.ID,
			Name:            container.Names[0][1:],
			PublicPorts:     filterPublicTcpPorts(container.Ports),
			Project:         container.Labels["com.docker.compose.project"],
			Service:         container.Labels["com.docker.compose.service"],
			ContainerNumber: container.Labels["com.docker.compose.container-number"],
			HttpPort:        httpPort,
			IsMain:          ok,
		}
	}
}

func hasPublicTcpPorts(apiPorts []docker.APIPort) bool {
	for _, port := range apiPorts {
		if port.PublicPort != 0 && port.Type != "tcp" {
			return true
		}
	}
	return false
}

func filterPublicTcpPorts(apiPorts []docker.APIPort) (publicPorts []docker.APIPort) {
	for _, port := range apiPorts {
		if port.PublicPort == 0 || port.Type != "tcp" {
			continue
		}
		publicPorts = append(publicPorts, port)
	}
	return
}

func handleEvent(event *docker.APIEvents) bool {
	if event.Type != "container" {
		return false
	}

	switch event.Action {
	case "start", "stop":
		fmt.Println("Action: ", event.Action)
		fmt.Println("ID: ", event.ID)
		return true
	}

	return false
}
<commit_msg>Finish the docker client things<commit_after>package docker

import (
	"fmt"
	"strconv"

	"github.com/fsouza/go-dockerclient"
	"github.com/ninech/reception/common"
)

type Container struct {
	ID              string
	Name            string
	PublicPorts     []docker.APIPort
	Project         string
	Service         string
	ContainerNumber string
	HttpPort        int64
	IsMain          bool
}

type Client struct {
	Endpoint     string
	HostMap      *common.HostToHostMap
	containerMap *map[string]Container
	dockerClient *docker.Client
}

// private_port.service.project.docker:public_port
// service.project.docker:public_port
// project.docker:port // reception.main | app | 80 | 8080

func (client *Client) Launch() error {
	if client.Endpoint == "" {
		client.Endpoint = "unix:///var/run/docker.sock"
	}

	dockerClient, err := docker.NewClient(client.Endpoint)
	if err != nil {
		return err
	}
	client.dockerClient = dockerClient

	containerMap := make(map[string]Container)
	client.containerMap = &containerMap

	client.updateMappings()

	listener := make(chan *docker.APIEvents)
	err = dockerClient.AddEventListener(listener)
	if err != nil {
		return err
	}

	defer func() {
		err = dockerClient.RemoveEventListener(listener)
		if err != nil {
			fmt.Println(err)
		}
	}()

	for {
		event := <-listener
		err = client.handleEvent(event)
		if err != nil {
			return err
		}
	}
	return nil
}

// re-reads the containers and re-builds the hostToHost Mapping
func (client *Client) updateMappings() error {

	err := client.updateContainers()
	if err != nil {
		return err
	}

	err = client.updateHostMap()
	if err != nil {
		return err
	}
	return nil
}

// updates the map which maps containers names to their public ports
func (client *Client) updateHostMap() error {
	var mainProjectContainer, appProjectContainer *Container

	client.HostMap.Lock()
	defer client.HostMap.Unlock()

	// clean the map
	for key := range client.HostMap.M {
		delete(client.HostMap.M, key)
	}

	containerMap := *client.containerMap

	// add the appropriate names for any container to the map
	for _, container := range containerMap {

		if container.IsMain {
			if mainProjectContainer == nil {
				mainProjectContainer = &container
			} else {
				fmt.Errorf(
					"More than one container with 'reception.main' label, at least '%v' (chosen) and '%v' (ignoring).",
					mainProjectContainer.Name,
					container.Name,
				)
			}
		}

		if container.Name == "app" && appProjectContainer == nil {
			appProjectContainer = &container
		}

		mainServiceHostSet := false
		for _, port := range container.PublicPorts {
			// private_port.service.project.docker:public_port
			backendHostname := fmt.Sprintf("%v:%v", port.IP, port.PublicPort)

			frontendPortHostname := fmt.Sprintf(
				"%v.%v.%v.docker",
				port.PrivatePort,
				container.Service,
				container.Project)
			client.HostMap.M[frontendPortHostname] = backendHostname
			fmt.Printf("Added route from '%v' to '%v'.\n", frontendPortHostname, backendHostname)

			// service.project.docker:public_port
			if !mainServiceHostSet {
				switch port.PrivatePort {
				case container.HttpPort:
					// this is the designated http port set by the user
				case 80, 8080, 3000:
					// these are common http ports
				default:
					continue
				}

				frontendServiceHostname := fmt.Sprintf(
					"%v.%v.docker",
					container.Service,
					container.Project)
				client.HostMap.M[frontendServiceHostname] = backendHostname
				fmt.Printf("Added route from '%v' to '%v'.\n", frontendServiceHostname, backendHostname)

				mainServiceHostSet = true
			}
		}

		if mainProjectContainer == nil {
			if appProjectContainer != nil {
				mainProjectContainer = appProjectContainer
			} else {
				return nil
			}
		}

		// project.docker:public_port
		var theMainPort, aCommonHttpPort *docker.APIPort
		lowestPort := &mainProjectContainer.PublicPorts[0]
		for _, port := range mainProjectContainer.PublicPorts {
			if port.PrivatePort < lowestPort.PrivatePort {
				lowestPort = &port
			}

			switch port.PrivatePort {
			case container.HttpPort:
				theMainPort = &port
				break
			case 80, 8080, 3000:
				aCommonHttpPort = &port
			}
		}

		var thePort *docker.APIPort
		if theMainPort != nil {
			thePort = theMainPort
		} else if aCommonHttpPort != nil {
			thePort = aCommonHttpPort
		} else {
			thePort = lowestPort
		}

		frontendProjectHostname := fmt.Sprintf(
			"%v.%v.docker",
			container.Service,
			container.Project)
		backendHostname := fmt.Sprintf("%v:%v", thePort.IP, thePort.PublicPort)
		client.HostMap.M[frontendProjectHostname] = backendHostname
		fmt.Printf("Added route from '%v' to '%v'.\n", frontendProjectHostname, backendHostname)
	}

	return nil
}

// updates the list of containers
func (client *Client) updateContainers() error {
	containers, err := client.dockerClient.ListContainers(docker.ListContainersOptions{All: false})
	if err != nil {
		return err
	}

	// clean old content
	containerMap := *client.containerMap
	for k := range containerMap {
		delete(containerMap, k)
	}

	// add all containers to the map
	for _, container := range containers {
		client.addToContainerMap(container)
	}
	return nil
}

// adds a container to the container map
func (client *Client) addToContainerMap(container docker.APIContainers) {
	if !hasPublicTcpPorts(container.Ports) {
		return
	}

	_, ok := container.Labels["reception.main"]
	httpPort, err := strconv.ParseInt(container.Labels["reception.http-port"], 10, 64)
	if err != nil {
		httpPort = 8080
	}

	publicTcpPorts := filterPublicTcpPorts(container.Ports)

	containerMap := *client.containerMap
	containerMap[container.ID] = Container{
		ID:              container.ID,
		Name:            container.Names[0][1:],
		PublicPorts:     publicTcpPorts,
		Project:         container.Labels["com.docker.compose.project"],
		Service:         container.Labels["com.docker.compose.service"],
		ContainerNumber: container.Labels["com.docker.compose.container-number"],
		HttpPort:        httpPort,
		IsMain:          ok,
	}
}

// returns true, if any of the ports are exposed to the host machine
func hasPublicTcpPorts(apiPorts []docker.APIPort) bool {
	for _, port := range apiPorts {
		if port.PublicPort != 0 && port.Type == "tcp" {
			return true
		}
	}
	return false
}

// of the given ports it only returns those who are exposed to the host machine
func filterPublicTcpPorts(apiPorts []docker.APIPort) (publicPorts []docker.APIPort) {
	for _, port := range apiPorts {
		if port.PublicPort == 0 || port.Type != "tcp" {
			continue
		}
		publicPorts = append(publicPorts, port)
	}
	return
}

// handles an event emitted by Docker
func (client *Client) handleEvent(event *docker.APIEvents) error {
	if event.Type != "container" {
		return nil
	}

	switch event.Action {
	case "start", "stop":

		err := client.updateMappings()
		if err != nil {
			return err
		}
	}
	return nil
}
<|endoftext|>