<commit_before>package sharing

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strings"

	"github.com/cozy/cozy-stack/client/auth"
	"github.com/cozy/cozy-stack/client/request"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/pkg/oauth"
	"github.com/cozy/cozy-stack/pkg/permissions"
	"github.com/cozy/cozy-stack/web/jsonapi"
)

// CreateSharingRequest sends information about the sharing to the recipient's cozy
func (m *Member) CreateSharingRequest(inst *instance.Instance, s *Sharing, c *Credentials, u *url.URL) error {
	rules := make([]Rule, 0, len(s.Rules))
	for _, rule := range s.Rules {
		if rule.Local {
			continue
		}
		if rule.FilesByID() {
			values := make([]string, len(rule.Values))
			for i, v := range rule.Values {
				values[i] = XorID(v, c.XorKey)
			}
			rule.Values = values
		}
		rules = append(rules, rule)
	}
	members := make([]Member, len(s.Members))
	for i, m := range s.Members {
		// Instance and name are private...
		members[i] = Member{
			Status:     m.Status,
			PublicName: m.PublicName,
			Email:      m.Email,
			ReadOnly:   m.ReadOnly,
		}
		// ... except for the sharer and the recipient of this request
		if i == 0 || &s.Credentials[i-1] == c {
			members[i].Instance = m.Instance
		}
	}
	sh := APISharing{
		&Sharing{
			SID:         s.SID,
			Active:      false,
			Owner:       false,
			Open:        s.Open,
			Description: s.Description,
			AppSlug:     s.AppSlug,
			PreviewPath: s.PreviewPath,
			CreatedAt:   s.CreatedAt,
			UpdatedAt:   s.UpdatedAt,
			Rules:       rules,
			Members:     members,
		},
		nil,
		nil,
	}
	data, err := jsonapi.MarshalObject(&sh)
	if err != nil {
		return err
	}
	body, err := json.Marshal(jsonapi.Document{Data: &data})
	if err != nil {
		return err
	}
	res, err := request.Req(&request.Options{
		Method: http.MethodPut,
		Scheme: u.Scheme,
		Domain: u.Host,
		Path:   "/sharings/" + s.SID,
		Headers: request.Headers{
			"Accept":       "application/vnd.api+json",
			"Content-Type": "application/vnd.api+json",
		},
		Body: bytes.NewReader(body),
	})
	if err != nil {
		return err
	}
	res.Body.Close()
	if res.StatusCode/100 != 2 {
		return ErrRequestFailed
	}
	return nil
}

// RegisterCozyURL saves a new Cozy URL for a member
func (s *Sharing) RegisterCozyURL(inst *instance.Instance, m *Member, cozyURL string) error {
	if !s.Owner {
		return ErrInvalidSharing
	}

	cozyURL = strings.TrimSpace(cozyURL)
	if !strings.Contains(cozyURL, "://") {
		cozyURL = "https://" + cozyURL
	}
	u, err := url.Parse(cozyURL)
	if err != nil || u.Host == "" {
		return ErrInvalidURL
	}
	u.Path = ""
	u.RawPath = ""
	u.RawQuery = ""
	u.Fragment = ""
	m.Instance = u.String()

	creds := s.FindCredentials(m)
	if creds == nil {
		return ErrInvalidSharing
	}
	if err = m.CreateSharingRequest(inst, s, creds, u); err != nil {
		inst.Logger().WithField("nspace", "sharing").Warnf("Error on sharing request: %s", err)
		return ErrRequestFailed
	}
	return couchdb.UpdateDoc(inst, s)
}

// GenerateOAuthURL takes care of creating a correct OAuth request for
// the given member of the sharing.
func (m *Member) GenerateOAuthURL(s *Sharing) (string, error) {
	if !s.Owner || len(s.Members) != len(s.Credentials)+1 {
		return "", ErrInvalidSharing
	}
	creds := s.FindCredentials(m)
	if creds == nil {
		return "", ErrInvalidSharing
	}
	if m.Instance == "" {
		return "", ErrNoOAuthClient
	}

	u, err := url.Parse(m.Instance)
	if err != nil {
		return "", err
	}
	u.Path = "/auth/authorize/sharing"

	q := url.Values{
		"sharing_id": {s.SID},
		"state":      {creds.State},
	}
	u.RawQuery = q.Encode()

	return u.String(), nil
}

// CreateOAuthClient creates an OAuth client for a recipient of the given sharing
func CreateOAuthClient(inst *instance.Instance, m *Member) (*oauth.Client, error) {
	if m.Instance == "" {
		return nil, ErrInvalidURL
	}
	cli := oauth.Client{
		RedirectURIs: []string{m.Instance + "/sharings/answer"},
		ClientName:   "Sharing " + m.PublicName,
		ClientKind:   "sharing",
		SoftwareID:   "github.com/cozy/cozy-stack",
		ClientURI:    m.Instance + "/",
	}
	if err := cli.Create(inst); err != nil {
		return nil, ErrInternalServerError
	}
	return &cli, nil
}

// DeleteOAuthClient removes the client associated to the given member
func DeleteOAuthClient(inst *instance.Instance, m *Member, cred *Credentials) error {
	if m.Instance == "" {
		return ErrInvalidURL
	}
	clientID := cred.InboundClientID
	if clientID == "" {
		return nil
	}
	client, err := oauth.FindClient(inst, clientID)
	if err != nil {
		return err
	}
	if cerr := client.Delete(inst); cerr != nil {
		return errors.New(cerr.Error)
	}
	return nil
}

// ConvertOAuthClient converts an OAuth client from one type (pkg/oauth.Client)
// to another (client/auth.Client)
func ConvertOAuthClient(c *oauth.Client) *auth.Client {
	return &auth.Client{
		ClientID:          c.ClientID,
		ClientSecret:      c.ClientSecret,
		SecretExpiresAt:   c.SecretExpiresAt,
		RegistrationToken: c.RegistrationToken,
		RedirectURIs:      c.RedirectURIs,
		ClientName:        c.ClientName,
		ClientKind:        c.ClientKind,
		ClientURI:         c.ClientURI,
		LogoURI:           c.LogoURI,
		PolicyURI:         c.PolicyURI,
		SoftwareID:        c.SoftwareID,
		SoftwareVersion:   c.SoftwareVersion,
	}
}

// CreateAccessToken creates an access token for the given OAuth client,
// with a scope on this sharing.
func CreateAccessToken(inst *instance.Instance, cli *oauth.Client, sharingID string, verb permissions.VerbSet) (*auth.AccessToken, error) {
	scope := consts.Sharings + ":" + verb.String() + ":" + sharingID
	cli.CouchID = cli.ClientID // XXX CouchID is required by CreateJWT
	refresh, err := cli.CreateJWT(inst, permissions.RefreshTokenAudience, scope)
	if err != nil {
		return nil, err
	}
	access, err := cli.CreateJWT(inst, permissions.AccessTokenAudience, scope)
	if err != nil {
		return nil, err
	}
	return &auth.AccessToken{
		TokenType:    "bearer",
		AccessToken:  access,
		RefreshToken: refresh,
		Scope:        scope,
	}, nil
}

// SendAnswer says to the sharer's Cozy that the sharing has been accepted, and
// materialize that by an exchange of credentials.
func (s *Sharing) SendAnswer(inst *instance.Instance, state string) error {
	if s.Owner || len(s.Members) < 2 || len(s.Credentials) != 1 {
		return ErrInvalidSharing
	}
	u, err := url.Parse(s.Members[0].Instance)
	if s.Members[0].Instance == "" || err != nil {
		return ErrInvalidSharing
	}
	cli, err := CreateOAuthClient(inst, &s.Members[0])
	if err != nil {
		return err
	}
	token, err := CreateAccessToken(inst, cli, s.SID, permissions.ALL)
	if err != nil {
		return err
	}
	name, err := inst.PublicName()
	if err != nil {
		inst.Logger().WithField("nspace", "sharing").
			Infof("No name for instance %v", inst)
	}
	ac := APICredentials{
		Credentials: &Credentials{
			State:       state,
			Client:      ConvertOAuthClient(cli),
			AccessToken: token,
		},
		PublicName: name,
		CID:        s.SID,
	}
	data, err := jsonapi.MarshalObject(&ac)
	if err != nil {
		return err
	}
	body, err := json.Marshal(jsonapi.Document{Data: &data})
	if err != nil {
		return err
	}
	res, err := request.Req(&request.Options{
		Method: http.MethodPost,
		Scheme: u.Scheme,
		Domain: u.Host,
		Path:   "/sharings/" + s.SID + "/answer",
		Headers: request.Headers{
			"Accept":       "application/vnd.api+json",
			"Content-Type": "application/vnd.api+json",
		},
		Body: bytes.NewReader(body),
	})
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode/100 != 2 {
		return ErrRequestFailed
	}

	for i, m := range s.Members {
		if i > 0 && m.Instance != "" {
			if m.Status == MemberStatusMailNotSent ||
				m.Status == MemberStatusPendingInvitation {
				m.Status = MemberStatusReady
			}
		}
	}

	if err = s.SetupReceiver(inst); err != nil {
		return err
	}

	var creds Credentials
	if _, err = jsonapi.Bind(res.Body, &creds); err != nil {
		return ErrRequestFailed
	}
	s.Credentials[0].XorKey = creds.XorKey
	s.Credentials[0].InboundClientID = cli.ClientID
	s.Credentials[0].AccessToken = creds.AccessToken
	s.Credentials[0].Client = creds.Client
	s.Active = true
	return couchdb.UpdateDoc(inst, s)
}

// ProcessAnswer takes somes credentials and update the sharing with those.
func (s *Sharing) ProcessAnswer(inst *instance.Instance, creds *APICredentials) (*APICredentials, error) {
	if !s.Owner || len(s.Members) != len(s.Credentials)+1 {
		return nil, ErrInvalidSharing
	}
	for i, c := range s.Credentials {
		if c.State == creds.State {
			s.Members[i+1].Status = MemberStatusReady
			s.Members[i+1].PublicName = creds.PublicName
			s.Credentials[i].Client = creds.Client
			s.Credentials[i].AccessToken = creds.AccessToken
			ac := APICredentials{
				CID: s.SID,
				Credentials: &Credentials{
					XorKey: c.XorKey,
				},
			}
			// Create the credentials for the recipient
			cli, err := CreateOAuthClient(inst, &s.Members[i+1])
			if err != nil {
				return &ac, nil
			}
			s.Credentials[i].InboundClientID = cli.ClientID
			ac.Credentials.Client = ConvertOAuthClient(cli)
			var verb permissions.VerbSet
			// In case of read-only, the recipient only needs read access on the
			// sharing, e.g. to notify the sharer of a revocation
			if s.ReadOnly() || s.Members[i+1].ReadOnly {
				verb = permissions.Verbs(permissions.GET)
			} else {
				verb = permissions.ALL
			}
			token, err := CreateAccessToken(inst, cli, s.SID, verb)
			if err != nil {
				return &ac, nil
			}
			ac.Credentials.AccessToken = token

			s.Active = true
			if err := couchdb.UpdateDoc(inst, s); err != nil {
				return nil, err
			}
			go s.Setup(inst, &s.Members[i+1])
			return &ac, nil
		}
	}
	return nil, ErrMemberNotFound
}

// RefreshToken is used after a failed request with a 4xx error code.
// It renews the access token and retries the request
func RefreshToken(inst *instance.Instance, s *Sharing, m *Member, creds *Credentials, opts *request.Options, body []byte) (*http.Response, error) {
	if err := creds.Refresh(inst, s, m); err != nil {
		return nil, err
	}
	opts.Headers["Authorization"] = creds.AccessToken.AccessToken
	if body != nil {
		opts.Body = bytes.NewReader(body)
	}
	res, err := request.Req(opts)
	if err != nil {
		return nil, err
	}
	if res.StatusCode/100 == 5 {
		res.Body.Close()
		return nil, ErrInternalServerError
	}
	if res.StatusCode/100 != 2 {
		res.Body.Close()
		return nil, ErrClientError
	}
	return res, nil
}
<commit_msg>Fix member status for single recipient<commit_after>package sharing

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strings"

	"github.com/cozy/cozy-stack/client/auth"
	"github.com/cozy/cozy-stack/client/request"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/instance"
	"github.com/cozy/cozy-stack/pkg/oauth"
	"github.com/cozy/cozy-stack/pkg/permissions"
	"github.com/cozy/cozy-stack/web/jsonapi"
)

// CreateSharingRequest sends information about the sharing to the recipient's cozy
func (m *Member) CreateSharingRequest(inst *instance.Instance, s *Sharing, c *Credentials, u *url.URL) error {
	rules := make([]Rule, 0, len(s.Rules))
	for _, rule := range s.Rules {
		if rule.Local {
			continue
		}
		if rule.FilesByID() {
			values := make([]string, len(rule.Values))
			for i, v := range rule.Values {
				values[i] = XorID(v, c.XorKey)
			}
			rule.Values = values
		}
		rules = append(rules, rule)
	}
	members := make([]Member, len(s.Members))
	for i, m := range s.Members {
		// Instance and name are private...
		members[i] = Member{
			Status:     m.Status,
			PublicName: m.PublicName,
			Email:      m.Email,
			ReadOnly:   m.ReadOnly,
		}
		// ... except for the sharer and the recipient of this request
		if i == 0 || &s.Credentials[i-1] == c {
			members[i].Instance = m.Instance
		}
	}
	sh := APISharing{
		&Sharing{
			SID:         s.SID,
			Active:      false,
			Owner:       false,
			Open:        s.Open,
			Description: s.Description,
			AppSlug:     s.AppSlug,
			PreviewPath: s.PreviewPath,
			CreatedAt:   s.CreatedAt,
			UpdatedAt:   s.UpdatedAt,
			Rules:       rules,
			Members:     members,
		},
		nil,
		nil,
	}
	data, err := jsonapi.MarshalObject(&sh)
	if err != nil {
		return err
	}
	body, err := json.Marshal(jsonapi.Document{Data: &data})
	if err != nil {
		return err
	}
	res, err := request.Req(&request.Options{
		Method: http.MethodPut,
		Scheme: u.Scheme,
		Domain: u.Host,
		Path:   "/sharings/" + s.SID,
		Headers: request.Headers{
			"Accept":       "application/vnd.api+json",
			"Content-Type": "application/vnd.api+json",
		},
		Body: bytes.NewReader(body),
	})
	if err != nil {
		return err
	}
	res.Body.Close()
	if res.StatusCode/100 != 2 {
		return ErrRequestFailed
	}
	return nil
}

// RegisterCozyURL saves a new Cozy URL for a member
func (s *Sharing) RegisterCozyURL(inst *instance.Instance, m *Member, cozyURL string) error {
	if !s.Owner {
		return ErrInvalidSharing
	}

	cozyURL = strings.TrimSpace(cozyURL)
	if !strings.Contains(cozyURL, "://") {
		cozyURL = "https://" + cozyURL
	}
	u, err := url.Parse(cozyURL)
	if err != nil || u.Host == "" {
		return ErrInvalidURL
	}
	u.Path = ""
	u.RawPath = ""
	u.RawQuery = ""
	u.Fragment = ""
	m.Instance = u.String()

	creds := s.FindCredentials(m)
	if creds == nil {
		return ErrInvalidSharing
	}
	if err = m.CreateSharingRequest(inst, s, creds, u); err != nil {
		inst.Logger().WithField("nspace", "sharing").Warnf("Error on sharing request: %s", err)
		return ErrRequestFailed
	}
	return couchdb.UpdateDoc(inst, s)
}

// GenerateOAuthURL takes care of creating a correct OAuth request for
// the given member of the sharing.
func (m *Member) GenerateOAuthURL(s *Sharing) (string, error) {
	if !s.Owner || len(s.Members) != len(s.Credentials)+1 {
		return "", ErrInvalidSharing
	}
	creds := s.FindCredentials(m)
	if creds == nil {
		return "", ErrInvalidSharing
	}
	if m.Instance == "" {
		return "", ErrNoOAuthClient
	}

	u, err := url.Parse(m.Instance)
	if err != nil {
		return "", err
	}
	u.Path = "/auth/authorize/sharing"

	q := url.Values{
		"sharing_id": {s.SID},
		"state":      {creds.State},
	}
	u.RawQuery = q.Encode()

	return u.String(), nil
}

// CreateOAuthClient creates an OAuth client for a recipient of the given sharing
func CreateOAuthClient(inst *instance.Instance, m *Member) (*oauth.Client, error) {
	if m.Instance == "" {
		return nil, ErrInvalidURL
	}
	cli := oauth.Client{
		RedirectURIs: []string{m.Instance + "/sharings/answer"},
		ClientName:   "Sharing " + m.PublicName,
		ClientKind:   "sharing",
		SoftwareID:   "github.com/cozy/cozy-stack",
		ClientURI:    m.Instance + "/",
	}
	if err := cli.Create(inst); err != nil {
		return nil, ErrInternalServerError
	}
	return &cli, nil
}

// DeleteOAuthClient removes the client associated to the given member
func DeleteOAuthClient(inst *instance.Instance, m *Member, cred *Credentials) error {
	if m.Instance == "" {
		return ErrInvalidURL
	}
	clientID := cred.InboundClientID
	if clientID == "" {
		return nil
	}
	client, err := oauth.FindClient(inst, clientID)
	if err != nil {
		return err
	}
	if cerr := client.Delete(inst); cerr != nil {
		return errors.New(cerr.Error)
	}
	return nil
}

// ConvertOAuthClient converts an OAuth client from one type (pkg/oauth.Client)
// to another (client/auth.Client)
func ConvertOAuthClient(c *oauth.Client) *auth.Client {
	return &auth.Client{
		ClientID:          c.ClientID,
		ClientSecret:      c.ClientSecret,
		SecretExpiresAt:   c.SecretExpiresAt,
		RegistrationToken: c.RegistrationToken,
		RedirectURIs:      c.RedirectURIs,
		ClientName:        c.ClientName,
		ClientKind:        c.ClientKind,
		ClientURI:         c.ClientURI,
		LogoURI:           c.LogoURI,
		PolicyURI:         c.PolicyURI,
		SoftwareID:        c.SoftwareID,
		SoftwareVersion:   c.SoftwareVersion,
	}
}

// CreateAccessToken creates an access token for the given OAuth client,
// with a scope on this sharing.
func CreateAccessToken(inst *instance.Instance, cli *oauth.Client, sharingID string, verb permissions.VerbSet) (*auth.AccessToken, error) {
	scope := consts.Sharings + ":" + verb.String() + ":" + sharingID
	cli.CouchID = cli.ClientID // XXX CouchID is required by CreateJWT
	refresh, err := cli.CreateJWT(inst, permissions.RefreshTokenAudience, scope)
	if err != nil {
		return nil, err
	}
	access, err := cli.CreateJWT(inst, permissions.AccessTokenAudience, scope)
	if err != nil {
		return nil, err
	}
	return &auth.AccessToken{
		TokenType:    "bearer",
		AccessToken:  access,
		RefreshToken: refresh,
		Scope:        scope,
	}, nil
}

// SendAnswer says to the sharer's Cozy that the sharing has been accepted, and
// materialize that by an exchange of credentials.
func (s *Sharing) SendAnswer(inst *instance.Instance, state string) error {
	if s.Owner || len(s.Members) < 2 || len(s.Credentials) != 1 {
		return ErrInvalidSharing
	}
	u, err := url.Parse(s.Members[0].Instance)
	if s.Members[0].Instance == "" || err != nil {
		return ErrInvalidSharing
	}
	cli, err := CreateOAuthClient(inst, &s.Members[0])
	if err != nil {
		return err
	}
	token, err := CreateAccessToken(inst, cli, s.SID, permissions.ALL)
	if err != nil {
		return err
	}
	name, err := inst.PublicName()
	if err != nil {
		inst.Logger().WithField("nspace", "sharing").
			Infof("No name for instance %v", inst)
	}
	ac := APICredentials{
		Credentials: &Credentials{
			State:       state,
			Client:      ConvertOAuthClient(cli),
			AccessToken: token,
		},
		PublicName: name,
		CID:        s.SID,
	}
	data, err := jsonapi.MarshalObject(&ac)
	if err != nil {
		return err
	}
	body, err := json.Marshal(jsonapi.Document{Data: &data})
	if err != nil {
		return err
	}
	res, err := request.Req(&request.Options{
		Method: http.MethodPost,
		Scheme: u.Scheme,
		Domain: u.Host,
		Path:   "/sharings/" + s.SID + "/answer",
		Headers: request.Headers{
			"Accept":       "application/vnd.api+json",
			"Content-Type": "application/vnd.api+json",
		},
		Body: bytes.NewReader(body),
	})
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode/100 != 2 {
		return ErrRequestFailed
	}

	for i, m := range s.Members {
		if i > 0 && m.Instance != "" {
			if m.Status == MemberStatusMailNotSent ||
				m.Status == MemberStatusPendingInvitation {
				s.Members[i].Status = MemberStatusReady
			}
		}
	}

	if err = s.SetupReceiver(inst); err != nil {
		return err
	}

	var creds Credentials
	if _, err = jsonapi.Bind(res.Body, &creds); err != nil {
		return ErrRequestFailed
	}
	s.Credentials[0].XorKey = creds.XorKey
	s.Credentials[0].InboundClientID = cli.ClientID
	s.Credentials[0].AccessToken = creds.AccessToken
	s.Credentials[0].Client = creds.Client
	s.Active = true
	return couchdb.UpdateDoc(inst, s)
}

// ProcessAnswer takes somes credentials and update the sharing with those.
func (s *Sharing) ProcessAnswer(inst *instance.Instance, creds *APICredentials) (*APICredentials, error) {
	if !s.Owner || len(s.Members) != len(s.Credentials)+1 {
		return nil, ErrInvalidSharing
	}
	for i, c := range s.Credentials {
		if c.State == creds.State {
			s.Members[i+1].Status = MemberStatusReady
			s.Members[i+1].PublicName = creds.PublicName
			s.Credentials[i].Client = creds.Client
			s.Credentials[i].AccessToken = creds.AccessToken
			ac := APICredentials{
				CID: s.SID,
				Credentials: &Credentials{
					XorKey: c.XorKey,
				},
			}
			// Create the credentials for the recipient
			cli, err := CreateOAuthClient(inst, &s.Members[i+1])
			if err != nil {
				return &ac, nil
			}
			s.Credentials[i].InboundClientID = cli.ClientID
			ac.Credentials.Client = ConvertOAuthClient(cli)
			var verb permissions.VerbSet
			// In case of read-only, the recipient only needs read access on the
			// sharing, e.g. to notify the sharer of a revocation
			if s.ReadOnly() || s.Members[i+1].ReadOnly {
				verb = permissions.Verbs(permissions.GET)
			} else {
				verb = permissions.ALL
			}
			token, err := CreateAccessToken(inst, cli, s.SID, verb)
			if err != nil {
				return &ac, nil
			}
			ac.Credentials.AccessToken = token

			s.Active = true
			if err := couchdb.UpdateDoc(inst, s); err != nil {
				return nil, err
			}
			go s.Setup(inst, &s.Members[i+1])
			return &ac, nil
		}
	}
	return nil, ErrMemberNotFound
}

// RefreshToken is used after a failed request with a 4xx error code.
// It renews the access token and retries the request
func RefreshToken(inst *instance.Instance, s *Sharing, m *Member, creds *Credentials, opts *request.Options, body []byte) (*http.Response, error) {
	if err := creds.Refresh(inst, s, m); err != nil {
		return nil, err
	}
	opts.Headers["Authorization"] = creds.AccessToken.AccessToken
	if body != nil {
		opts.Body = bytes.NewReader(body)
	}
	res, err := request.Req(opts)
	if err != nil {
		return nil, err
	}
	if res.StatusCode/100 == 5 {
		res.Body.Close()
		return nil, ErrInternalServerError
	}
	if res.StatusCode/100 != 2 {
		res.Body.Close()
		return nil, ErrClientError
	}
	return res, nil
}
<|endoftext|>


<commit_before>package tpm

// Manual testing needed or tpm hardware in VM
import (
	"io"
	"os"
	"testing"

	"github.com/stretchr/testify/require"
)

const (
	testReadPcrIndex  uint32 = 23
	testWritePcrIndex uint32 = 16
	testString        string = "teststring"
)

// Beware, TPM testa are a horrible idea because of state transitions
// for PCR. Powercycles are not possible due security architecture.
// Tests must be run as root and follow specific execution flow.
// Also do not run test on a production system if the tpm is used elsewhere.

func TestTPM1NewTPM(t *testing.T) {
	// TODO use a fake TPM. Unfortunately go-tpm checks if it's a device or a
	// socket, and fails otherwise. Need to use github.com/stefanberger/swtpm or
	// mock go-tpm entirely
	TPMOpener = func(string) (io.ReadWriteCloser, error) {
		fd, err := os.Open("tests/fake_tpm")
		if err != nil {
			return nil, err
		}
		return io.ReadWriteCloser(fd), nil
	}
	TpmCapabilities = "tests/fake_caps_tpm12"
	TpmOwnershipState = "tests/fake_owned_1"
	TpmActivatedState = "tests/fake_active_1"
	TpmEnabledState = "tests/fake_enabled_1"
	TpmTempDeactivatedState = "tests/fake_temp_deactivated_0"

	tpm, err := NewTPM()
	require.NoError(t, err)
	require.Equal(t, tpm12, tpm.Version())
}

func TestTPM1ReadPcr(t *testing.T) {
	tpm, err := NewTPM()
	tpm.(*TPM1).pcrReader = func(io.ReadWriter, uint32) ([]byte, error) {
		return make([]byte, 20), nil
	}
	require.NoError(t, err)
	pcrData, err := tpm.ReadPCR(testReadPcrIndex)
	require.NoError(t, err)
	require.Equal(t, pcrData, make([]byte, 20))
}

/*
func TestMeasureTPM1(t *testing.T) {
	oldPcrValue, err := ReadPcrTPM1(testWritePcrIndex)
	require.NoError(t, err)

	pcrValue := sha1.Sum([]byte(testString))
	finalPcr := sha1.Sum(append(oldPcrValue, pcrValue[:]...))

	err = MeasureTPM1(testWritePcrIndex, []byte(testString))
	require.NoError(t, err)

	newPcrValue, err := ReadPcrTPM1(testWritePcrIndex)
	require.NoError(t, err)

	require.Equal(t, finalPcr[:], newPcrValue)
}

func TestOwnerClearTPM1(t *testing.T) {
	err := OwnerClearTPM1("keins")
	require.NoError(t, err)
}

func TestTakeOwnershipTPM1(t *testing.T) {
	err := TakeOwnershipTPM1("", "")
	require.NoError(t, err)
}
*/
<commit_msg>Also tested negative case<commit_after>package tpm

// Manual testing needed or tpm hardware in VM
import (
	"errors"
	"io"
	"os"
	"testing"

	"github.com/stretchr/testify/require"
)

const (
	testReadPcrIndex  uint32 = 23
	testWritePcrIndex uint32 = 16
	testString        string = "teststring"
)

// Beware, TPM testa are a horrible idea because of state transitions
// for PCR. Powercycles are not possible due security architecture.
// Tests must be run as root and follow specific execution flow.
// Also do not run test on a production system if the tpm is used elsewhere.

func TestTPM1NewTPM(t *testing.T) {
	// TODO use a fake TPM. Unfortunately go-tpm checks if it's a device or a
	// socket, and fails otherwise. Need to use github.com/stefanberger/swtpm or
	// mock go-tpm entirely
	TPMOpener = func(string) (io.ReadWriteCloser, error) {
		fd, err := os.Open("tests/fake_tpm")
		if err != nil {
			return nil, err
		}
		return io.ReadWriteCloser(fd), nil
	}
	TpmCapabilities = "tests/fake_caps_tpm12"
	TpmOwnershipState = "tests/fake_owned_1"
	TpmActivatedState = "tests/fake_active_1"
	TpmEnabledState = "tests/fake_enabled_1"
	TpmTempDeactivatedState = "tests/fake_temp_deactivated_0"

	tpm, err := NewTPM()
	require.NoError(t, err)
	require.Equal(t, tpm12, tpm.Version())
}

func TestTPM1ReadPcr(t *testing.T) {
	tpm, err := NewTPM()
	tpm.(*TPM1).pcrReader = func(io.ReadWriter, uint32) ([]byte, error) {
		return make([]byte, 20), nil
	}
	require.NoError(t, err)
	pcrData, err := tpm.ReadPCR(testReadPcrIndex)
	require.NoError(t, err)
	require.Equal(t, pcrData, make([]byte, 20))
}

func TestTPM1ReadPcrError(t *testing.T) {
	tpm, err := NewTPM()
	tpm.(*TPM1).pcrReader = func(io.ReadWriter, uint32) ([]byte, error) {
		return nil, errors.New("Fake error")
	}
	require.NoError(t, err)
	_, err = tpm.ReadPCR(testReadPcrIndex)
	require.Error(t, err)
}

/*
func TestMeasureTPM1(t *testing.T) {
	oldPcrValue, err := ReadPcrTPM1(testWritePcrIndex)
	require.NoError(t, err)

	pcrValue := sha1.Sum([]byte(testString))
	finalPcr := sha1.Sum(append(oldPcrValue, pcrValue[:]...))

	err = MeasureTPM1(testWritePcrIndex, []byte(testString))
	require.NoError(t, err)

	newPcrValue, err := ReadPcrTPM1(testWritePcrIndex)
	require.NoError(t, err)

	require.Equal(t, finalPcr[:], newPcrValue)
}

func TestOwnerClearTPM1(t *testing.T) {
	err := OwnerClearTPM1("keins")
	require.NoError(t, err)
}

func TestTakeOwnershipTPM1(t *testing.T) {
	err := TakeOwnershipTPM1("", "")
	require.NoError(t, err)
}
*/
<|endoftext|>


<commit_before>/*
Copyright 2019 Google LLC.

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

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

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

package controllers

import (
	"context"

	"github.com/go-logr/logr"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	flinkoperatorv1alpha1 "github.com/googlecloudplatform/flink-operator/api/v1alpha1"
)

// FlinkSessionClusterReconciler reconciles a FlinkSessionCluster object
type FlinkSessionClusterReconciler struct {
	client.Client
	Log logr.Logger
}

// FlinkSessionClusterReconcileState holds the context and state for a
// reconcile request.
type _FlinkSessionClusterReconcileState struct {
	request             ctrl.Request
	context             context.Context
	log                 logr.Logger
	flinkSessionCluster flinkoperatorv1alpha1.FlinkSessionCluster
}

// +kubebuilder:rbac:groups=flinkoperator.k8s.io,resources=flinksessionclusters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=flinkoperator.k8s.io,resources=flinksessionclusters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services/status,verbs=get

func (reconciler *FlinkSessionClusterReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) {
	var context = context.Background()
	var log = reconciler.Log.WithValues("flinksessioncluster", request.NamespacedName)
	var state = _FlinkSessionClusterReconcileState{
		request:             request,
		context:             context,
		log:                 log,
		flinkSessionCluster: flinkoperatorv1alpha1.FlinkSessionCluster{},
	}
	var flinkSessionCluster = &state.flinkSessionCluster

	log.Info("-------- Start reconciling --------")

	// Get the FlinkSessionCluster resource.
	var err = reconciler.Get(context, request.NamespacedName, flinkSessionCluster)
	if err != nil {
		log.Info("Failed to get FlinkSessionCluster, it might have been deleted by the user", "error", err)
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}
	log.Info("Found FlinkSessionCluster", "resource", flinkSessionCluster)

	// Create or update JobManager deployment.
	var jobManagerDeployment appsv1.Deployment = reconciler.getDesiredJobManagerDeployment(&state)
	err = reconciler.createOrUpdateDeployment(&state, &jobManagerDeployment, "JobManager")
	if err != nil {
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

func (reconciler *FlinkSessionClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&flinkoperatorv1alpha1.FlinkSessionCluster{}).
		Owns(&appsv1.Deployment{}).
		Owns(&corev1.Service{}).
		Complete(reconciler)
}

func (reconciler *FlinkSessionClusterReconciler) createOrUpdateDeployment(
	reconcileState *_FlinkSessionClusterReconcileState,
	deployment *appsv1.Deployment,
	component string) error {
	var context = reconcileState.context
	var log = reconcileState.log.WithValues("component", component)
	var currentDeployment = appsv1.Deployment{}
	// Check if deployment already exists.
	var err = reconciler.Get(
		context,
		types.NamespacedName{
			Namespace: deployment.ObjectMeta.Namespace,
			Name:      deployment.Name,
		},
		&currentDeployment)
	if err != nil {
		err = reconciler.createDeployment(reconcileState, deployment, component)
	} else {
		log.Info("Deployment already exists", "deployment", currentDeployment)
		// TODO(dagang): compare and update if needed.
	}
	return err
}

func (reconciler *FlinkSessionClusterReconciler) createDeployment(
	reconcileState *_FlinkSessionClusterReconcileState,
	deployment *appsv1.Deployment,
	component string) error {
	var context = reconcileState.context
	var log = reconcileState.log.WithValues("component", component)
	log.Info("Creating deployment", "deployment", *deployment)
	var err = reconciler.Create(context, deployment)
	if err != nil {
		log.Info("Failed to create deployment", "error", err)
	} else {
		log.Info("Deployment created")
	}
	return err
}

func (reconciler *FlinkSessionClusterReconciler) updateManagerDeployment(
	reconcileState *_FlinkSessionClusterReconcileState,
	deployment *appsv1.Deployment,
	component string) error {
	var context = reconcileState.context
	var log = reconcileState.log.WithValues("component", component)
	log.Info("Updating deployment", "deployment", deployment)
	var err = reconciler.Update(context, deployment)
	if err != nil {
		log.Info("Failed to update deployment", "error", err)
	}
	return err
}

func toOwnerReference(flinkSessionCluster *flinkoperatorv1alpha1.FlinkSessionCluster) metav1.OwnerReference {
	return metav1.OwnerReference{
		APIVersion:         flinkSessionCluster.APIVersion,
		Kind:               flinkSessionCluster.Kind,
		Name:               flinkSessionCluster.Name,
		UID:                flinkSessionCluster.UID,
		Controller:         &[]bool{true}[0],
		BlockOwnerDeletion: &[]bool{false}[0],
	}
}

func (reconciler *FlinkSessionClusterReconciler) getDesiredJobManagerDeployment(
	reconcileState *_FlinkSessionClusterReconcileState) appsv1.Deployment {
	var request = reconcileState.request
	var flinkSessionCluster = reconcileState.flinkSessionCluster
	var imageSpec = flinkSessionCluster.Spec.ImageSpec
	var jobManagerSpec = flinkSessionCluster.Spec.JobManagerSpec
	var rpcPort = corev1.ContainerPort{Name: "rpc", ContainerPort: *jobManagerSpec.Ports.RPC}
	var blobPort = corev1.ContainerPort{Name: "blob", ContainerPort: *jobManagerSpec.Ports.Blob}
	var queryPort = corev1.ContainerPort{Name: "query", ContainerPort: *jobManagerSpec.Ports.QueryPort}
	var uiPort = corev1.ContainerPort{Name: "ui", ContainerPort: *jobManagerSpec.Ports.UI}
	var jobManagerDeploymentName = request.Name + "-jobmanager"
	var labels = map[string]string{
		"flink-cluster":   request.Name,
		"flink-component": "jobmanager",
	}
	var jobManagerDeployment = appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Namespace:       request.Namespace,
			Name:            jobManagerDeploymentName,
			OwnerReferences: []metav1.OwnerReference{toOwnerReference(&flinkSessionCluster)},
			Labels:          labels,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: jobManagerSpec.Replicas,
			Selector: &metav1.LabelSelector{MatchLabels: labels},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labels,
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						corev1.Container{
							Name:  "jobmanager",
							Image: *imageSpec.URI,
							Args:  []string{"jobmanager"},
							Ports: []corev1.ContainerPort{rpcPort, blobPort, queryPort, uiPort},
							Env:   []corev1.EnvVar{corev1.EnvVar{Name: "JOB_MANAGER_RPC_ADDRESS", Value: jobManagerDeploymentName}},
						},
					},
				},
			},
		},
	}
	return jobManagerDeployment
}

func (reconciler *FlinkSessionClusterReconciler) updateStatus(
	reconcileState *_FlinkSessionClusterReconcileState,
	status string) error {
	var flinkSessionCluster = flinkoperatorv1alpha1.FlinkSessionCluster{}
	reconcileState.flinkSessionCluster.DeepCopyInto(&flinkSessionCluster)
	flinkSessionCluster.Status = flinkoperatorv1alpha1.FlinkSessionClusterStatus{Status: status}
	return reconciler.Update(reconcileState.context, &flinkSessionCluster)
}
<commit_msg>Create or update JobManager service and TaskManager deployment in FlinkSessionCluster reconciler<commit_after>/*
Copyright 2019 Google LLC.

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

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

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

package controllers

import (
	"context"

	"github.com/go-logr/logr"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	flinkoperatorv1alpha1 "github.com/googlecloudplatform/flink-operator/api/v1alpha1"
)

// FlinkSessionClusterReconciler reconciles a FlinkSessionCluster object
type FlinkSessionClusterReconciler struct {
	client.Client
	Log logr.Logger
}

// FlinkSessionClusterReconcileState holds the context and state for a
// reconcile request.
type _FlinkSessionClusterReconcileState struct {
	request             ctrl.Request
	context             context.Context
	log                 logr.Logger
	flinkSessionCluster flinkoperatorv1alpha1.FlinkSessionCluster
}

// +kubebuilder:rbac:groups=flinkoperator.k8s.io,resources=flinksessionclusters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=flinkoperator.k8s.io,resources=flinksessionclusters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services/status,verbs=get

func (reconciler *FlinkSessionClusterReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) {
	var context = context.Background()
	var log = reconciler.Log.WithValues("flinksessioncluster", request.NamespacedName)
	var state = _FlinkSessionClusterReconcileState{
		request:             request,
		context:             context,
		log:                 log,
		flinkSessionCluster: flinkoperatorv1alpha1.FlinkSessionCluster{},
	}
	var flinkSessionCluster = &state.flinkSessionCluster

	log.Info("========== Start reconciling ==========")

	// Get the FlinkSessionCluster resource.
	var err = reconciler.Get(context, request.NamespacedName, flinkSessionCluster)
	if err != nil {
		log.Info("Failed to get FlinkSessionCluster, it might have been deleted by the user", "error", err)
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}
	log.Info("Found FlinkSessionCluster", "resource", flinkSessionCluster)

	// Create or update JobManager deployment.
	var jobManagerDeployment = reconciler.getDesiredJobManagerDeployment(&state)
	err = reconciler.createOrUpdateDeployment(&state, &jobManagerDeployment, "JobManager")
	if err != nil {
		return ctrl.Result{}, err
	}

	// Create or update JobManager service.
	var jobManagerService = reconciler.getDesiredJobManagerService(&state)
	err = reconciler.createOrUpdateService(&state, &jobManagerService, "JobManager")
	if err != nil {
		return ctrl.Result{}, err
	}

	// Create or update TaskManager deployment.
	var taskManagerDeployment = reconciler.getDesiredTaskManagerDeployment(&state)
	err = reconciler.createOrUpdateDeployment(&state, &taskManagerDeployment, "TaskManager")
	if err != nil {
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

func (reconciler *FlinkSessionClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&flinkoperatorv1alpha1.FlinkSessionCluster{}).
		Owns(&appsv1.Deployment{}).
		Owns(&corev1.Service{}).
		Complete(reconciler)
}

func (reconciler *FlinkSessionClusterReconciler) createOrUpdateDeployment(
	reconcileState *_FlinkSessionClusterReconcileState,
	deployment *appsv1.Deployment,
	component string) error {
	var context = reconcileState.context
	var log = reconcileState.log.WithValues("component", component)
	var currentDeployment = appsv1.Deployment{}
	// Check if deployment already exists.
	var err = reconciler.Get(
		context,
		types.NamespacedName{
			Namespace: deployment.ObjectMeta.Namespace,
			Name:      deployment.Name,
		},
		&currentDeployment)
	if err != nil {
		err = reconciler.createDeployment(reconcileState, deployment, component)
	} else {
		log.Info("Deployment already exists", "deployment", currentDeployment)
		// TODO(dagang): compare and update if needed.
	}
	return err
}

func (reconciler *FlinkSessionClusterReconciler) createDeployment(
	reconcileState *_FlinkSessionClusterReconcileState,
	deployment *appsv1.Deployment,
	component string) error {
	var context = reconcileState.context
	var log = reconcileState.log.WithValues("component", component)
	log.Info("Creating deployment", "deployment", *deployment)
	var err = reconciler.Create(context, deployment)
	if err != nil {
		log.Info("Failed to create deployment", "error", err)
	} else {
		log.Info("Deployment created")
	}
	return err
}

func (reconciler *FlinkSessionClusterReconciler) updateManagerDeployment(
	reconcileState *_FlinkSessionClusterReconcileState,
	deployment *appsv1.Deployment,
	component string) error {
	var context = reconcileState.context
	var log = reconcileState.log.WithValues("component", component)
	log.Info("Updating deployment", "deployment", deployment)
	var err = reconciler.Update(context, deployment)
	if err != nil {
		log.Info("Failed to update deployment", "error", err)
	}
	return err
}

func (reconciler *FlinkSessionClusterReconciler) createOrUpdateService(
	reconcileState *_FlinkSessionClusterReconcileState,
	service *corev1.Service,
	component string) error {
	var context = reconcileState.context
	var log = reconcileState.log.WithValues("component", component)
	var currentService = corev1.Service{}
	// Check if service already exists.
	var err = reconciler.Get(
		context,
		types.NamespacedName{
			Namespace: service.ObjectMeta.Namespace,
			Name:      service.Name,
		},
		&currentService)
	if err != nil {
		err = reconciler.createService(reconcileState, service, component)
	} else {
		log.Info("Service already exists", "resource", currentService)
		// TODO(dagang): compare and update if needed.
	}
	return err
}

func (reconciler *FlinkSessionClusterReconciler) createService(
	reconcileState *_FlinkSessionClusterReconcileState,
	service *corev1.Service,
	component string) error {
	var context = reconcileState.context
	var log = reconcileState.log.WithValues("component", component)
	log.Info("Creating service", "resource", *service)
	var err = reconciler.Create(context, service)
	if err != nil {
		log.Info("Failed to create service", "error", err)
	} else {
		log.Info("Service created")
	}
	return err
}

func toOwnerReference(flinkSessionCluster *flinkoperatorv1alpha1.FlinkSessionCluster) metav1.OwnerReference {
	return metav1.OwnerReference{
		APIVersion:         flinkSessionCluster.APIVersion,
		Kind:               flinkSessionCluster.Kind,
		Name:               flinkSessionCluster.Name,
		UID:                flinkSessionCluster.UID,
		Controller:         &[]bool{true}[0],
		BlockOwnerDeletion: &[]bool{false}[0],
	}
}

func (reconciler *FlinkSessionClusterReconciler) getDesiredJobManagerDeployment(
	reconcileState *_FlinkSessionClusterReconcileState) appsv1.Deployment {
	var request = reconcileState.request
	var flinkSessionCluster = reconcileState.flinkSessionCluster
	var imageSpec = flinkSessionCluster.Spec.ImageSpec
	var jobManagerSpec = flinkSessionCluster.Spec.JobManagerSpec
	var rpcPort = corev1.ContainerPort{Name: "rpc", ContainerPort: *jobManagerSpec.Ports.RPC}
	var blobPort = corev1.ContainerPort{Name: "blob", ContainerPort: *jobManagerSpec.Ports.Blob}
	var queryPort = corev1.ContainerPort{Name: "query", ContainerPort: *jobManagerSpec.Ports.QueryPort}
	var uiPort = corev1.ContainerPort{Name: "ui", ContainerPort: *jobManagerSpec.Ports.UI}
	var jobManagerDeploymentName = request.Name + "-jobmanager"
	var labels = map[string]string{
		"cluster":   request.Name,
		"app":       "flink",
		"component": "jobmanager",
	}
	var jobManagerDeployment = appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Namespace:       request.Namespace,
			Name:            jobManagerDeploymentName,
			OwnerReferences: []metav1.OwnerReference{toOwnerReference(&flinkSessionCluster)},
			Labels:          labels,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: jobManagerSpec.Replicas,
			Selector: &metav1.LabelSelector{MatchLabels: labels},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labels,
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						corev1.Container{
							Name:  "jobmanager",
							Image: *imageSpec.URI,
							Args:  []string{"jobmanager"},
							Ports: []corev1.ContainerPort{rpcPort, blobPort, queryPort, uiPort},
							Env:   []corev1.EnvVar{corev1.EnvVar{Name: "JOB_MANAGER_RPC_ADDRESS", Value: jobManagerDeploymentName}},
						},
					},
				},
			},
		},
	}
	return jobManagerDeployment
}

func (reconciler *FlinkSessionClusterReconciler) getDesiredJobManagerService(
	reconcileState *_FlinkSessionClusterReconcileState) corev1.Service {
	var request = reconcileState.request
	var flinkSessionCluster = reconcileState.flinkSessionCluster
	var jobManagerSpec = flinkSessionCluster.Spec.JobManagerSpec
	var rpcPort = corev1.ServicePort{
		Name:       "rpc",
		Port:       *jobManagerSpec.Ports.RPC,
		TargetPort: intstr.FromString("rpc")}
	var blobPort = corev1.ServicePort{
		Name:       "blob",
		Port:       *jobManagerSpec.Ports.Blob,
		TargetPort: intstr.FromString("blob")}
	var queryPort = corev1.ServicePort{
		Name:       "query",
		Port:       *jobManagerSpec.Ports.QueryPort,
		TargetPort: intstr.FromString("query")}
	var uiPort = corev1.ServicePort{
		Name:       "ui",
		Port:       *jobManagerSpec.Ports.UI,
		TargetPort: intstr.FromString("ui")}
	var jobManagerServiceName = request.Name + "-jobmanager"
	var labels = map[string]string{
		"cluster":   request.Name,
		"app":       "flink",
		"component": "jobmanager",
	}
	var jobManagerService = corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Namespace:       request.Namespace,
			Name:            jobManagerServiceName,
			OwnerReferences: []metav1.OwnerReference{toOwnerReference(&flinkSessionCluster)},
			Labels:          labels,
		},
		Spec: corev1.ServiceSpec{
			Selector: labels,
			Ports:    []corev1.ServicePort{rpcPort, blobPort, queryPort, uiPort},
		},
	}
	return jobManagerService
}

func (reconciler *FlinkSessionClusterReconciler) getDesiredTaskManagerDeployment(
	reconcileState *_FlinkSessionClusterReconcileState) appsv1.Deployment {
	var request = reconcileState.request
	var flinkSessionCluster = reconcileState.flinkSessionCluster
	var imageSpec = flinkSessionCluster.Spec.ImageSpec
	var taskManagerSpec = flinkSessionCluster.Spec.TaskManagerSpec
	var dataPort = corev1.ContainerPort{Name: "data", ContainerPort: *taskManagerSpec.Ports.Data}
	var rpcPort = corev1.ContainerPort{Name: "rpc", ContainerPort: *taskManagerSpec.Ports.RPC}
	var queryPort = corev1.ContainerPort{Name: "query", ContainerPort: *taskManagerSpec.Ports.Query}
	var taskManagerDeploymentName = request.Name + "-taskmanager"
	var jobManagerDeploymentName = request.Name + "-jobmanager"
	var labels = map[string]string{
		"cluster":   request.Name,
		"app":       "flink",
		"component": "taskmanager",
	}
	var taskManagerDeployment = appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Namespace:       request.Namespace,
			Name:            taskManagerDeploymentName,
			OwnerReferences: []metav1.OwnerReference{toOwnerReference(&flinkSessionCluster)},
			Labels:          labels,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: taskManagerSpec.Replicas,
			Selector: &metav1.LabelSelector{MatchLabels: labels},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labels,
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						corev1.Container{
							Name:  "taskmanager",
							Image: *imageSpec.URI,
							Args:  []string{"taskmanager"},
							Ports: []corev1.ContainerPort{dataPort, rpcPort, queryPort},
							Env:   []corev1.EnvVar{corev1.EnvVar{Name: "JOB_MANAGER_RPC_ADDRESS", Value: jobManagerDeploymentName}},
						},
					},
				},
			},
		},
	}
	return taskManagerDeployment
}

func (reconciler *FlinkSessionClusterReconciler) updateStatus(
	reconcileState *_FlinkSessionClusterReconcileState,
	status string) error {
	var flinkSessionCluster = flinkoperatorv1alpha1.FlinkSessionCluster{}
	reconcileState.flinkSessionCluster.DeepCopyInto(&flinkSessionCluster)
	flinkSessionCluster.Status = flinkoperatorv1alpha1.FlinkSessionClusterStatus{Status: status}
	return reconciler.Update(reconcileState.context, &flinkSessionCluster)
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strconv"
	"time"

	"github.com/Sirupsen/logrus"
	"github.com/fsouza/go-dockerclient"
	"github.com/garyburd/redigo/redis"
)

// DockerClient for running code
var DockerClient *docker.Client

// DockerAPIVersion is the API version connect to docker
const DockerAPIVersion = "1.23"

// Runner runs the code
type Runner struct {
	Lang          string `json:"lang"`
	Source        string `json:"source"`
	Version       string `json:"version"`
	Timeout       int    `json:"timeout"` // How long is the code going to run
	closeNotifier <-chan bool
	logger        *logrus.Logger
	containerID   string
}

// Runnerthrottle Limit the max throttle for runner
var Runnerthrottle chan struct{}

// FetchCode get the code from Redis Server according to the UUID
func FetchCode(uuid string, redisConn redis.Conn) (r *Runner, err error) {
	value, err := redis.Bytes(redisConn.Do("GET", uuid+"#run"))

	if err != nil {
		return
	}

	r = &Runner{}
	err = json.Unmarshal(value, r)
	return
}

// Run the code in the container
func (r *Runner) Run(output messages, conn redis.Conn, uuid string) {
	Runnerthrottle <- struct{}{}
	defer func() { <-Runnerthrottle }()

	err := r.createContainer(uuid)
	if err != nil {
		r.logger.Errorf("Container %s cannot be created - %v", uuid, err)
		return
	}

	stdoutReader, stdoutWriter := io.Pipe()
	stdinReader, stdinWriter := io.Pipe()

	if err != nil {
		r.logger.Error(err)
	}

	defer stdinWriter.Close()
	defer stdoutWriter.Close()

	go pipeStdin(conn, uuid, stdinWriter, r.logger)
	go pipeStdout(stdoutReader, output, r.logger)

	// Start running the container
	err = r.startContainer()
	if err != nil {
		r.logger.Errorf("Container %s cannot be started - %v", uuid, err)
		return
	}
	defer DockerClient.RemoveContainer(docker.RemoveContainerOptions{ID: r.containerID, Force: true})

	successChan := make(chan struct{})
	errorChan := make(chan error)

	go r.waitContainer(successChan, errorChan)

	go func(r *Runner, uuid string) {
		err = r.attachContainer(stdoutWriter, stdinReader)
		if err != nil {
			r.logger.Errorf("Container %s cannot be attached - %v", uuid, err)
		}
	}(r, uuid)

	select {
	case <-r.closeNotifier:
		DockerClient.StopContainer(r.containerID, 0)
		r.logger.Infof("Container %s is stopped since the streamming has been halted", uuid)
	case <-successChan:
		r.logger.Infof("Container %s is executed successfully", uuid)
	case err := <-errorChan:
		r.logger.Errorf("Container %s failed caused by - %v", uuid, err)
	case <-time.After(time.Duration(r.Timeout) * time.Second):
		msg := fmt.Sprintf("Container %s is terminated caused by 15 sec timeout\n", uuid)
		r.logger.Error(msg)
		output <- msg
	}
}

func pipeStdin(conn redis.Conn, uuid string, stdin *io.PipeWriter, logger *logrus.Logger) {
	psc := redis.PubSubConn{Conn: conn}
	psc.Subscribe(uuid + "#stdin")

	defer func() {
		psc.Unsubscribe(uuid + "#stdin")
		psc.Close()
		conn.Close()
	}()

StdinSubscriptionLoop:
	for {
		switch n := psc.Receive().(type) {
		case redis.Message:
			stdinData := strconv.QuoteToASCII(string(n.Data))
			logger.Infof("Message: %s %s", n.Channel, stdinData)
			stdin.Write(n.Data)
		case error:
			break StdinSubscriptionLoop
		}
	}
	logger.Info("Stdin subscription closed")
}

func pipeStdout(stdout *io.PipeReader, output messages, logger *logrus.Logger) {
	buffer := make([]byte, 512)
	for {
		n, err := stdout.Read(buffer)
		if err != nil {
			if err != io.EOF {
				stdout.Close()
				logger.Error(err)
			}

			close(output)
			break
		}

		data := buffer[0:n]
		output <- string(data)

		// Clear the buffer
		for i := 0; i < n; i++ {
			buffer[i] = 0
		}
	}
}

// NewDockerClient create a new docker client
func NewDockerClient() (*docker.Client, error) {
	dockerHost := os.Getenv("DOCKER_HOST")

	// If DOCKER_HOST exists in env, using docker-machine
	if dockerHost != "" {
		return docker.NewVersionedClientFromEnv(DockerAPIVersion)
	}

	// Otherwise using sock connection
	//TODO: Deal with the TLS case (even though you are not using it for now)
	endpoint := "unix:///var/run/docker.sock"
	return docker.NewVersionedClient(endpoint, DockerAPIVersion)
}

var imageMapper = map[string]string{
	"swift":  "koderunr-swift",
	"ruby":   "koderunr-ruby",
	"python": "koderunr-python",
	"go":     "koderunr-go",
	"c":      "koderunr-c",
	"elixir": "koderunr-erl",
}

func (r *Runner) image() string {
	return imageMapper[r.Lang]
}

func (r *Runner) createContainer(uuid string) error {
	cmd := []string{r.Source, uuid}

	if r.Version != "" {
		cmd = append(cmd, r.Version)
	}
	container, err := DockerClient.CreateContainer(docker.CreateContainerOptions{
		Name: uuid,
		Config: &docker.Config{
			Image:           r.image(),
			NetworkDisabled: true,
			OpenStdin:       true,
			Cmd:             cmd,
			KernelMemory:    1024 * 1024 * 4,
			PidsLimit:       100,
		},
	})

	if err != nil {
		return err
	}

	r.containerID = container.ID
	return nil
}

func (r *Runner) startContainer() error {
	return DockerClient.StartContainer(r.containerID, &docker.HostConfig{
		CPUQuota:   20000,
		MemorySwap: -1,
		Privileged: false,
		CapDrop:    []string{"all"},
		Memory:     80 * 1024 * 1024, // so the memory swap will be the same size
	})
}

func (r *Runner) attachContainer(stdoutWriter *io.PipeWriter, stdinReader *io.PipeReader) error {
	return DockerClient.AttachToContainer(docker.AttachToContainerOptions{
		Container:    r.containerID,
		Stdin:        true,
		Stdout:       true,
		Stderr:       true,
		Stream:       true,
		OutputStream: stdoutWriter,
		ErrorStream:  stdoutWriter,
		InputStream:  stdinReader,
	})
}

func (r *Runner) waitContainer(successChan chan<- struct{}, errorChan chan<- error) {
	_, err := DockerClient.WaitContainer(r.containerID)
	if err != nil {
		r.logger.Error(err)
		errorChan <- err
		return
	}
	successChan <- struct{}{}
}
<commit_msg>upgrade go-dockercient<commit_after>package main

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strconv"
	"time"

	"github.com/Sirupsen/logrus"
	"github.com/fsouza/go-dockerclient"
	"github.com/garyburd/redigo/redis"
)

// DockerClient for running code
var DockerClient *docker.Client

// DockerAPIVersion is the API version connect to docker
const DockerAPIVersion = "1.23"

// Runner runs the code
type Runner struct {
	Lang          string `json:"lang"`
	Source        string `json:"source"`
	Version       string `json:"version"`
	Timeout       int    `json:"timeout"` // How long is the code going to run
	closeNotifier <-chan bool
	logger        *logrus.Logger
	containerID   string
}

// Runnerthrottle Limit the max throttle for runner
var Runnerthrottle chan struct{}

// FetchCode get the code from Redis Server according to the UUID
func FetchCode(uuid string, redisConn redis.Conn) (r *Runner, err error) {
	value, err := redis.Bytes(redisConn.Do("GET", uuid+"#run"))

	if err != nil {
		return
	}

	r = &Runner{}
	err = json.Unmarshal(value, r)
	return
}

// Run the code in the container
func (r *Runner) Run(output messages, conn redis.Conn, uuid string) {
	Runnerthrottle <- struct{}{}
	defer func() { <-Runnerthrottle }()

	err := r.createContainer(uuid)
	if err != nil {
		r.logger.Errorf("Container %s cannot be created - %v", uuid, err)
		return
	}

	stdoutReader, stdoutWriter := io.Pipe()
	stdinReader, stdinWriter := io.Pipe()

	if err != nil {
		r.logger.Error(err)
	}

	defer stdinWriter.Close()
	defer stdoutWriter.Close()

	go pipeStdin(conn, uuid, stdinWriter, r.logger)
	go pipeStdout(stdoutReader, output, r.logger)

	// Start running the container
	err = r.startContainer()
	if err != nil {
		r.logger.Errorf("Container %s cannot be started - %v", uuid, err)
		return
	}
	defer DockerClient.RemoveContainer(docker.RemoveContainerOptions{ID: r.containerID, Force: true})

	successChan := make(chan struct{})
	errorChan := make(chan error)

	go r.waitContainer(successChan, errorChan)

	go func(r *Runner, uuid string) {
		err = r.attachContainer(stdoutWriter, stdinReader)
		if err != nil {
			r.logger.Errorf("Container %s cannot be attached - %v", uuid, err)
		}
	}(r, uuid)

	select {
	case <-r.closeNotifier:
		DockerClient.StopContainer(r.containerID, 0)
		r.logger.Infof("Container %s is stopped since the streamming has been halted", uuid)
	case <-successChan:
		r.logger.Infof("Container %s is executed successfully", uuid)
	case err := <-errorChan:
		r.logger.Errorf("Container %s failed caused by - %v", uuid, err)
	case <-time.After(time.Duration(r.Timeout) * time.Second):
		msg := fmt.Sprintf("Container %s is terminated caused by 15 sec timeout\n", uuid)
		r.logger.Error(msg)
		output <- msg
	}
}

func pipeStdin(conn redis.Conn, uuid string, stdin *io.PipeWriter, logger *logrus.Logger) {
	psc := redis.PubSubConn{Conn: conn}
	psc.Subscribe(uuid + "#stdin")

	defer func() {
		psc.Unsubscribe(uuid + "#stdin")
		psc.Close()
		conn.Close()
	}()

StdinSubscriptionLoop:
	for {
		switch n := psc.Receive().(type) {
		case redis.Message:
			stdinData := strconv.QuoteToASCII(string(n.Data))
			logger.Infof("Message: %s %s", n.Channel, stdinData)
			stdin.Write(n.Data)
		case error:
			break StdinSubscriptionLoop
		}
	}
	logger.Info("Stdin subscription closed")
}

func pipeStdout(stdout *io.PipeReader, output messages, logger *logrus.Logger) {
	buffer := make([]byte, 512)
	for {
		n, err := stdout.Read(buffer)
		if err != nil {
			if err != io.EOF {
				stdout.Close()
				logger.Error(err)
			}

			close(output)
			break
		}

		data := buffer[0:n]
		output <- string(data)

		// Clear the buffer
		for i := 0; i < n; i++ {
			buffer[i] = 0
		}
	}
}

// NewDockerClient create a new docker client
func NewDockerClient() (*docker.Client, error) {
	dockerHost := os.Getenv("DOCKER_HOST")

	// If DOCKER_HOST exists in env, using docker-machine
	if dockerHost != "" {
		return docker.NewVersionedClientFromEnv(DockerAPIVersion)
	}

	// Otherwise using sock connection
	//TODO: Deal with the TLS case (even though you are not using it for now)
	endpoint := "unix:///var/run/docker.sock"
	return docker.NewVersionedClient(endpoint, DockerAPIVersion)
}

var imageMapper = map[string]string{
	"swift":  "koderunr-swift",
	"ruby":   "koderunr-ruby",
	"python": "koderunr-python",
	"go":     "koderunr-go",
	"c":      "koderunr-c",
	"elixir": "koderunr-erl",
}

func (r *Runner) image() string {
	return imageMapper[r.Lang]
}

func (r *Runner) createContainer(uuid string) error {
	cmd := []string{r.Source, uuid}

	if r.Version != "" {
		cmd = append(cmd, r.Version)
	}
	container, err := DockerClient.CreateContainer(docker.CreateContainerOptions{
		Name: uuid,
		Config: &docker.Config{
			Image:           r.image(),
			NetworkDisabled: true,
			OpenStdin:       true,
			Cmd:             cmd,
			KernelMemory:    1024 * 1024 * 4,
		},
	})

	if err != nil {
		return err
	}

	r.containerID = container.ID
	return nil
}

func (r *Runner) startContainer() error {
	return DockerClient.StartContainer(r.containerID, &docker.HostConfig{
		CPUQuota:   20000,
		MemorySwap: -1,
		Privileged: false,
		CapDrop:    []string{"all"},
		Memory:     80 * 1024 * 1024, // so the memory swap will be the same size
		PidsLimit:  100,
	})
}

func (r *Runner) attachContainer(stdoutWriter *io.PipeWriter, stdinReader *io.PipeReader) error {
	return DockerClient.AttachToContainer(docker.AttachToContainerOptions{
		Container:    r.containerID,
		Stdin:        true,
		Stdout:       true,
		Stderr:       true,
		Stream:       true,
		OutputStream: stdoutWriter,
		ErrorStream:  stdoutWriter,
		InputStream:  stdinReader,
	})
}

func (r *Runner) waitContainer(successChan chan<- struct{}, errorChan chan<- error) {
	_, err := DockerClient.WaitContainer(r.containerID)
	if err != nil {
		r.logger.Error(err)
		errorChan <- err
		return
	}
	successChan <- struct{}{}
}
<|endoftext|>


<commit_before>package server

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"os"
	"runtime/debug"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/go-playground/validator"

	"github.com/lukaszbudnik/migrator/common"
	"github.com/lukaszbudnik/migrator/config"
	"github.com/lukaszbudnik/migrator/coordinator"
	"github.com/lukaszbudnik/migrator/types"
)

const (
	defaultPort     string = "8080"
	requestIDHeader string = "X-Request-Id"
)

type migrationsPostRequest struct {
	Response types.MigrationsResponseType `json:"response" binding:"required,response" validate:"response"`
	Mode     types.MigrationsModeType     `json:"mode" binding:"required" validate:"mode"`
}

type tenantsPostRequest struct {
	Name string `json:"name" binding:"required"`
	migrationsPostRequest
}

type migrationsSuccessResponse struct {
	Results           *types.MigrationResults `json:"results"`
	AppliedMigrations []types.Migration       `json:"appliedMigrations,omitempty"`
}

type errorResponse struct {
	ErrorMessage string      `json:"error"`
	Details      interface{} `json:"details,omitempty"`
}

// GetPort gets the port from config or defaultPort
func GetPort(config *config.Config) string {
	if len(strings.TrimSpace(config.Port)) == 0 {
		return defaultPort
	}
	return config.Port
}

func requestIDHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		requestID := c.Request.Header.Get(requestIDHeader)
		if requestID == "" {
			requestID = fmt.Sprintf("%d", time.Now().UnixNano())
		}
		ctx := context.WithValue(c.Request.Context(), common.RequestIDKey{}, requestID)
		c.Request = c.Request.WithContext(ctx)
		c.Next()
	}
}

func recovery() gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				// If the connection is dead, we can't write a status to it.
				if brokenPipe {
					common.LogPanic(c.Request.Context(), "Broken pipe: %v", err)
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
				} else {
					common.LogPanic(c.Request.Context(), "Panic recovered: %v", err)
					if gin.IsDebugging() {
						debug.PrintStack()
					}
					c.AbortWithStatusJSON(http.StatusInternalServerError, &errorResponse{err.(string), nil})
				}
			}
		}()
		c.Next()
	}
}

func requestLoggerHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		common.LogInfo(c.Request.Context(), "clientIP=%v method=%v request=%v", c.ClientIP(), c.Request.Method, c.Request.URL.RequestURI())
		c.Next()
	}
}

func makeHandler(config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator, handler func(*gin.Context, *config.Config, func(context.Context, *config.Config) coordinator.Coordinator)) gin.HandlerFunc {
	return func(c *gin.Context) {
		handler(c, config, newCoordinator)
	}
}

func configHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	c.YAML(200, config)
}

func migrationsSourceHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	coordinator := newCoordinator(c.Request.Context(), config)
	defer coordinator.Dispose()
	migrations := coordinator.GetSourceMigrations()
	common.LogInfo(c.Request.Context(), "Returning source migrations: %v", len(migrations))
	c.JSON(http.StatusOK, migrations)
}

func migrationsAppliedHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	coordinator := newCoordinator(c.Request.Context(), config)
	defer coordinator.Dispose()
	dbMigrations := coordinator.GetAppliedMigrations()
	common.LogInfo(c.Request.Context(), "Returning applied migrations: %v", len(dbMigrations))
	c.JSON(http.StatusOK, dbMigrations)
}

func migrationsPostHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	var request migrationsPostRequest

	if err := c.ShouldBindJSON(&request); err != nil {
		common.LogError(c.Request.Context(), "Error reading request: %v", err.Error())
		c.AbortWithStatusJSON(http.StatusBadRequest, errorResponse{"Invalid request, please see documentation for valid JSON payload", nil})
		return
	}

	coordinator := newCoordinator(c.Request.Context(), config)
	defer coordinator.Dispose()

	if ok, offendingMigrations := coordinator.VerifySourceMigrationsCheckSums(); !ok {
		common.LogError(c.Request.Context(), "Checksum verification failed for migrations: %v", len(offendingMigrations))
		c.AbortWithStatusJSON(http.StatusFailedDependency, errorResponse{"Checksum verification failed. Please review offending migrations.", offendingMigrations})
		return
	}

	results, appliedMigrations := coordinator.ApplyMigrations()

	common.LogInfo(c.Request.Context(), "Returning applied migrations: %v", len(appliedMigrations))

	c.JSON(http.StatusOK, migrationsSuccessResponse{results, appliedMigrations})
}

func tenantsGetHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	coordinator := newCoordinator(c.Request.Context(), config)
	defer coordinator.Dispose()
	tenants := coordinator.GetTenants()
	common.LogInfo(c.Request.Context(), "Returning tenants: %v", len(tenants))
	c.JSON(http.StatusOK, tenants)
}

func tenantsPostHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	var tenant tenantsPostRequest
	err := c.ShouldBindJSON(&tenant)
	if err != nil {
		common.LogError(c.Request.Context(), "Bad request: %v", err.Error())
		c.AbortWithStatusJSON(http.StatusBadRequest, errorResponse{"Invalid request, please see documentation for valid JSON payload", nil})
		return
	}

	coordinator := newCoordinator(c.Request.Context(), config)
	defer coordinator.Dispose()

	if ok, offendingMigrations := coordinator.VerifySourceMigrationsCheckSums(); !ok {
		common.LogError(c.Request.Context(), "Checksum verification failed for migrations: %v", len(offendingMigrations))
		c.AbortWithStatusJSON(http.StatusFailedDependency, errorResponse{"Checksum verification failed. Please review offending migrations.", offendingMigrations})
		return
	}

	results, appliedMigrations := coordinator.AddTenantAndApplyMigrations(tenant.Name)

	text := fmt.Sprintf("Tenant %v added, migrations applied: %v", tenant.Name, len(appliedMigrations))

	common.LogInfo(c.Request.Context(), text)
	c.JSON(http.StatusOK, migrationsSuccessResponse{results, appliedMigrations})
}

// SetupRouter setups router
func SetupRouter(config *config.Config, newCoordinator func(ctx context.Context, config *config.Config) coordinator.Coordinator) *gin.Engine {
	r := gin.New()
	r.HandleMethodNotAllowed = true
	r.Use(recovery(), requestIDHandler(), requestLoggerHandler())

	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterValidation("response", types.ValidateMigrationsResponseType)
		v.RegisterValidation("mode", types.ValidateMigrationsModeType)
	}

	v1 := r.Group("/v1")

	v1.GET("/config", makeHandler(config, newCoordinator, configHandler))

	v1.GET("/tenants", makeHandler(config, newCoordinator, tenantsGetHandler))
	v1.POST("/tenants", makeHandler(config, newCoordinator, tenantsPostHandler))

	v1.GET("/migrations/source", makeHandler(config, newCoordinator, migrationsSourceHandler))
	v1.GET("/migrations/applied", makeHandler(config, newCoordinator, migrationsAppliedHandler))
	v1.POST("/migrations", makeHandler(config, newCoordinator, migrationsPostHandler))

	return r
}
<commit_msg>fixed typos<commit_after>package server

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"os"
	"runtime/debug"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/go-playground/validator"

	"github.com/lukaszbudnik/migrator/common"
	"github.com/lukaszbudnik/migrator/config"
	"github.com/lukaszbudnik/migrator/coordinator"
	"github.com/lukaszbudnik/migrator/types"
)

const (
	defaultPort     string = "8080"
	requestIDHeader string = "X-Request-Id"
)

type migrationsPostRequest struct {
	Response types.MigrationsResponseType `json:"response" binding:"required,response"`
	Mode     types.MigrationsModeType     `json:"mode" binding:"required,mode"`
}

type tenantsPostRequest struct {
	Name string `json:"name" binding:"required"`
	migrationsPostRequest
}

type migrationsSuccessResponse struct {
	Results           *types.MigrationResults `json:"results"`
	AppliedMigrations []types.Migration       `json:"appliedMigrations,omitempty"`
}

type errorResponse struct {
	ErrorMessage string      `json:"error"`
	Details      interface{} `json:"details,omitempty"`
}

// GetPort gets the port from config or defaultPort
func GetPort(config *config.Config) string {
	if len(strings.TrimSpace(config.Port)) == 0 {
		return defaultPort
	}
	return config.Port
}

func requestIDHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		requestID := c.Request.Header.Get(requestIDHeader)
		if requestID == "" {
			requestID = fmt.Sprintf("%d", time.Now().UnixNano())
		}
		ctx := context.WithValue(c.Request.Context(), common.RequestIDKey{}, requestID)
		c.Request = c.Request.WithContext(ctx)
		c.Next()
	}
}

func recovery() gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				// If the connection is dead, we can't write a status to it.
				if brokenPipe {
					common.LogPanic(c.Request.Context(), "Broken pipe: %v", err)
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
				} else {
					common.LogPanic(c.Request.Context(), "Panic recovered: %v", err)
					if gin.IsDebugging() {
						debug.PrintStack()
					}
					c.AbortWithStatusJSON(http.StatusInternalServerError, &errorResponse{err.(string), nil})
				}
			}
		}()
		c.Next()
	}
}

func requestLoggerHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		common.LogInfo(c.Request.Context(), "clientIP=%v method=%v request=%v", c.ClientIP(), c.Request.Method, c.Request.URL.RequestURI())
		c.Next()
	}
}

func makeHandler(config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator, handler func(*gin.Context, *config.Config, func(context.Context, *config.Config) coordinator.Coordinator)) gin.HandlerFunc {
	return func(c *gin.Context) {
		handler(c, config, newCoordinator)
	}
}

func configHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	c.YAML(200, config)
}

func migrationsSourceHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	coordinator := newCoordinator(c.Request.Context(), config)
	defer coordinator.Dispose()
	migrations := coordinator.GetSourceMigrations()
	common.LogInfo(c.Request.Context(), "Returning source migrations: %v", len(migrations))
	c.JSON(http.StatusOK, migrations)
}

func migrationsAppliedHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	coordinator := newCoordinator(c.Request.Context(), config)
	defer coordinator.Dispose()
	dbMigrations := coordinator.GetAppliedMigrations()
	common.LogInfo(c.Request.Context(), "Returning applied migrations: %v", len(dbMigrations))
	c.JSON(http.StatusOK, dbMigrations)
}

func migrationsPostHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	var request migrationsPostRequest

	if err := c.ShouldBindJSON(&request); err != nil {
		common.LogError(c.Request.Context(), "Error reading request: %v", err.Error())
		c.AbortWithStatusJSON(http.StatusBadRequest, errorResponse{"Invalid request, please see documentation for valid JSON payload", nil})
		return
	}

	coordinator := newCoordinator(c.Request.Context(), config)
	defer coordinator.Dispose()

	if ok, offendingMigrations := coordinator.VerifySourceMigrationsCheckSums(); !ok {
		common.LogError(c.Request.Context(), "Checksum verification failed for migrations: %v", len(offendingMigrations))
		c.AbortWithStatusJSON(http.StatusFailedDependency, errorResponse{"Checksum verification failed. Please review offending migrations.", offendingMigrations})
		return
	}

	results, appliedMigrations := coordinator.ApplyMigrations()

	common.LogInfo(c.Request.Context(), "Returning applied migrations: %v", len(appliedMigrations))

	c.JSON(http.StatusOK, migrationsSuccessResponse{results, appliedMigrations})
}

func tenantsGetHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	coordinator := newCoordinator(c.Request.Context(), config)
	defer coordinator.Dispose()
	tenants := coordinator.GetTenants()
	common.LogInfo(c.Request.Context(), "Returning tenants: %v", len(tenants))
	c.JSON(http.StatusOK, tenants)
}

func tenantsPostHandler(c *gin.Context, config *config.Config, newCoordinator func(context.Context, *config.Config) coordinator.Coordinator) {
	var tenant tenantsPostRequest
	err := c.ShouldBindJSON(&tenant)
	if err != nil {
		common.LogError(c.Request.Context(), "Bad request: %v", err.Error())
		c.AbortWithStatusJSON(http.StatusBadRequest, errorResponse{"Invalid request, please see documentation for valid JSON payload", nil})
		return
	}

	coordinator := newCoordinator(c.Request.Context(), config)
	defer coordinator.Dispose()

	if ok, offendingMigrations := coordinator.VerifySourceMigrationsCheckSums(); !ok {
		common.LogError(c.Request.Context(), "Checksum verification failed for migrations: %v", len(offendingMigrations))
		c.AbortWithStatusJSON(http.StatusFailedDependency, errorResponse{"Checksum verification failed. Please review offending migrations.", offendingMigrations})
		return
	}

	results, appliedMigrations := coordinator.AddTenantAndApplyMigrations(tenant.Name)

	text := fmt.Sprintf("Tenant %v added, migrations applied: %v", tenant.Name, len(appliedMigrations))

	common.LogInfo(c.Request.Context(), text)
	c.JSON(http.StatusOK, migrationsSuccessResponse{results, appliedMigrations})
}

// SetupRouter setups router
func SetupRouter(config *config.Config, newCoordinator func(ctx context.Context, config *config.Config) coordinator.Coordinator) *gin.Engine {
	r := gin.New()
	r.HandleMethodNotAllowed = true
	r.Use(recovery(), requestIDHandler(), requestLoggerHandler())

	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterValidation("response", types.ValidateMigrationsResponseType)
		v.RegisterValidation("mode", types.ValidateMigrationsModeType)
	}

	v1 := r.Group("/v1")

	v1.GET("/config", makeHandler(config, newCoordinator, configHandler))

	v1.GET("/tenants", makeHandler(config, newCoordinator, tenantsGetHandler))
	v1.POST("/tenants", makeHandler(config, newCoordinator, tenantsPostHandler))

	v1.GET("/migrations/source", makeHandler(config, newCoordinator, migrationsSourceHandler))
	v1.GET("/migrations/applied", makeHandler(config, newCoordinator, migrationsAppliedHandler))
	v1.POST("/migrations", makeHandler(config, newCoordinator, migrationsPostHandler))

	return r
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"io"

	"log"
	"net/http"
	"sync"

	"github.com/Leimy/rx-go/bot"
	"github.com/Leimy/rx-go/nowplaying"
	"github.com/Leimy/rx-go/twit"
)

var nowPlaying *nowplaying.NowPlaying
var botFrom chan string
var botTo chan string
var tweeter twit.Tweeter

// just some counters
var stats struct {
	sync.RWMutex
	botRestarts uint64
}

func incBotRestarts() {
	stats.Lock()
	defer stats.Unlock()
	stats.botRestarts++
}

func getBotRestarts() uint64 {
	stats.RLock()
	defer stats.RUnlock()
	return stats.botRestarts
}

// Set up the metadata handler
func handleMeta() {
	http.HandleFunc("/metadata", func(w http.ResponseWriter, r *http.Request) {
		switch method := r.Method; method {
		case "GET":
			curSong := ""
			for curSong == "" {
				curSong = nowPlaying.Get()
			}
			fmt.Fprintf(w, "Now Playing: %s", curSong)
		default:
			log.Printf("%s Not implemented", method)
		}
	})
}

// Call only once per uri
func handleTwit(uri string, tweeter twit.Tweeter) {
	var message = make([]byte, 160)
	http.HandleFunc(uri, func(w http.ResponseWriter, r *http.Request) {
		switch method := r.Method; method {
		case "PUT", "POST":
			defer r.Body.Close()
			if _, err := r.Body.Read(message); err != nil && err != io.EOF {
				log.Printf("Failed to read request: %s %v", message, err)
			} else {
				log.Printf("Requested to tweet: %s", message)
				tweeter(string(message))
			}
		default:
			log.Printf("Unsupported method: %s", method)
		}
	})
}

// stats
func handleStats() {
	http.HandleFunc("/botstats", func(w http.ResponseWriter, r *http.Request) {
		switch method := r.Method; method {
		case "GET":
			fmt.Fprintf(w, "Restart count: %q", getBotRestarts())
		default:
			log.Printf("Unsupported method: %s", method)
		}
	})
}

// TODO-Maybe: module for the automatic behaviors. (only this server cares)
type autoState struct {
	sync.RWMutex
	tweet bool
	last  bool
}

func newAutoState() *autoState {
	return &autoState{sync.RWMutex{}, false, false}
}

func (as *autoState) getTweet() bool {
	as.RLock()
	defer as.RUnlock()
	return as.tweet
}

func (as *autoState) getLast() bool {
	as.RLock()
	defer as.RUnlock()
	return as.last
}

func (as *autoState) toggleTweet() {
	as.Lock()
	defer as.Unlock()
	as.tweet = !as.tweet
}

func (as *autoState) toggleLast() {
	as.Lock()
	defer as.Unlock()
	as.last = !as.last
}

var autos *autoState

// Subscribes to nowPlaying, and reads from the subscription
// Takes actions based on the settings of automatic behaviors
func metaSubscriber() {
	updates := make(chan string)
	nowPlaying.Subscribe(updates)
	for {
		for line := range updates {
			if autos.getTweet() {
				// TODO: Use the REST endpoint
				tweeter(line)
			}
			if autos.getLast() {
				botTo <- line
			}
		}
	}
}

// Commands from IRC
func procLine(line string) {
	log.Printf("got: %q", line)
	switch line {
	case "?lastsong?":
		botTo <- nowPlaying.Get()
	case "?tweet?":
		//TODO: use the REST endpoint
		tweeter(nowPlaying.Get())
	case "?autotweet?":
		autos.toggleTweet()
		botTo <- fmt.Sprintf("Autotweet is %v", autos.getTweet())
	case "?autolast?":
		autos.toggleLast()
		botTo <- fmt.Sprintf("Autolast is %v", autos.getLast())
	}
}

// Keeps the bot alive, never returns
func keepBotAlive() {
	botFrom = make(chan string)
	botTo = make(chan string)
	done := make(chan bool)
	start := func() {
		defer close(botTo)
		bot.NewBot("#radioxenu", "son_of_metabot", "irc.radioxenu.com:6667", botFrom, botTo)
		done <- true
	}
	for {
		go start()
		for line := range botFrom {
			procLine(line)
		}
		<-done // wait until previous bot is dead before making another
	}
}

func init() {
	nowPlaying = nowplaying.NewNowPlaying("http://radioxenu.com:8000/relay")
	autos = newAutoState()
	tweeter = twit.MakeTweeter("@radioxenu http://tunein.com/radio/Radio-Xenu-s118981/")
	handleTwit("/tweet", tweeter)
	handleMeta()
	handleStats()
}

func main() {
	go nowPlaying.StartUpdating()
	go metaSubscriber()
	go keepBotAlive()
	log.Fatal(http.ListenAndServe(":8080", nil))
}
<commit_msg>possible double-close bug<commit_after>package main

import (
	"fmt"
	"io"

	"log"
	"net/http"
	"sync"

	"github.com/Leimy/rx-go/bot"
	"github.com/Leimy/rx-go/nowplaying"
	"github.com/Leimy/rx-go/twit"
)

var nowPlaying *nowplaying.NowPlaying
var botFrom chan string
var botTo chan string
var tweeter twit.Tweeter

// just some counters
var stats struct {
	sync.RWMutex
	botRestarts uint64
}

func incBotRestarts() {
	stats.Lock()
	defer stats.Unlock()
	stats.botRestarts++
}

func getBotRestarts() uint64 {
	stats.RLock()
	defer stats.RUnlock()
	return stats.botRestarts
}

// Set up the metadata handler
func handleMeta() {
	http.HandleFunc("/metadata", func(w http.ResponseWriter, r *http.Request) {
		switch method := r.Method; method {
		case "GET":
			curSong := ""
			for curSong == "" {
				curSong = nowPlaying.Get()
			}
			fmt.Fprintf(w, "Now Playing: %s", curSong)
		default:
			log.Printf("%s Not implemented", method)
		}
	})
}

// Call only once per uri
func handleTwit(uri string, tweeter twit.Tweeter) {
	var message = make([]byte, 160)
	http.HandleFunc(uri, func(w http.ResponseWriter, r *http.Request) {
		switch method := r.Method; method {
		case "PUT", "POST":
			if _, err := r.Body.Read(message); err != nil && err != io.EOF {
				log.Printf("Failed to read request: %s %v", message, err)
			} else {
				log.Printf("Requested to tweet: %s", message)
				tweeter(string(message))
			}
		default:
			log.Printf("Unsupported method: %s", method)
		}
	})
}

// stats
func handleStats() {
	http.HandleFunc("/botstats", func(w http.ResponseWriter, r *http.Request) {
		switch method := r.Method; method {
		case "GET":
			fmt.Fprintf(w, "Restart count: %v", getBotRestarts())
		default:
			log.Printf("Unsupported method: %s", method)
		}
	})
}

// TODO-Maybe: module for the automatic behaviors. (only this server cares)
type autoState struct {
	sync.RWMutex
	tweet bool
	last  bool
}

func newAutoState() *autoState {
	return &autoState{sync.RWMutex{}, false, false}
}

func (as *autoState) getTweet() bool {
	as.RLock()
	defer as.RUnlock()
	return as.tweet
}

func (as *autoState) getLast() bool {
	as.RLock()
	defer as.RUnlock()
	return as.last
}

func (as *autoState) toggleTweet() {
	as.Lock()
	defer as.Unlock()
	as.tweet = !as.tweet
}

func (as *autoState) toggleLast() {
	as.Lock()
	defer as.Unlock()
	as.last = !as.last
}

var autos *autoState

// Subscribes to nowPlaying, and reads from the subscription
// Takes actions based on the settings of automatic behaviors
func metaSubscriber() {
	updates := make(chan string)
	nowPlaying.Subscribe(updates)
	for {
		for line := range updates {
			if autos.getTweet() {
				// TODO: Use the REST endpoint
				tweeter(line)
			}
			if autos.getLast() {
				botTo <- line
			}
		}
	}
}

// Commands from IRC
func procLine(line string) {
	log.Printf("got: %q", line)
	switch line {
	case "?lastsong?":
		botTo <- nowPlaying.Get()
	case "?tweet?":
		//TODO: use the REST endpoint
		tweeter(nowPlaying.Get())
	case "?autotweet?":
		autos.toggleTweet()
		botTo <- fmt.Sprintf("Autotweet is %v", autos.getTweet())
	case "?autolast?":
		autos.toggleLast()
		botTo <- fmt.Sprintf("Autolast is %v", autos.getLast())
	}
}

// Keeps the bot alive, never returns
func keepBotAlive() {
	done := make(chan bool)
	defer close(done)
	start := func() {
		botFrom = make(chan string)
		botTo = make(chan string)
		defer close(botTo)
		bot.NewBot("#radioxenu", "son_of_metabot", "irc.radioxenu.com:6667", botFrom, botTo)
		done <- true
	}
	for {
		go start()
		for line := range botFrom {
			procLine(line)
		}
		<-done // wait until previous bot is dead before making another
		incBotRestarts()
	}
}

func init() {
	nowPlaying = nowplaying.NewNowPlaying("http://radioxenu.com:8000/relay")
	autos = newAutoState()
	tweeter = twit.MakeTweeter("@radioxenu http://tunein.com/radio/Radio-Xenu-s118981/")
	handleTwit("/tweet", tweeter)
	handleMeta()
	handleStats()
}

func main() {
	go nowPlaying.StartUpdating()
	go metaSubscriber()
	go keepBotAlive()
	log.Fatal(http.ListenAndServe(":8080", nil))
}
<|endoftext|>


<commit_before>package rsec16

import "github.com/akalin/gopar/gf2p16"

func applyMatrix(m gf2p16.Matrix, in, out [][]uint16) {
	if len(in[0]) != len(out[0]) {
		panic("mismatched lengths")
	}

	// TODO: Optimize this.
	n := gf2p16.NewMatrixFromFunction(len(in), len(in[0]), func(i, j int) gf2p16.T {
		return gf2p16.T(in[i][j])
	})
	prod := m.Times(n)
	for i := 0; i < len(out); i++ {
		for j := 0; j < len(out[0]); j++ {
			out[i][j] = uint16(prod.At(i, j))
		}
	}
}
<commit_msg>Rewrite applyMatrix to use gf2p16 slice methods<commit_after>package rsec16

import "github.com/akalin/gopar/gf2p16"

func applyMatrix(m gf2p16.Matrix, in, out [][]uint16) {
	if len(in[0]) != len(out[0]) {
		panic("mismatched lengths")
	}

	// TODO: Maybe iterate over input slices first.
	for i, outSlice := range out {
		c := m.At(i, 0)
		gf2p16.MulSlice(c, in[0], outSlice)
		for j := 1; j < len(in); j++ {
			c := m.At(i, j)
			gf2p16.MulAndAddSlice(c, in[j], outSlice)
		}
	}
}
<|endoftext|>


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

package server

import (
	"fmt"
	"github.com/andreaskoch/allmark/config"
	"github.com/andreaskoch/allmark/path"
	"github.com/andreaskoch/allmark/renderer"
	"github.com/andreaskoch/allmark/repository"
	"github.com/andreaskoch/allmark/util"
	"github.com/andreaskoch/allmark/watcher"
	"log"
	"math"
	"net/http"
	"strings"
)

var (
	routes map[string]string

	useTempDir = true
)

const (

	// Routes
	ItemHandlerRoute  = "/"
	DebugHandlerRoute = "/debug/index"
)

func Serve(repositoryPath string) {

	index := renderer.RenderRepository(repositoryPath, useTempDir)

	// get the configuration
	config := config.GetConfig(repositoryPath)

	// Initialize the routing table
	initializeRoutes(index)

	// register handlers
	http.HandleFunc(ItemHandlerRoute, itemHandler)
	http.HandleFunc(DebugHandlerRoute, indexDebugger)

	// serve theme files
	if themeFolder := config.ThemeFolder(); util.DirectoryExists(themeFolder) {
		staticFolder := "/theme/"
		http.Handle(staticFolder, http.StripPrefix(staticFolder, http.FileServer(http.Dir(themeFolder))))
	}

	// start http server: http
	httpBinding := getHttpBinding(config)
	fmt.Printf("Starting http server %q\n", httpBinding)

	http.ListenAndServe(httpBinding, nil)
}

func getHttpBinding(config *config.Config) string {

	// validate the port
	port := config.Server.Http.Port
	if port < 1 || port > math.MaxUint16 {
		panic(fmt.Sprintf("%q is an invalid value for a port. Ports can only be in the range of %v to %v,", port, 1, math.MaxUint16))
	}

	return fmt.Sprintf(":%v", port)
}

func initializeRoutes(index *repository.ItemIndex) {

	routes = make(map[string]string)

	pathProvider := path.NewProvider(index.Path(), useTempDir)
	for _, item := range index.Items() {
		registerItem(pathProvider, item)
	}
}

func registerItem(pathProvider *path.Provider, item *repository.Item) {

	// recurse for child items
	for _, child := range item.Childs() {
		registerItem(pathProvider, child)
	}

	// attach change listener
	item.OnChange("Update routing table on change", func(event *watcher.WatchEvent) {
		registerItem(pathProvider, item)
	})

	// get the item route and
	// add it to the routing table
	registerRoute(pathProvider, item)

	// get the file routes and
	// add them to the routing table
	for _, file := range item.Files.Items() {
		registerRoute(pathProvider, file)
	}
}

func registerRoute(pathProvider *path.Provider, pather path.Pather) {

	if pather == nil {
		log.Printf("Cannot add a route for an uninitialized item %q.\n", pather.Path())
		return
	}

	route := pathProvider.GetWebRoute(pather)
	filePath := pathProvider.GetFilepath(pather)

	if strings.TrimSpace(route) == "" {
		log.Println("Cannot add an empty route to the routing table.")
		return
	}

	fmt.Printf("Registering route %q\n", route)
	routes[route] = filePath
}
<commit_msg>Write an error to the console when the HTTP server returns an error<commit_after>// Copyright 2013 Andreas Koch. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package server

import (
	"fmt"
	"github.com/andreaskoch/allmark/config"
	"github.com/andreaskoch/allmark/path"
	"github.com/andreaskoch/allmark/renderer"
	"github.com/andreaskoch/allmark/repository"
	"github.com/andreaskoch/allmark/util"
	"github.com/andreaskoch/allmark/watcher"
	"log"
	"math"
	"net/http"
	"os"
	"strings"
)

var (
	routes map[string]string

	useTempDir = true
)

const (

	// Routes
	ItemHandlerRoute  = "/"
	DebugHandlerRoute = "/debug/index"
)

func Serve(repositoryPath string) {

	index := renderer.RenderRepository(repositoryPath, useTempDir)

	// get the configuration
	config := config.GetConfig(repositoryPath)

	// Initialize the routing table
	initializeRoutes(index)

	// register handlers
	http.HandleFunc(ItemHandlerRoute, itemHandler)
	http.HandleFunc(DebugHandlerRoute, indexDebugger)

	// serve theme files
	if themeFolder := config.ThemeFolder(); util.DirectoryExists(themeFolder) {
		staticFolder := "/theme/"
		http.Handle(staticFolder, http.StripPrefix(staticFolder, http.FileServer(http.Dir(themeFolder))))
	}

	// start http server: http
	httpBinding := getHttpBinding(config)
	fmt.Printf("Starting http server %q\n", httpBinding)

	if err := http.ListenAndServe(httpBinding, nil); err != nil {
		fmt.Fprintf(os.Stderr, "Server failed with error: %v", err)
	}
}

func getHttpBinding(config *config.Config) string {

	// validate the port
	port := config.Server.Http.Port
	if port < 1 || port > math.MaxUint16 {
		panic(fmt.Sprintf("%q is an invalid value for a port. Ports can only be in the range of %v to %v,", port, 1, math.MaxUint16))
	}

	return fmt.Sprintf(":%v", port)
}

func initializeRoutes(index *repository.ItemIndex) {

	routes = make(map[string]string)

	pathProvider := path.NewProvider(index.Path(), useTempDir)
	for _, item := range index.Items() {
		registerItem(pathProvider, item)
	}
}

func registerItem(pathProvider *path.Provider, item *repository.Item) {

	// recurse for child items
	for _, child := range item.Childs() {
		registerItem(pathProvider, child)
	}

	// attach change listener
	item.OnChange("Update routing table on change", func(event *watcher.WatchEvent) {
		registerItem(pathProvider, item)
	})

	// get the item route and
	// add it to the routing table
	registerRoute(pathProvider, item)

	// get the file routes and
	// add them to the routing table
	for _, file := range item.Files.Items() {
		registerRoute(pathProvider, file)
	}
}

func registerRoute(pathProvider *path.Provider, pather path.Pather) {

	if pather == nil {
		log.Printf("Cannot add a route for an uninitialized item %q.\n", pather.Path())
		return
	}

	route := pathProvider.GetWebRoute(pather)
	filePath := pathProvider.GetFilepath(pather)

	if strings.TrimSpace(route) == "" {
		log.Println("Cannot add an empty route to the routing table.")
		return
	}

	fmt.Printf("Registering route %q\n", route)
	routes[route] = filePath
}
<|endoftext|>