<commit_before>package forgotpwdemail

import (
	"fmt"
	"time"

	"github.com/go-gomail/gomail"
	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
	authTemplate "github.com/skygeario/skygear-server/pkg/auth/template"
	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
	"github.com/skygeario/skygear-server/pkg/core/config"
	"github.com/skygeario/skygear-server/pkg/core/mail"
	"github.com/skygeario/skygear-server/pkg/core/template"
)

type Sender interface {
	Send(
		email string,
		authInfo authinfo.AuthInfo,
		userProfile userprofile.UserProfile,
		hashedPassword []byte,
	) error
}

type DefaultSender struct {
	Config         config.ForgotPasswordConfiguration
	Dialer         *gomail.Dialer
	CodeGenerator  *CodeGenerator
	TemplateEngine *template.Engine
}

func NewDefaultSender(
	config config.TenantConfiguration,
	dialer *gomail.Dialer,
	templateEngine *template.Engine,
) Sender {
	return &DefaultSender{
		Config:         config.ForgotPassword,
		Dialer:         dialer,
		CodeGenerator:  &CodeGenerator{config.MasterKey},
		TemplateEngine: templateEngine,
	}
}

func (d *DefaultSender) Send(
	email string,
	authInfo authinfo.AuthInfo,
	userProfile userprofile.UserProfile,
	hashedPassword []byte,
) (err error) {
	expireAt :=
		time.Now().UTC().
			Truncate(time.Second * 1).
			Add(time.Second * time.Duration(d.Config.ResetURLLifeTime))
	code := d.CodeGenerator.Generate(authInfo, userProfile, hashedPassword, expireAt)
	context := map[string]interface{}{
		"appname": d.Config.AppName,
		"link": fmt.Sprintf(
			"%s/reset-password?code=%s&user_id=%s&expire_at=%d",
			d.Config.URLPrefix,
			code,
			authInfo.ID,
			expireAt.UTC().Unix(),
		),
		"email":      email,
		"user_id":    userProfile.ID,
		"user":       userProfile.ToMap(),
		"url_prefix": d.Config.URLPrefix,
		"code":       code,
		"expire_at":  expireAt,
	}

	var textBody string
	if textBody, err = d.TemplateEngine.ParseTextTemplate(
		authTemplate.TemplateNameForgotPasswordEmailText,
		context,
		template.ParseOption{Required: true},
	); err != nil {
		return
	}

	var htmlBody string
	if htmlBody, err = d.TemplateEngine.ParseHTMLTemplate(
		authTemplate.TemplateNameForgotPasswordEmailHTML,
		context,
		template.ParseOption{Required: false},
	); err != nil {
		return
	}

	sendReq := mail.SendRequest{
		Dialer:      d.Dialer,
		Sender:      d.Config.Sender,
		SenderName:  d.Config.SenderName,
		Recipient:   email,
		Subject:     d.Config.Subject,
		ReplyTo:     d.Config.ReplyTo,
		ReplyToName: d.Config.ReplyToName,
		TextBody:    textBody,
		HTMLBody:    htmlBody,
	}

	err = sendReq.Execute()
	return
}
<commit_msg>Fix link in forgot password email<commit_after>package forgotpwdemail

import (
	"fmt"
	"time"

	"github.com/go-gomail/gomail"
	"github.com/skygeario/skygear-server/pkg/auth/dependency/userprofile"
	authTemplate "github.com/skygeario/skygear-server/pkg/auth/template"
	"github.com/skygeario/skygear-server/pkg/core/auth/authinfo"
	"github.com/skygeario/skygear-server/pkg/core/config"
	"github.com/skygeario/skygear-server/pkg/core/mail"
	"github.com/skygeario/skygear-server/pkg/core/template"
)

type Sender interface {
	Send(
		email string,
		authInfo authinfo.AuthInfo,
		userProfile userprofile.UserProfile,
		hashedPassword []byte,
	) error
}

type DefaultSender struct {
	Config         config.ForgotPasswordConfiguration
	Dialer         *gomail.Dialer
	CodeGenerator  *CodeGenerator
	TemplateEngine *template.Engine
}

func NewDefaultSender(
	config config.TenantConfiguration,
	dialer *gomail.Dialer,
	templateEngine *template.Engine,
) Sender {
	return &DefaultSender{
		Config:         config.ForgotPassword,
		Dialer:         dialer,
		CodeGenerator:  &CodeGenerator{config.MasterKey},
		TemplateEngine: templateEngine,
	}
}

func (d *DefaultSender) Send(
	email string,
	authInfo authinfo.AuthInfo,
	userProfile userprofile.UserProfile,
	hashedPassword []byte,
) (err error) {
	expireAt :=
		time.Now().UTC().
			Truncate(time.Second * 1).
			Add(time.Second * time.Duration(d.Config.ResetURLLifeTime))
	code := d.CodeGenerator.Generate(authInfo, userProfile, hashedPassword, expireAt)
	context := map[string]interface{}{
		"appname": d.Config.AppName,
		"link": fmt.Sprintf(
			"%s/forgot_password/reset_password_form?code=%s&user_id=%s&expire_at=%d",
			d.Config.URLPrefix,
			code,
			authInfo.ID,
			expireAt.UTC().Unix(),
		),
		"email":      email,
		"user_id":    userProfile.ID,
		"user":       userProfile.ToMap(),
		"url_prefix": d.Config.URLPrefix,
		"code":       code,
		"expire_at":  expireAt,
	}

	var textBody string
	if textBody, err = d.TemplateEngine.ParseTextTemplate(
		authTemplate.TemplateNameForgotPasswordEmailText,
		context,
		template.ParseOption{Required: true},
	); err != nil {
		return
	}

	var htmlBody string
	if htmlBody, err = d.TemplateEngine.ParseHTMLTemplate(
		authTemplate.TemplateNameForgotPasswordEmailHTML,
		context,
		template.ParseOption{Required: false},
	); err != nil {
		return
	}

	sendReq := mail.SendRequest{
		Dialer:      d.Dialer,
		Sender:      d.Config.Sender,
		SenderName:  d.Config.SenderName,
		Recipient:   email,
		Subject:     d.Config.Subject,
		ReplyTo:     d.Config.ReplyTo,
		ReplyToName: d.Config.ReplyToName,
		TextBody:    textBody,
		HTMLBody:    htmlBody,
	}

	err = sendReq.Execute()
	return
}
<|endoftext|>


<commit_before>// +skip_license_check

/*
This file contains portions of code directly taken from the 'xenolf/lego' project.
A copy of the license for this code can be found in the file named LICENSE in
this directory.
*/

// Package cloudflare implements a DNS provider for solving the DNS-01
// challenge using cloudflare DNS.
package cloudflare

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util"
	pkgutil "github.com/jetstack/cert-manager/pkg/util"
)

// CloudFlareAPIURL represents the API endpoint to call.
// TODO: Unexport?
const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4"

// Mockable Interface
type DNSProviderType interface {
	makeRequest(method, uri string, body io.Reader) (json.RawMessage, error)
}

// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
	dns01Nameservers []string
	authEmail        string
	authKey          string
	authToken        string
}

// DNSZone is the Zone-Record returned from Cloudflare (we`ll ignore everything we don't need)
// See https://api.cloudflare.com/#zone-properties
type DNSZone struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

// NewDNSProvider returns a DNSProvider instance configured for cloudflare.
// Credentials must be passed in the environment variables: CLOUDFLARE_EMAIL
// and CLOUDFLARE_API_KEY.
func NewDNSProvider(dns01Nameservers []string) (*DNSProvider, error) {
	email := os.Getenv("CLOUDFLARE_EMAIL")
	key := os.Getenv("CLOUDFLARE_API_KEY")
	return NewDNSProviderCredentials(email, key, "", dns01Nameservers)
}

// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for cloudflare.
func NewDNSProviderCredentials(email, key, token string, dns01Nameservers []string) (*DNSProvider, error) {
	if (email == "" && key != "") || (key == "" && token == "") {
		return nil, fmt.Errorf("no Cloudflare credential has been given (can be either an API key or an API token)")
	}
	if key != "" && token != "" {
		return nil, fmt.Errorf("the Cloudflare API key and API token cannot be both present simultaneously")
	}
	// Cloudflare uses the X-Auth-Key header for its authentication.
	// However, if the value of the X-Auth-Key header is invalid, the go
	// http library will "helpfully" print out the value to help with
	// debugging. To prevent leaking the X-Auth-Key value into the logs, we
	// first check that the X-Auth-Key header contains a valid value to
	// prevent the Go HTTP library from displaying it.
	if !validHeaderFieldValue(key) {
		return nil, fmt.Errorf("the Cloudflare API key is invalid (does the API key contain a newline?)")
	}

	if !validHeaderFieldValue(token) {
		return nil, fmt.Errorf("the Cloudflare API token is invalid (does the API token contain a newline?)")
	}

	return &DNSProvider{
		authEmail:        email,
		authKey:          key,
		authToken:        token,
		dns01Nameservers: dns01Nameservers,
	}, nil
}

// This will try to traverse the official Cloudflare API to find the nearest valid Zone.
// It's a replacement for /pkg/issuer/acme/dns/util/wait.go#FindZoneByFqdn
//  example.com.                                   ← Zone-Record found for the SLD (in most cases)
//  └── foo.example.com.                           ← Zone-Record could be possibly here, but in this case not.
//      └── _acme-challenge.foo.example.com.       ← Starting point, the FQDN.
// It will try to call the API for each branch (from bottom to top) and see if there's a Zone-Record returned.
// Calling See https://api.cloudflare.com/#zone-list-zones
func FindNearestZoneForFQDN(c DNSProviderType, fqdn string) (DNSZone, error) {
	if fqdn == "" {
		return DNSZone{}, fmt.Errorf("FindNearestZoneForFQDN: FQDN-Parameter can't be empty, please specify a domain!")
	}
	mappedFQDN := strings.Split(fqdn, ".")
	nextName := util.UnFqdn(fqdn) //remove the trailing dot
	for i := 0; i < len(mappedFQDN)-1; i++ {
		var from, to = len(mappedFQDN[i]) + 1, len(nextName)
		if from > to {
			continue
		}
		if mappedFQDN[i] == "*" { //skip wildcard sub-domain-entries
			nextName = string([]rune(nextName)[from:to])
			continue
		}
		result, err := c.makeRequest("GET", "/zones?name="+nextName, nil)
		if err != nil {
			continue
		}
		var zones []DNSZone
		err = json.Unmarshal(result, &zones)
		if err != nil {
			return DNSZone{}, err
		}

		if len(zones) > 0 {
			return zones[0], nil //we're returning the first zone found, might need to test that further
		}
		nextName = string([]rune(nextName)[from:to])
	}
	return DNSZone{}, fmt.Errorf("Found no Zones for domain %s (neither in the sub-domain noir in the SLD) please make sure your domain-entries in the config are correct and the API is correctly setup with Zone.read rights.", fqdn)
}

// Present creates a TXT record to fulfil the dns-01 challenge
func (c *DNSProvider) Present(domain, fqdn, value string) error {
	zoneID, err := c.getHostedZoneID(fqdn)
	if err != nil {
		return err
	}

	record, err := c.findTxtRecord(fqdn)
	if err != nil && err != errNoExistingRecord {
		// this is a real error
		return err
	}
	if record != nil {
		if record.Content == value {
			// the record is already set to the desired value
			return nil
		}

		_, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
		if err != nil {
			return err
		}
	}

	rec := cloudFlareRecord{
		Type:    "TXT",
		Name:    util.UnFqdn(fqdn),
		Content: value,
		TTL:     120,
	}

	body, err := json.Marshal(rec)
	if err != nil {
		return err
	}

	_, err = c.makeRequest("POST", fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
	if err != nil {
		return err
	}

	return nil
}

// CleanUp removes the TXT record matching the specified parameters
func (c *DNSProvider) CleanUp(domain, fqdn, value string) error {
	record, err := c.findTxtRecord(fqdn)
	// Nothing to cleanup
	if err == errNoExistingRecord {
		return nil
	}
	if err != nil {
		return err
	}

	_, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
	if err != nil {
		return err
	}

	return nil
}

func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
	hostedZone, err := FindNearestZoneForFQDN(c, fqdn)
	if err != nil {
		return "", err
	}
	return hostedZone.ID, nil
}

var errNoExistingRecord = errors.New("No existing record found")

func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) {
	zoneID, err := c.getHostedZoneID(fqdn)
	if err != nil {
		return nil, err
	}

	result, err := c.makeRequest(
		"GET",
		fmt.Sprintf("/zones/%s/dns_records?per_page=100&type=TXT&name=%s", zoneID, util.UnFqdn(fqdn)),
		nil,
	)
	if err != nil {
		return nil, err
	}

	var records []cloudFlareRecord
	err = json.Unmarshal(result, &records)
	if err != nil {
		return nil, err
	}

	for _, rec := range records {
		if rec.Name == util.UnFqdn(fqdn) {
			return &rec, nil
		}
	}

	return nil, errNoExistingRecord
}

func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
	// APIError contains error details for failed requests
	type APIError struct {
		Code       int        `json:"code,omitempty"`
		Message    string     `json:"message,omitempty"`
		ErrorChain []APIError `json:"error_chain,omitempty"`
	}

	// APIResponse represents a response from CloudFlare API
	type APIResponse struct {
		Success bool            `json:"success"`
		Errors  []*APIError     `json:"errors"`
		Result  json.RawMessage `json:"result"`
	}

	req, err := http.NewRequest(method, fmt.Sprintf("%s%s", CloudFlareAPIURL, uri), body)
	if err != nil {
		return nil, err
	}

	if c.authEmail != "" {
		req.Header.Set("X-Auth-Email", c.authEmail)
	}
	if c.authToken != "" {
		req.Header.Set("Authorization", "Bearer "+c.authToken)
	} else {
		req.Header.Set("X-Auth-Key", c.authKey)
	}
	req.Header.Set("User-Agent", pkgutil.CertManagerUserAgent)

	client := http.Client{
		Timeout: 30 * time.Second,
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("while querying the Cloudflare API for %s %q: %v", method, uri, err)
	}

	defer resp.Body.Close()

	var r APIResponse
	err = json.NewDecoder(resp.Body).Decode(&r)
	if err != nil {
		return nil, err
	}

	if !r.Success {
		if len(r.Errors) > 0 {
			errStr := ""
			for _, apiErr := range r.Errors {
				errStr += fmt.Sprintf("\t Error: %d: %s", apiErr.Code, apiErr.Message)
				for _, chainErr := range apiErr.ErrorChain {
					errStr += fmt.Sprintf("<- %d: %s", chainErr.Code, chainErr.Message)
				}
			}
			return nil, fmt.Errorf("while querying the Cloudflare API for %s %q \n%s", method, uri, errStr)
		}
		return nil, fmt.Errorf("while querying the Cloudflare API for %s %q", method, uri)
	}

	return r.Result, nil
}

// cloudFlareRecord represents a CloudFlare DNS record
type cloudFlareRecord struct {
	Name    string `json:"name"`
	Type    string `json:"type"`
	Content string `json:"content"`
	ID      string `json:"id,omitempty"`
	TTL     int    `json:"ttl,omitempty"`
	ZoneID  string `json:"zone_id,omitempty"`
}

// following functions are copy-pasted from go's internal
// http server
func validHeaderFieldValue(v string) bool {
	for i := 0; i < len(v); i++ {
		b := v[i]
		if isCTL(b) && !isLWS(b) {
			return false
		}
	}
	return true
}

func isCTL(b byte) bool {
	const del = 0x7f // a CTL
	return b < ' ' || b == del
}

func isLWS(b byte) bool { return b == ' ' || b == '\t' }
<commit_msg>fix typo in error message<commit_after>// +skip_license_check

/*
This file contains portions of code directly taken from the 'xenolf/lego' project.
A copy of the license for this code can be found in the file named LICENSE in
this directory.
*/

// Package cloudflare implements a DNS provider for solving the DNS-01
// challenge using cloudflare DNS.
package cloudflare

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util"
	pkgutil "github.com/jetstack/cert-manager/pkg/util"
)

// CloudFlareAPIURL represents the API endpoint to call.
// TODO: Unexport?
const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4"

// Mockable Interface
type DNSProviderType interface {
	makeRequest(method, uri string, body io.Reader) (json.RawMessage, error)
}

// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
	dns01Nameservers []string
	authEmail        string
	authKey          string
	authToken        string
}

// DNSZone is the Zone-Record returned from Cloudflare (we`ll ignore everything we don't need)
// See https://api.cloudflare.com/#zone-properties
type DNSZone struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

// NewDNSProvider returns a DNSProvider instance configured for cloudflare.
// Credentials must be passed in the environment variables: CLOUDFLARE_EMAIL
// and CLOUDFLARE_API_KEY.
func NewDNSProvider(dns01Nameservers []string) (*DNSProvider, error) {
	email := os.Getenv("CLOUDFLARE_EMAIL")
	key := os.Getenv("CLOUDFLARE_API_KEY")
	return NewDNSProviderCredentials(email, key, "", dns01Nameservers)
}

// NewDNSProviderCredentials uses the supplied credentials to return a
// DNSProvider instance configured for cloudflare.
func NewDNSProviderCredentials(email, key, token string, dns01Nameservers []string) (*DNSProvider, error) {
	if (email == "" && key != "") || (key == "" && token == "") {
		return nil, fmt.Errorf("no Cloudflare credential has been given (can be either an API key or an API token)")
	}
	if key != "" && token != "" {
		return nil, fmt.Errorf("the Cloudflare API key and API token cannot be both present simultaneously")
	}
	// Cloudflare uses the X-Auth-Key header for its authentication.
	// However, if the value of the X-Auth-Key header is invalid, the go
	// http library will "helpfully" print out the value to help with
	// debugging. To prevent leaking the X-Auth-Key value into the logs, we
	// first check that the X-Auth-Key header contains a valid value to
	// prevent the Go HTTP library from displaying it.
	if !validHeaderFieldValue(key) {
		return nil, fmt.Errorf("the Cloudflare API key is invalid (does the API key contain a newline?)")
	}

	if !validHeaderFieldValue(token) {
		return nil, fmt.Errorf("the Cloudflare API token is invalid (does the API token contain a newline?)")
	}

	return &DNSProvider{
		authEmail:        email,
		authKey:          key,
		authToken:        token,
		dns01Nameservers: dns01Nameservers,
	}, nil
}

// This will try to traverse the official Cloudflare API to find the nearest valid Zone.
// It's a replacement for /pkg/issuer/acme/dns/util/wait.go#FindZoneByFqdn
//  example.com.                                   ← Zone-Record found for the SLD (in most cases)
//  └── foo.example.com.                           ← Zone-Record could be possibly here, but in this case not.
//      └── _acme-challenge.foo.example.com.       ← Starting point, the FQDN.
// It will try to call the API for each branch (from bottom to top) and see if there's a Zone-Record returned.
// Calling See https://api.cloudflare.com/#zone-list-zones
func FindNearestZoneForFQDN(c DNSProviderType, fqdn string) (DNSZone, error) {
	if fqdn == "" {
		return DNSZone{}, fmt.Errorf("FindNearestZoneForFQDN: FQDN-Parameter can't be empty, please specify a domain!")
	}
	mappedFQDN := strings.Split(fqdn, ".")
	nextName := util.UnFqdn(fqdn) //remove the trailing dot
	for i := 0; i < len(mappedFQDN)-1; i++ {
		var from, to = len(mappedFQDN[i]) + 1, len(nextName)
		if from > to {
			continue
		}
		if mappedFQDN[i] == "*" { //skip wildcard sub-domain-entries
			nextName = string([]rune(nextName)[from:to])
			continue
		}
		result, err := c.makeRequest("GET", "/zones?name="+nextName, nil)
		if err != nil {
			continue
		}
		var zones []DNSZone
		err = json.Unmarshal(result, &zones)
		if err != nil {
			return DNSZone{}, err
		}

		if len(zones) > 0 {
			return zones[0], nil //we're returning the first zone found, might need to test that further
		}
		nextName = string([]rune(nextName)[from:to])
	}
	return DNSZone{}, fmt.Errorf("Found no Zones for domain %s (neither in the sub-domain nor in the SLD) please make sure your domain-entries in the config are correct and the API key is correctly setup with Zone.read rights.", fqdn)
}

// Present creates a TXT record to fulfil the dns-01 challenge
func (c *DNSProvider) Present(domain, fqdn, value string) error {
	zoneID, err := c.getHostedZoneID(fqdn)
	if err != nil {
		return err
	}

	record, err := c.findTxtRecord(fqdn)
	if err != nil && err != errNoExistingRecord {
		// this is a real error
		return err
	}
	if record != nil {
		if record.Content == value {
			// the record is already set to the desired value
			return nil
		}

		_, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
		if err != nil {
			return err
		}
	}

	rec := cloudFlareRecord{
		Type:    "TXT",
		Name:    util.UnFqdn(fqdn),
		Content: value,
		TTL:     120,
	}

	body, err := json.Marshal(rec)
	if err != nil {
		return err
	}

	_, err = c.makeRequest("POST", fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
	if err != nil {
		return err
	}

	return nil
}

// CleanUp removes the TXT record matching the specified parameters
func (c *DNSProvider) CleanUp(domain, fqdn, value string) error {
	record, err := c.findTxtRecord(fqdn)
	// Nothing to cleanup
	if err == errNoExistingRecord {
		return nil
	}
	if err != nil {
		return err
	}

	_, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
	if err != nil {
		return err
	}

	return nil
}

func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
	hostedZone, err := FindNearestZoneForFQDN(c, fqdn)
	if err != nil {
		return "", err
	}
	return hostedZone.ID, nil
}

var errNoExistingRecord = errors.New("No existing record found")

func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) {
	zoneID, err := c.getHostedZoneID(fqdn)
	if err != nil {
		return nil, err
	}

	result, err := c.makeRequest(
		"GET",
		fmt.Sprintf("/zones/%s/dns_records?per_page=100&type=TXT&name=%s", zoneID, util.UnFqdn(fqdn)),
		nil,
	)
	if err != nil {
		return nil, err
	}

	var records []cloudFlareRecord
	err = json.Unmarshal(result, &records)
	if err != nil {
		return nil, err
	}

	for _, rec := range records {
		if rec.Name == util.UnFqdn(fqdn) {
			return &rec, nil
		}
	}

	return nil, errNoExistingRecord
}

func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
	// APIError contains error details for failed requests
	type APIError struct {
		Code       int        `json:"code,omitempty"`
		Message    string     `json:"message,omitempty"`
		ErrorChain []APIError `json:"error_chain,omitempty"`
	}

	// APIResponse represents a response from CloudFlare API
	type APIResponse struct {
		Success bool            `json:"success"`
		Errors  []*APIError     `json:"errors"`
		Result  json.RawMessage `json:"result"`
	}

	req, err := http.NewRequest(method, fmt.Sprintf("%s%s", CloudFlareAPIURL, uri), body)
	if err != nil {
		return nil, err
	}

	if c.authEmail != "" {
		req.Header.Set("X-Auth-Email", c.authEmail)
	}
	if c.authToken != "" {
		req.Header.Set("Authorization", "Bearer "+c.authToken)
	} else {
		req.Header.Set("X-Auth-Key", c.authKey)
	}
	req.Header.Set("User-Agent", pkgutil.CertManagerUserAgent)

	client := http.Client{
		Timeout: 30 * time.Second,
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("while querying the Cloudflare API for %s %q: %v", method, uri, err)
	}

	defer resp.Body.Close()

	var r APIResponse
	err = json.NewDecoder(resp.Body).Decode(&r)
	if err != nil {
		return nil, err
	}

	if !r.Success {
		if len(r.Errors) > 0 {
			errStr := ""
			for _, apiErr := range r.Errors {
				errStr += fmt.Sprintf("\t Error: %d: %s", apiErr.Code, apiErr.Message)
				for _, chainErr := range apiErr.ErrorChain {
					errStr += fmt.Sprintf("<- %d: %s", chainErr.Code, chainErr.Message)
				}
			}
			return nil, fmt.Errorf("while querying the Cloudflare API for %s %q \n%s", method, uri, errStr)
		}
		return nil, fmt.Errorf("while querying the Cloudflare API for %s %q", method, uri)
	}

	return r.Result, nil
}

// cloudFlareRecord represents a CloudFlare DNS record
type cloudFlareRecord struct {
	Name    string `json:"name"`
	Type    string `json:"type"`
	Content string `json:"content"`
	ID      string `json:"id,omitempty"`
	TTL     int    `json:"ttl,omitempty"`
	ZoneID  string `json:"zone_id,omitempty"`
}

// following functions are copy-pasted from go's internal
// http server
func validHeaderFieldValue(v string) bool {
	for i := 0; i < len(v); i++ {
		b := v[i]
		if isCTL(b) && !isLWS(b) {
			return false
		}
	}
	return true
}

func isCTL(b byte) bool {
	const del = 0x7f // a CTL
	return b < ' ' || b == del
}

func isLWS(b byte) bool { return b == ' ' || b == '\t' }
<|endoftext|>


<commit_before>/*
Copyright 2019 The Skaffold Authors

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

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

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

package v1alpha1

import (
	"testing"

	yaml "gopkg.in/yaml.v2"

	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/v1alpha2"
	"github.com/GoogleContainerTools/skaffold/testutil"
)

func TestUpgrade_gitTagger(t *testing.T) {
	yaml := `apiVersion: skaffold/v1alpha1
kind: Config
build:
  tagPolicy: gitCommit
`
	expected := `apiVersion: skaffold/v1alpha2
kind: Config
build:
  tagPolicy:
    gitCommit: {}
`
	verifyUpgrade(t, yaml, expected)
}

func TestUpgrade_sha256Tagger(t *testing.T) {
	yaml := `apiVersion: skaffold/v1alpha1
kind: Config
build:
  tagPolicy: sha256
`
	expected := `apiVersion: skaffold/v1alpha2
kind: Config
build:
  tagPolicy:
    sha256: {}
`
	verifyUpgrade(t, yaml, expected)
}

func TestUpgrade_deploy(t *testing.T) {
	yaml := `apiVersion: skaffold/v1alpha1
kind: Config
build:
  artifacts:
  - imageName: gcr.io/k8s-skaffold/skaffold-example
deploy:
  kubectl:
    manifests:
    - paths:
      - k8s-*
`
	expected := `apiVersion: skaffold/v1alpha2
kind: Config
build:
  artifacts:
  - imageName: gcr.io/k8s-skaffold/skaffold-example
deploy:
  kubectl:
    manifests:
    - k8s-*
`
	verifyUpgrade(t, yaml, expected)
}

func verifyUpgrade(t *testing.T, input, output string) {
	config := NewSkaffoldConfig()
	err := yaml.UnmarshalStrict([]byte(input), config)
	testutil.CheckErrorAndDeepEqual(t, false, err, Version, config.GetVersion())

	upgraded, err := config.Upgrade()
	testutil.CheckError(t, false, err)

	expected := v1alpha2.NewSkaffoldConfig()
	err = yaml.UnmarshalStrict([]byte(output), expected)

	testutil.CheckErrorAndDeepEqual(t, false, err, expected, upgraded)
}
<commit_msg>Add missing test<commit_after>/*
Copyright 2019 The Skaffold Authors

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

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

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

package v1alpha1

import (
	"testing"

	yaml "gopkg.in/yaml.v2"

	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/v1alpha2"
	"github.com/GoogleContainerTools/skaffold/testutil"
)

func TestUpgrade_gitTagger(t *testing.T) {
	yaml := `apiVersion: skaffold/v1alpha1
kind: Config
build:
  tagPolicy: gitCommit
`
	expected := `apiVersion: skaffold/v1alpha2
kind: Config
build:
  tagPolicy:
    gitCommit: {}
`
	verifyUpgrade(t, yaml, expected)
}

func TestUpgrade_sha256Tagger(t *testing.T) {
	yaml := `apiVersion: skaffold/v1alpha1
kind: Config
build:
  tagPolicy: sha256
`
	expected := `apiVersion: skaffold/v1alpha2
kind: Config
build:
  tagPolicy:
    sha256: {}
`
	verifyUpgrade(t, yaml, expected)
}

func TestUpgrade_deploy(t *testing.T) {
	yaml := `apiVersion: skaffold/v1alpha1
kind: Config
build:
  artifacts:
  - imageName: gcr.io/k8s-skaffold/skaffold-example
deploy:
  kubectl:
    manifests:
    - paths:
      - k8s-*
`
	expected := `apiVersion: skaffold/v1alpha2
kind: Config
build:
  artifacts:
  - imageName: gcr.io/k8s-skaffold/skaffold-example
deploy:
  kubectl:
    manifests:
    - k8s-*
`
	verifyUpgrade(t, yaml, expected)
}

func TestUpgrade_helm(t *testing.T) {
	yaml := `apiVersion: skaffold/v1alpha1
kind: Config
build:
  artifacts:
  - imageName: gcr.io/k8s-skaffold/skaffold-example
deploy:
  helm:
    releases:
    - name: release
      chartPath: path
      valuesFilePath: valuesFile
      values: {key:value}
      namespace: ns
      version: 1.0
`
	expected := `apiVersion: skaffold/v1alpha2
kind: Config
build:
  artifacts:
  - imageName: gcr.io/k8s-skaffold/skaffold-example
deploy:
  helm:
    releases:
    - name: release
      chartPath: path
      valuesFilePath: valuesFile
      values: {key:value}
      namespace: ns
      version: 1.0
`
	verifyUpgrade(t, yaml, expected)
}

func verifyUpgrade(t *testing.T, input, output string) {
	config := NewSkaffoldConfig()
	err := yaml.UnmarshalStrict([]byte(input), config)
	testutil.CheckErrorAndDeepEqual(t, false, err, Version, config.GetVersion())

	upgraded, err := config.Upgrade()
	testutil.CheckError(t, false, err)

	expected := v1alpha2.NewSkaffoldConfig()
	err = yaml.UnmarshalStrict([]byte(output), expected)

	testutil.CheckErrorAndDeepEqual(t, false, err, expected, upgraded)
}
<|endoftext|>


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

// Pipe adapter to connect code expecting an io.Reader
// with code expecting an io.Writer.

package io

import (
	"errors"
	"sync"
)

// ErrClosedPipe is the error used for read or write operations on a closed pipe.
var ErrClosedPipe = errors.New("io: read/write on closed pipe")

// A pipe is the shared pipe structure underlying PipeReader and PipeWriter.
type pipe struct {
	rl    sync.Mutex // gates readers one at a time
	wl    sync.Mutex // gates writers one at a time
	l     sync.Mutex // protects remaining fields
	data  []byte     // data remaining in pending write
	rwait sync.Cond  // waiting reader
	wwait sync.Cond  // waiting writer
	rerr  error      // if reader closed, error to give writes
	werr  error      // if writer closed, error to give reads
}

func (p *pipe) read(b []byte) (n int, err error) {
	// One reader at a time.
	p.rl.Lock()
	defer p.rl.Unlock()

	p.l.Lock()
	defer p.l.Unlock()
	for {
		if p.rerr != nil {
			return 0, ErrClosedPipe
		}
		if p.data != nil {
			break
		}
		if p.werr != nil {
			return 0, p.werr
		}
		p.rwait.Wait()
	}
	n = copy(b, p.data)
	p.data = p.data[n:]
	if len(p.data) == 0 {
		p.data = nil
		p.wwait.Signal()
	}
	return
}

var zero [0]byte

func (p *pipe) write(b []byte) (n int, err error) {
	// pipe uses nil to mean not available
	if b == nil {
		b = zero[:]
	}

	// One writer at a time.
	p.wl.Lock()
	defer p.wl.Unlock()

	p.l.Lock()
	defer p.l.Unlock()
	if p.werr != nil {
		err = ErrClosedPipe
		return
	}
	p.data = b
	p.rwait.Signal()
	for {
		if p.data == nil {
			break
		}
		if p.rerr != nil {
			err = p.rerr
			break
		}
		if p.werr != nil {
			err = ErrClosedPipe
		}
		p.wwait.Wait()
	}
	n = len(b) - len(p.data)
	p.data = nil // in case of rerr or werr
	return
}

func (p *pipe) rclose(err error) {
	if err == nil {
		err = ErrClosedPipe
	}
	p.l.Lock()
	defer p.l.Unlock()
	p.rerr = err
	p.rwait.Signal()
	p.wwait.Signal()
}

func (p *pipe) wclose(err error) {
	if err == nil {
		err = EOF
	}
	p.l.Lock()
	defer p.l.Unlock()
	p.werr = err
	p.rwait.Signal()
	p.wwait.Signal()
}

// A PipeReader is the read half of a pipe.
type PipeReader struct {
	p *pipe
}

// Read implements the standard Read interface:
// it reads data from the pipe, blocking until a writer
// arrives or the write end is closed.
// If the write end is closed with an error, that error is
// returned as err; otherwise err is EOF.
func (r *PipeReader) Read(data []byte) (n int, err error) {
	return r.p.read(data)
}

// Close closes the reader; subsequent writes to the
// write half of the pipe will return the error ErrClosedPipe.
func (r *PipeReader) Close() error {
	return r.CloseWithError(nil)
}

// CloseWithError closes the reader; subsequent writes
// to the write half of the pipe will return the error err.
func (r *PipeReader) CloseWithError(err error) error {
	r.p.rclose(err)
	return nil
}

// A PipeWriter is the write half of a pipe.
type PipeWriter struct {
	p *pipe
}

// Write implements the standard Write interface:
// it writes data to the pipe, blocking until readers
// have consumed all the data or the read end is closed.
// If the read end is closed with an error, that err is
// returned as err; otherwise err is ErrClosedPipe.
func (w *PipeWriter) Write(data []byte) (n int, err error) {
	return w.p.write(data)
}

// Close closes the writer; subsequent reads from the
// read half of the pipe will return no bytes and EOF.
func (w *PipeWriter) Close() error {
	return w.CloseWithError(nil)
}

// CloseWithError closes the writer; subsequent reads from the
// read half of the pipe will return no bytes and the error err,
// or EOF if err is nil.
//
// CloseWithError always returns nil.
func (w *PipeWriter) CloseWithError(err error) error {
	w.p.wclose(err)
	return nil
}

// Pipe creates a synchronous in-memory pipe.
// It can be used to connect code expecting an io.Reader
// with code expecting an io.Writer.
// Reads on one end are matched with writes on the other,
// copying data directly between the two; there is no internal buffering.
// It is safe to call Read and Write in parallel with each other or with
// Close. Close will complete once pending I/O is done. Parallel calls to
// Read, and parallel calls to Write, are also safe:
// the individual calls will be gated sequentially.
func Pipe() (*PipeReader, *PipeWriter) {
	p := new(pipe)
	p.rwait.L = &p.l
	p.wwait.L = &p.l
	r := &PipeReader{p}
	w := &PipeWriter{p}
	return r, w
}
<commit_msg>io: clarify Pipe docs<commit_after>// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Pipe adapter to connect code expecting an io.Reader
// with code expecting an io.Writer.

package io

import (
	"errors"
	"sync"
)

// ErrClosedPipe is the error used for read or write operations on a closed pipe.
var ErrClosedPipe = errors.New("io: read/write on closed pipe")

// A pipe is the shared pipe structure underlying PipeReader and PipeWriter.
type pipe struct {
	rl    sync.Mutex // gates readers one at a time
	wl    sync.Mutex // gates writers one at a time
	l     sync.Mutex // protects remaining fields
	data  []byte     // data remaining in pending write
	rwait sync.Cond  // waiting reader
	wwait sync.Cond  // waiting writer
	rerr  error      // if reader closed, error to give writes
	werr  error      // if writer closed, error to give reads
}

func (p *pipe) read(b []byte) (n int, err error) {
	// One reader at a time.
	p.rl.Lock()
	defer p.rl.Unlock()

	p.l.Lock()
	defer p.l.Unlock()
	for {
		if p.rerr != nil {
			return 0, ErrClosedPipe
		}
		if p.data != nil {
			break
		}
		if p.werr != nil {
			return 0, p.werr
		}
		p.rwait.Wait()
	}
	n = copy(b, p.data)
	p.data = p.data[n:]
	if len(p.data) == 0 {
		p.data = nil
		p.wwait.Signal()
	}
	return
}

var zero [0]byte

func (p *pipe) write(b []byte) (n int, err error) {
	// pipe uses nil to mean not available
	if b == nil {
		b = zero[:]
	}

	// One writer at a time.
	p.wl.Lock()
	defer p.wl.Unlock()

	p.l.Lock()
	defer p.l.Unlock()
	if p.werr != nil {
		err = ErrClosedPipe
		return
	}
	p.data = b
	p.rwait.Signal()
	for {
		if p.data == nil {
			break
		}
		if p.rerr != nil {
			err = p.rerr
			break
		}
		if p.werr != nil {
			err = ErrClosedPipe
		}
		p.wwait.Wait()
	}
	n = len(b) - len(p.data)
	p.data = nil // in case of rerr or werr
	return
}

func (p *pipe) rclose(err error) {
	if err == nil {
		err = ErrClosedPipe
	}
	p.l.Lock()
	defer p.l.Unlock()
	p.rerr = err
	p.rwait.Signal()
	p.wwait.Signal()
}

func (p *pipe) wclose(err error) {
	if err == nil {
		err = EOF
	}
	p.l.Lock()
	defer p.l.Unlock()
	p.werr = err
	p.rwait.Signal()
	p.wwait.Signal()
}

// A PipeReader is the read half of a pipe.
type PipeReader struct {
	p *pipe
}

// Read implements the standard Read interface:
// it reads data from the pipe, blocking until a writer
// arrives or the write end is closed.
// If the write end is closed with an error, that error is
// returned as err; otherwise err is EOF.
func (r *PipeReader) Read(data []byte) (n int, err error) {
	return r.p.read(data)
}

// Close closes the reader; subsequent writes to the
// write half of the pipe will return the error ErrClosedPipe.
func (r *PipeReader) Close() error {
	return r.CloseWithError(nil)
}

// CloseWithError closes the reader; subsequent writes
// to the write half of the pipe will return the error err.
func (r *PipeReader) CloseWithError(err error) error {
	r.p.rclose(err)
	return nil
}

// A PipeWriter is the write half of a pipe.
type PipeWriter struct {
	p *pipe
}

// Write implements the standard Write interface:
// it writes data to the pipe, blocking until one or more readers
// have consumed all the data or the read end is closed.
// If the read end is closed with an error, that err is
// returned as err; otherwise err is ErrClosedPipe.
func (w *PipeWriter) Write(data []byte) (n int, err error) {
	return w.p.write(data)
}

// Close closes the writer; subsequent reads from the
// read half of the pipe will return no bytes and EOF.
func (w *PipeWriter) Close() error {
	return w.CloseWithError(nil)
}

// CloseWithError closes the writer; subsequent reads from the
// read half of the pipe will return no bytes and the error err,
// or EOF if err is nil.
//
// CloseWithError always returns nil.
func (w *PipeWriter) CloseWithError(err error) error {
	w.p.wclose(err)
	return nil
}

// Pipe creates a synchronous in-memory pipe.
// It can be used to connect code expecting an io.Reader
// with code expecting an io.Writer.
//
// Reads and Writes on the pipe are matched one to one
// except when multiple Reads are needed to consume a single Write.
// That is, each Write to the PipeWriter blocks until it has satisfied
// one or more Reads from the PipeReader that fully consume
// the written data.
// The data is copied directly from the Write to the corresponding
// Read (or Reads); there is no internal buffering.
//
// It is safe to call Read and Write in parallel with each other or with Close.
// Parallel calls to Read and parallel calls to Write are also safe:
// the individual calls will be gated sequentially.
func Pipe() (*PipeReader, *PipeWriter) {
	p := new(pipe)
	p.rwait.L = &p.l
	p.wwait.L = &p.l
	r := &PipeReader{p}
	w := &PipeWriter{p}
	return r, w
}
<|endoftext|>


<commit_before>package main

import "strings"
import "strconv"
import "fmt"
import "errors"
import "net"
import "net/http"
import "net/url"
import "time"
import "html"
import "log"
import "io"
import "bufio"
import "bytes"
import "flag"
import "encoding/xml"
import "golang.org/x/tools/blog/atom"

// Maximum size a request for a bug-xml is read in byte.
var maxBugRequestRead int64

// Maximum number of requests per second. Set to something negative to disable
var maxRequestsPerSecond int

// Channel to block on during too many requests in a second
var tooManyRequestsBlocker chan bool = make(chan bool)

const versionMajor = 0
const versionMinor = 1
const versionBugfix = 0
const versionGit = true

const bugzillaDateFormat = "2006-01-02 15:04:05 -0700"
const userAgentName = "bugzillatoatom"

// returns the minimum of the given values
func min(a int, b int) int {
    if a <= b {
        return a
    } else {
        return b
    }
}

func getVersion() string {
    if versionGit {
        return strconv.Itoa(versionMajor) + "." + strconv.Itoa(versionMinor) + "." + strconv.Itoa(versionBugfix) + "-git"
    } else {
        return strconv.Itoa(versionMajor) + "." + strconv.Itoa(versionMinor) + "." + strconv.Itoa(versionBugfix)
    }
}

// Read from r until given string is found. Appends toAppend afterwards if string
// is found and in any case returns the result
func readUntilString(r io.Reader, until string, toAppend string) (string, error) {
    var buffer bytes.Buffer
    eofReached := false
    rs := bufio.NewReader(io.LimitReader(r, maxBugRequestRead))

    for !eofReached {
        str, err := rs.ReadString('\n')

        if err == io.EOF {
            eofReached = true
        } else if err != nil {
            log.Printf("Error during reading from url: %s\n", err)
            return "", err
        }

        index := strings.Index(str, until)

        if index == -1 {
            buffer.WriteString(str)
        } else {
            buffer.WriteString(str[:index])
            buffer.WriteString(toAppend)
            break
        }
    }

    return buffer.String(), nil
}

// Requests the bug from given url
func doRequest(target *url.URL) (string, error) {
    request := http.Request{
        Method: http.MethodGet,
        URL: target,
        Header: http.Header{
            "User-Agent": { userAgentName + "/" + getVersion() },
        },
    }

    resp, err := http.DefaultClient.Do(&request)

    if err != nil {
        log.Printf("Error during GET to url \"%s\": %s\n", target, err)
        return "", err
    }

    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        log.Printf("Request to \"%s\" returned status code %d (%s).\n", target, resp.StatusCode, http.StatusText(resp.StatusCode))
        errStr := fmt.Sprintf("Request returned status code %d (%s).", resp.StatusCode, http.StatusText(resp.StatusCode))
        return "", errors.New(errStr)
    }

    // TODO: maybe we should search for something more clever to abort. <attachment could be given by a user in a report
    return readUntilString(resp.Body, "<attachment", "</bug></bugzilla>")
}

// converts the given xml string into an atom feed
func convertXmlToAtom(inXml string) (string, error) {
    type Who struct {
        Name string `xml:",chardata"`
        RealName string `xml:"name,attr"`
    }

    getFormatedName := func(w Who) string {
        if w.RealName == "" {
            return w.Name
        } else {
            return w.RealName + " (" + w.Name + ")"
        }
    }

    type Comment struct {
        CommentId int `xml:"commentid"`
        CommentCount int `xml:"comment_count"`
        AttachmentID int `xml:"attachid"`
        Who Who `xml:"who"`
        When string `xml:"bug_when"`
        Text string `xml:"thetext"`
    }

    type InResult struct {
        Urlbase string `xml:"urlbase,attr"`
        BugId int `xml:"bug>bug_id"`
        Description string `xml:"bug>short_desc"`
        Comments []Comment `xml:"bug>long_desc"`
    }

    inResult := InResult{}
    err := xml.Unmarshal([]byte(inXml), &inResult)
    if err != nil {
        log.Printf("Error during unmarshalling the xml: %s\n", err)
        return "", err
    } else if len(inResult.Comments) == 0 {
        // One comment, the initial one, should always be available
        err := errors.New("Zero comments in bug. There should be at least the initial one.")
        log.Println("Zero comments in bug. There should be at least the initial one.")
        return "", err
    }

    updateTime, err := time.Parse(bugzillaDateFormat, inResult.Comments[len(inResult.Comments)-1].When)
    if err != nil {
        log.Printf("Couldn't parse updateTime in initial comment: %s\n", err)
        return "", err
    }

    inUrl := fmt.Sprintf("%s/show_bug.cgi?id=%d", inResult.Urlbase, inResult.BugId)
    attachmentUrl := fmt.Sprintf("%s/attachment.cgi?id=", inResult.Urlbase)

    feed := &atom.Feed{
        Title: inResult.Description,
        ID: inUrl,
        Link: []atom.Link{atom.Link{Href: inUrl, Rel: "alternate"}},
        Updated: atom.Time(updateTime),
        Author: &atom.Person{Name: getFormatedName(inResult.Comments[0].Who)},
        Entry: make([]*atom.Entry, 0, len(inResult.Comments)),
    }

    for i, comment := range inResult.Comments {
        creationTime, err := time.Parse(bugzillaDateFormat, comment.When)
        if err != nil {
            log.Printf("Couldn't parse updateTime in comment %d: %s\n", i, err)
            return "", err
        }

        links := []atom.Link{atom.Link{Href: inUrl + "#c" + strconv.Itoa(comment.CommentCount), Rel: "alternate"}}
        if comment.AttachmentID != 0 {
            links = append(links, atom.Link{Href: attachmentUrl + strconv.Itoa(comment.AttachmentID), Rel: "enclosure"})
        }

        entry := &atom.Entry{
            Title: getFormatedName(comment.Who) + ": " + comment.Text[:min(100, len(comment.Text))],
            ID: inUrl + "#c" + strconv.Itoa(comment.CommentCount),
            Link: links,
            Published: atom.Time(creationTime),
            Author: &atom.Person{Name: getFormatedName(comment.Who)},
            Content: &atom.Text{Type: "html", Body: strings.Replace(html.EscapeString(comment.Text), "\n", "
", -1)},
        }

        feed.Entry = append(feed.Entry, entry)
    }

    atom, err := xml.MarshalIndent(feed, "", "\t")
    if err != nil {
        log.Printf("Error during creating the atom feed: %s\n", err)
        return "", err
    }

    return xml.Header + string(atom), nil
}

// Filters not allowed targets, defined by the given networks
// TODO: Technically an attack is possible. First return a harmless IP
//       for the check and another one later for the actual request.
func checkTargetAllowed(target string, forbiddenNetworks []*net.IPNet) (bool, error) {
    if forbiddenNetworks == nil {
        return true, nil
    }

    ips, err := net.LookupIP(target)

    if err != nil {
        return false, err
    }

    for _, ip := range ips {
        for _, ipnet := range forbiddenNetworks {
            if ipnet.Contains(ip) {
                log.Printf("Blocked target \"%s\" since it's IP %s is contained in blocked network %s.\n", target, ip, ipnet)
                return false, nil
            }
        }
    }

    return true, nil
}

func handleConvert(w http.ResponseWriter, r *http.Request, forbiddenNetworks []*net.IPNet) {
    // Block during too many requests in the last second
    if maxRequestsPerSecond >= 0 {
        <-tooManyRequestsBlocker
    }

    // Check for a possible recursive call
    if r.Header != nil {
        for _, agent := range r.Header["User-Agent"] {
            if strings.Contains(agent, userAgentName) {
                log.Printf("Blocked request by %s due tue User-Agent \"%s\".\n", r.RemoteAddr, agent)
                errStr := fmt.Sprintf("User-Agent \"%s\" blocked.", r.Header["User-Agent"])
                http.Error(w, errStr, http.StatusForbidden)
                return
            }
        }
    }

    formValueUrl := r.FormValue("url")

    // if the user didn't give a protocol simply assume http
    if !(strings.HasPrefix(formValueUrl, "http://") || strings.HasPrefix(formValueUrl, "https://")) {
        formValueUrl = "http://" + formValueUrl
    }

    target, err := url.Parse(formValueUrl)

    if err != nil {
        log.Printf("Error occurred during parsing the url \"%s\": %s.\n", r.FormValue("url"), err.Error())
        errStr := fmt.Sprintf("Error occurred during parsing the url \"%s\": %s\nAre you sure the url is correct?", r.FormValue("url"), err.Error())
        http.Error(w, errStr, http.StatusInternalServerError)
        return
    }

    parsedQuery := target.Query()
    parsedQuery.Set("ctype", "xml")
    target.RawQuery = parsedQuery.Encode()
    target.Fragment = ""

    portPosition := strings.Index(target.Host, ":")
    var hostWithoutPort string
    if portPosition >= 0 {
        hostWithoutPort = target.Host[:portPosition]
    } else {
        hostWithoutPort = target.Host
    }

    if hostWithoutPort == "" {
        log.Printf("Error occurred during parsing the url \"%s\": No host recognized.\n", formValueUrl)
        errStr := fmt.Sprintf("Error occurred during parsing the url \"%s\": No host recognized.\nAre you sure the url is correct?", formValueUrl)
        http.Error(w, errStr, http.StatusInternalServerError)
        return
    }

    allowed, err := checkTargetAllowed(hostWithoutPort, forbiddenNetworks)
    if err != nil {
        errStr := fmt.Sprintf("Error occurred during checking whether the host \"%s\" is blocked: %s.\nAre you sure the url is correct?", hostWithoutPort, err.Error())
        http.Error(w, errStr, http.StatusInternalServerError)
        return
    }

    if !allowed {
        errStr := fmt.Sprintf("Host \"%s\" of url \"%s\" is blocked.", hostWithoutPort, formValueUrl)
        http.Error(w, errStr, http.StatusForbidden)
        return
    }

    inXml, err := doRequest(target)
    if err != nil {
        errStr := fmt.Sprintf("Error occurred during fetching the url \"%s\": %s\nAre you sure the url is correct?", target.String(), err.Error())
        http.Error(w, errStr, http.StatusInternalServerError)
        return
    }

    atom, err := convertXmlToAtom(inXml)
    if err != nil {
        errStr := fmt.Sprintf("Error occurred during conversion of the url \"%s\" to atom: %s\nAre you sure the url is correct?", target.String(), err.Error())
        http.Error(w, errStr, http.StatusInternalServerError)
        return
    }

    fmt.Fprintf(w, "%s", atom)
}

func handleMain(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s", `

<title>bugzillatoatom</title>

<form action=convert>
    Convert a Bugzilla bug entry into an Atom feed. Enter an url:
    <input type="text" name="url">

`)
}

// Parses a given IP or CIDR into a CIDR. IPs are treated as CIDRs with full bitmask
func parseIPOrCIDR(str string) (*net.IPNet, error) {

    if !strings.Contains(str, "/") {
        if strings.Contains(str, ":") {
            str = str + "/128"
        } else {
            str = str + "/32"
        }
    }

    _, ipnet, err := net.ParseCIDR(str)
    return ipnet, err
}

// To allow forbiddenNetworks to be parsed as argument
type CIDRList []*net.IPNet

func (forbiddenNetworks *CIDRList) String() string {
    strs := []string{}

    for _, ipnet := range *forbiddenNetworks {
        strs = append(strs, ipnet.String())
    }

    return strings.Join(strs, ", ")
}

func (forbiddenNetworks *CIDRList) Set(value string) error {
    ipnet, err := parseIPOrCIDR(value)

    if err == nil {
        *forbiddenNetworks = append(*forbiddenNetworks, ipnet)
    }

    return err
}

func main() {
    version := flag.Bool("version", false, "Print the current version and exit")
    port := flag.Uint64("p", 9080, "Port to bind to")
    maxBugRequestReadFlag := flag.Uint64("requestsize", 1 * 1024 * 1024, "Maximum number of bytes to read during a request to another server.") // 1MiB per default
    flag.IntVar(&maxRequestsPerSecond, "persecond", 5, "Maximum number of requests to another server per second. Set to -1 to disable.")
    forbiddenNetworks := CIDRList{}
    flag.Var(&forbiddenNetworks, "b", "IP or Network in CIDR format to block. If a host is available under any blocked IP it will be blocked. Can be given multiple times.\n\tYou probably want to exclude localhost or local networks both on IPv4 and IPv6.")
    flag.Parse()

    if *version {
        log.Fatalln(getVersion())
    }

    if *maxBugRequestReadFlag & (1 << 63) != 0 {
        log.Fatalln("Too large requestsize")
    } else {
        maxBugRequestRead = int64(*maxBugRequestReadFlag)
    }

    // Add a timeout for the default http.Get() in case something goes wrong
    // on the oher side.
    http.DefaultClient = &http.Client{Timeout: time.Second * 30}

    for i := 0; i < maxRequestsPerSecond; i++ {
        go func() {
            for {
                tooManyRequestsBlocker <- true
                time.Sleep(time.Second)
            }
        }()
    }

    http.HandleFunc("/convert", func(w http.ResponseWriter, r *http.Request) { handleConvert(w, r, forbiddenNetworks) })
    http.HandleFunc("/", handleMain)

    log.Fatal(http.ListenAndServe(":" + strconv.FormatUint(*port, 10), nil))
}<commit_msg>Cleaned up logging<commit_after>package main

import "strings"
import "strconv"
import "fmt"
import "errors"
import "net"
import "net/http"
import "net/url"
import "time"
import "html"
import "log"
import "io"
import "bufio"
import "bytes"
import "flag"
import "encoding/xml"
import "golang.org/x/tools/blog/atom"

// Maximum size a request for a bug-xml is read in byte.
var maxBugRequestRead int64

// Maximum number of requests per second. Set to something negative to disable
var maxRequestsPerSecond int

// Channel to block on during too many requests in a second
var tooManyRequestsBlocker chan bool = make(chan bool)

const versionMajor = 0
const versionMinor = 1
const versionBugfix = 0
const versionGit = true

const bugzillaDateFormat = "2006-01-02 15:04:05 -0700"
const userAgentName = "bugzillatoatom"

// returns the minimum of the given values
func min(a int, b int) int {
    if a <= b {
        return a
    } else {
        return b
    }
}

func getVersion() string {
    if versionGit {
        return strconv.Itoa(versionMajor) + "." + strconv.Itoa(versionMinor) + "." + strconv.Itoa(versionBugfix) + "-git"
    } else {
        return strconv.Itoa(versionMajor) + "." + strconv.Itoa(versionMinor) + "." + strconv.Itoa(versionBugfix)
    }
}

// Read from r until given string is found. Appends toAppend afterwards if string
// is found and in any case returns the result
func readUntilString(r io.Reader, until string, toAppend string) (string, error) {
    var buffer bytes.Buffer
    eofReached := false
    rs := bufio.NewReader(io.LimitReader(r, maxBugRequestRead))

    for !eofReached {
        str, err := rs.ReadString('\n')

        if err == io.EOF {
            eofReached = true
        } else if err != nil {
            return "", errors.New(fmt.Sprintf("Error during reading from url: %s", err))
        }

        index := strings.Index(str, until)

        if index == -1 {
            buffer.WriteString(str)
        } else {
            buffer.WriteString(str[:index])
            buffer.WriteString(toAppend)
            break
        }
    }

    return buffer.String(), nil
}

// Requests the bug from given url
func doRequest(target *url.URL) (string, error) {
    request := http.Request{
        Method: http.MethodGet,
        URL: target,
        Header: http.Header{
            "User-Agent": { userAgentName + "/" + getVersion() },
        },
    }

    resp, err := http.DefaultClient.Do(&request)

    if err != nil {
        return "", errors.New(fmt.Sprintf("Error during GET to url \"%s\": %s", target, err))
    }

    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return "", errors.New(fmt.Sprintf("Request returned status code %d (%s).", resp.StatusCode, http.StatusText(resp.StatusCode)))
    }

    // TODO: maybe we should search for something more clever to abort. <attachment could be given by a user in a report
    return readUntilString(resp.Body, "<attachment", "</bug></bugzilla>")
}

// converts the given xml string into an atom feed
func convertXmlToAtom(inXml string) (string, error) {
    type Who struct {
        Name string `xml:",chardata"`
        RealName string `xml:"name,attr"`
    }

    getFormatedName := func(w Who) string {
        if w.RealName == "" {
            return w.Name
        } else {
            return w.RealName + " (" + w.Name + ")"
        }
    }

    type Comment struct {
        CommentId int `xml:"commentid"`
        CommentCount int `xml:"comment_count"`
        AttachmentID int `xml:"attachid"`
        Who Who `xml:"who"`
        When string `xml:"bug_when"`
        Text string `xml:"thetext"`
    }

    type InResult struct {
        Urlbase string `xml:"urlbase,attr"`
        BugId int `xml:"bug>bug_id"`
        Description string `xml:"bug>short_desc"`
        Comments []Comment `xml:"bug>long_desc"`
    }

    inResult := InResult{}
    err := xml.Unmarshal([]byte(inXml), &inResult)
    if err != nil {
        return "", errors.New(fmt.Sprintf("Error during unmarshalling the xml: %s", err))
    } else if len(inResult.Comments) == 0 {
        // One comment, the initial one, should always be available
        err := errors.New("Zero comments in bug. There should be at least the initial one.")
        return "", err
    }

    updateTime, err := time.Parse(bugzillaDateFormat, inResult.Comments[len(inResult.Comments)-1].When)
    if err != nil {
        return "", errors.New(fmt.Sprintf("Couldn't parse updateTime in initial comment: %s", err))
    }

    inUrl := fmt.Sprintf("%s/show_bug.cgi?id=%d", inResult.Urlbase, inResult.BugId)
    attachmentUrl := fmt.Sprintf("%s/attachment.cgi?id=", inResult.Urlbase)

    feed := &atom.Feed{
        Title: inResult.Description,
        ID: inUrl,
        Link: []atom.Link{atom.Link{Href: inUrl, Rel: "alternate"}},
        Updated: atom.Time(updateTime),
        Author: &atom.Person{Name: getFormatedName(inResult.Comments[0].Who)},
        Entry: make([]*atom.Entry, 0, len(inResult.Comments)),
    }

    for i, comment := range inResult.Comments {
        creationTime, err := time.Parse(bugzillaDateFormat, comment.When)
        if err != nil {
            return "", errors.New(fmt.Sprintf("Couldn't parse updateTime in comment %d: %s", i, err))
        }

        links := []atom.Link{atom.Link{Href: inUrl + "#c" + strconv.Itoa(comment.CommentCount), Rel: "alternate"}}
        if comment.AttachmentID != 0 {
            links = append(links, atom.Link{Href: attachmentUrl + strconv.Itoa(comment.AttachmentID), Rel: "enclosure"})
        }

        entry := &atom.Entry{
            Title: getFormatedName(comment.Who) + ": " + comment.Text[:min(100, len(comment.Text))],
            ID: inUrl + "#c" + strconv.Itoa(comment.CommentCount),
            Link: links,
            Published: atom.Time(creationTime),
            Author: &atom.Person{Name: getFormatedName(comment.Who)},
            Content: &atom.Text{Type: "html", Body: strings.Replace(html.EscapeString(comment.Text), "\n", "
", -1)},
        }

        feed.Entry = append(feed.Entry, entry)
    }

    atom, err := xml.MarshalIndent(feed, "", "\t")
    if err != nil {
        return "", errors.New(fmt.Sprintf("Error during creating the atom feed: %s", err))
    }

    return xml.Header + string(atom), nil
}

// Filters not allowed targets, defined by the given networks
// TODO: Technically an attack is possible. First return a harmless IP
//       for the check and another one later for the actual request.
func checkTargetAllowed(target string, forbiddenNetworks []*net.IPNet) (bool, error) {
    if forbiddenNetworks == nil {
        return true, nil
    }

    ips, err := net.LookupIP(target)

    if err != nil {
        return false, err
    }

    for _, ip := range ips {
        for _, ipnet := range forbiddenNetworks {
            if ipnet.Contains(ip) {
                log.Printf("Blocked target \"%s\" since it's IP %s is contained in blocked network %s.\n", target, ip, ipnet)
                return false, nil
            }
        }
    }

    return true, nil
}

func handleConvert(w http.ResponseWriter, r *http.Request, forbiddenNetworks []*net.IPNet) {
    // Block during too many requests in the last second
    if maxRequestsPerSecond >= 0 {
        <-tooManyRequestsBlocker
    }

    // Check for a possible recursive call
    if r.Header != nil {
        for _, agent := range r.Header["User-Agent"] {
            if strings.Contains(agent, userAgentName) {
                log.Printf("Blocked request by %s due to User-Agent \"%s\".\n", r.RemoteAddr, agent)
                errStr := fmt.Sprintf("User-Agent \"%s\" blocked.", r.Header["User-Agent"])
                http.Error(w, errStr, http.StatusForbidden)
                return
            }
        }
    }

    formValueUrl := r.FormValue("url")

    // if the user didn't give a protocol simply assume http
    if !(strings.HasPrefix(formValueUrl, "http://") || strings.HasPrefix(formValueUrl, "https://")) {
        formValueUrl = "http://" + formValueUrl
    }

    target, err := url.Parse(formValueUrl)

    if err != nil {
        log.Printf("Error occurred during parsing the url \"%s\": %s.\n", r.FormValue("url"), err.Error())
        errStr := fmt.Sprintf("Error occurred during parsing the url \"%s\": %s\nAre you sure the url is correct?", r.FormValue("url"), err.Error())
        http.Error(w, errStr, http.StatusInternalServerError)
        return
    }

    parsedQuery := target.Query()
    parsedQuery.Set("ctype", "xml")
    target.RawQuery = parsedQuery.Encode()
    target.Fragment = ""

    portPosition := strings.Index(target.Host, ":")
    var hostWithoutPort string
    if portPosition >= 0 {
        hostWithoutPort = target.Host[:portPosition]
    } else {
        hostWithoutPort = target.Host
    }

    if hostWithoutPort == "" {
        log.Printf("Error occurred during parsing the url \"%s\": No host recognized.\n", formValueUrl)
        errStr := fmt.Sprintf("Error occurred during parsing the url \"%s\": No host recognized.\nAre you sure the url is correct?", formValueUrl)
        http.Error(w, errStr, http.StatusInternalServerError)
        return
    }

    allowed, err := checkTargetAllowed(hostWithoutPort, forbiddenNetworks)
    if err != nil {
        log.Printf("Error occurred during checking whether the host \"%s\" is blocked: %s.", hostWithoutPort, err.Error())
        errStr := fmt.Sprintf("Error occurred during checking whether the host \"%s\" is blocked: %s.\nAre you sure the url is correct?", hostWithoutPort, err.Error())
        http.Error(w, errStr, http.StatusInternalServerError)
        return
    }

    if !allowed {
        // No log.Printf here, as it is reported further in with more information
        errStr := fmt.Sprintf("Host \"%s\" of url \"%s\" is blocked.", hostWithoutPort, formValueUrl)
        http.Error(w, errStr, http.StatusForbidden)
        return
    }

    inXml, err := doRequest(target)
    if err != nil {
        log.Printf("Error occurred during fetching the url \"%s\": %s\n", target.String(), err.Error())
        errStr := fmt.Sprintf("Error occurred during fetching the url \"%s\": %s\nAre you sure the url is correct?", target.String(), err.Error())
        http.Error(w, errStr, http.StatusInternalServerError)
        return
    }

    atom, err := convertXmlToAtom(inXml)
    if err != nil {
        log.Printf("Error occurred during conversion of the url \"%s\" to atom: %s\n", target.String(), err.Error())
        errStr := fmt.Sprintf("Error occurred during conversion of the url \"%s\" to atom: %s\nAre you sure the url is correct?", target.String(), err.Error())
        http.Error(w, errStr, http.StatusInternalServerError)
        return
    }

    fmt.Fprintf(w, "%s", atom)
}

func handleMain(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s", `

<title>bugzillatoatom</title>

<form action=convert>
    Convert a Bugzilla bug entry into an Atom feed. Enter an url:
    <input type="text" name="url">

`)
}

// Parses a given IP or CIDR into a CIDR. IPs are treated as CIDRs with full bitmask
func parseIPOrCIDR(str string) (*net.IPNet, error) {

    if !strings.Contains(str, "/") {
        if strings.Contains(str, ":") {
            str = str + "/128"
        } else {
            str = str + "/32"
        }
    }

    _, ipnet, err := net.ParseCIDR(str)
    return ipnet, err
}

// To allow forbiddenNetworks to be parsed as argument
type CIDRList []*net.IPNet

func (forbiddenNetworks *CIDRList) String() string {
    strs := []string{}

    for _, ipnet := range *forbiddenNetworks {
        strs = append(strs, ipnet.String())
    }

    return strings.Join(strs, ", ")
}

func (forbiddenNetworks *CIDRList) Set(value string) error {
    ipnet, err := parseIPOrCIDR(value)

    if err == nil {
        *forbiddenNetworks = append(*forbiddenNetworks, ipnet)
    }

    return err
}

func main() {
    version := flag.Bool("version", false, "Print the current version and exit")
    port := flag.Uint64("p", 9080, "Port to bind to")
    maxBugRequestReadFlag := flag.Uint64("requestsize", 1 * 1024 * 1024, "Maximum number of bytes to read during a request to another server.") // 1MiB per default
    flag.IntVar(&maxRequestsPerSecond, "persecond", 5, "Maximum number of requests to another server per second. Set to -1 to disable.")
    forbiddenNetworks := CIDRList{}
    flag.Var(&forbiddenNetworks, "b", "IP or Network in CIDR format to block. If a host is available under any blocked IP it will be blocked. Can be given multiple times.\n\tYou probably want to exclude localhost or local networks both on IPv4 and IPv6.")
    flag.Parse()

    if *version {
        log.Fatalln(getVersion())
    }

    if *maxBugRequestReadFlag & (1 << 63) != 0 {
        log.Fatalln("Too large requestsize")
    } else {
        maxBugRequestRead = int64(*maxBugRequestReadFlag)
    }

    // Add a timeout for the default http.Get() in case something goes wrong
    // on the oher side.
    http.DefaultClient = &http.Client{Timeout: time.Second * 30}

    for i := 0; i < maxRequestsPerSecond; i++ {
        go func() {
            for {
                tooManyRequestsBlocker <- true
                time.Sleep(time.Second)
            }
        }()
    }

    http.HandleFunc("/convert", func(w http.ResponseWriter, r *http.Request) { handleConvert(w, r, forbiddenNetworks) })
    http.HandleFunc("/", handleMain)

    log.Fatal(http.ListenAndServe(":" + strconv.FormatUint(*port, 10), nil))
}<|endoftext|>


<commit_before>/*
Copyright 2015 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 schedulercache

import (
	"fmt"
	"sync"
	"time"

	"github.com/golang/glog"
	"k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/labels"
	"k8s.io/kubernetes/pkg/util/wait"
)

var (
	cleanAssumedPeriod = 1 * time.Second
)

// New returns a Cache implementation.
// It automatically starts a go routine that manages expiration of assumed pods.
// "ttl" is how long the assumed pod will get expired.
// "stop" is the channel that would close the background goroutine.
func New(ttl time.Duration, stop <-chan struct{}) Cache {
	cache := newSchedulerCache(ttl, cleanAssumedPeriod, stop)
	cache.run()
	return cache
}

type schedulerCache struct {
	stop   <-chan struct{}
	ttl    time.Duration
	period time.Duration

	// This mutex guards all fields within this cache struct.
	mu sync.Mutex
	// a set of assumed pod keys.
	// The key could further be used to get an entry in podStates.
	assumedPods map[string]bool
	// a map from pod key to podState.
	podStates map[string]*podState
	nodes     map[string]*NodeInfo
}

type podState struct {
	pod *api.Pod
	// Used by assumedPod to determinate expiration.
	deadline *time.Time
}

func newSchedulerCache(ttl, period time.Duration, stop <-chan struct{}) *schedulerCache {
	return &schedulerCache{
		ttl:    ttl,
		period: period,
		stop:   stop,

		nodes:       make(map[string]*NodeInfo),
		assumedPods: make(map[string]bool),
		podStates:   make(map[string]*podState),
	}
}

func (cache *schedulerCache) UpdateNodeNameToInfoMap(nodeNameToInfo map[string]*NodeInfo) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()
	for name, info := range cache.nodes {
		if current, ok := nodeNameToInfo[name]; !ok || current.generation != info.generation {
			nodeNameToInfo[name] = info.Clone()
		}
	}
	for name := range nodeNameToInfo {
		if _, ok := cache.nodes[name]; !ok {
			delete(nodeNameToInfo, name)
		}
	}
	return nil
}

func (cache *schedulerCache) List(selector labels.Selector) ([]*api.Pod, error) {
	cache.mu.Lock()
	defer cache.mu.Unlock()
	var pods []*api.Pod
	for _, info := range cache.nodes {
		for _, pod := range info.pods {
			if selector.Matches(labels.Set(pod.Labels)) {
				pods = append(pods, pod)
			}
		}
	}
	return pods, nil
}

func (cache *schedulerCache) AssumePod(pod *api.Pod) error {
	return cache.assumePod(pod, time.Now())
}

// assumePod exists for making test deterministic by taking time as input argument.
func (cache *schedulerCache) assumePod(pod *api.Pod, now time.Time) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()

	key, err := getPodKey(pod)
	if err != nil {
		return err
	}
	if _, ok := cache.podStates[key]; ok {
		return fmt.Errorf("pod state wasn't initial but get assumed. Pod key: %v", key)
	}

	cache.addPod(pod)
	dl := now.Add(cache.ttl)
	ps := &podState{
		pod:      pod,
		deadline: &dl,
	}
	cache.podStates[key] = ps
	cache.assumedPods[key] = true
	return nil
}

func (cache *schedulerCache) ForgetPod(pod *api.Pod) error {
	key, err := getPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()

	_, ok := cache.podStates[key]
	switch {
	// Only assumed pod can be forgotten.
	case ok && cache.assumedPods[key]:
		err := cache.removePod(pod)
		if err != nil {
			return err
		}
		delete(cache.assumedPods, key)
		delete(cache.podStates, key)
	default:
		return fmt.Errorf("pod state wasn't assumed but get forgotten. Pod key: %v", key)
	}
	return nil
}

func (cache *schedulerCache) AddPod(pod *api.Pod) error {
	key, err := getPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()

	_, ok := cache.podStates[key]
	switch {
	case ok && cache.assumedPods[key]:
		delete(cache.assumedPods, key)
		cache.podStates[key].deadline = nil
	case !ok:
		// Pod was expired. We should add it back.
		cache.addPod(pod)
		ps := &podState{
			pod: pod,
		}
		cache.podStates[key] = ps
	default:
		return fmt.Errorf("pod was already in added state. Pod key: %v", key)
	}
	return nil
}

func (cache *schedulerCache) UpdatePod(oldPod, newPod *api.Pod) error {
	key, err := getPodKey(oldPod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()

	_, ok := cache.podStates[key]
	switch {
	// An assumed pod won't have Update/Remove event. It needs to have Add event
	// before Update event, in which case the state would change from Assumed to Added.
	case ok && !cache.assumedPods[key]:
		if err := cache.updatePod(oldPod, newPod); err != nil {
			return err
		}
	default:
		return fmt.Errorf("pod state wasn't added but get updated. Pod key: %v", key)
	}
	return nil
}

func (cache *schedulerCache) updatePod(oldPod, newPod *api.Pod) error {
	if err := cache.removePod(oldPod); err != nil {
		return err
	}
	cache.addPod(newPod)
	return nil
}

func (cache *schedulerCache) addPod(pod *api.Pod) {
	n, ok := cache.nodes[pod.Spec.NodeName]
	if !ok {
		n = NewNodeInfo()
		cache.nodes[pod.Spec.NodeName] = n
	}
	n.addPod(pod)
}

func (cache *schedulerCache) removePod(pod *api.Pod) error {
	n := cache.nodes[pod.Spec.NodeName]
	if err := n.removePod(pod); err != nil {
		return err
	}
	if len(n.pods) == 0 && n.node == nil {
		delete(cache.nodes, pod.Spec.NodeName)
	}
	return nil
}

func (cache *schedulerCache) RemovePod(pod *api.Pod) error {
	key, err := getPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()

	cachedstate, ok := cache.podStates[key]
	switch {
	// An assumed pod won't have Delete/Remove event. It needs to have Add event
	// before Remove event, in which case the state would change from Assumed to Added.
	case ok && !cache.assumedPods[key]:
		err := cache.removePod(cachedstate.pod)
		if err != nil {
			return err
		}
		delete(cache.podStates, key)
	default:
		return fmt.Errorf("pod state wasn't added but get removed. Pod key: %v", key)
	}
	return nil
}

func (cache *schedulerCache) AddNode(node *api.Node) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()

	n, ok := cache.nodes[node.Name]
	if !ok {
		n = NewNodeInfo()
		cache.nodes[node.Name] = n
	}
	return n.SetNode(node)
}

func (cache *schedulerCache) UpdateNode(oldNode, newNode *api.Node) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()

	n, ok := cache.nodes[newNode.Name]
	if !ok {
		n = NewNodeInfo()
		cache.nodes[newNode.Name] = n
	}
	return n.SetNode(newNode)
}

func (cache *schedulerCache) RemoveNode(node *api.Node) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()

	n := cache.nodes[node.Name]
	if err := n.RemoveNode(node); err != nil {
		return err
	}
	// We remove NodeInfo for this node only if there aren't any pods on this node.
	// We can't do it unconditionally, because notifications about pods are delivered
	// in a different watch, and thus can potentially be observed later, even though
	// they happened before node removal.
	if len(n.pods) == 0 && n.node == nil {
		delete(cache.nodes, node.Name)
	}
	return nil
}

func (cache *schedulerCache) run() {
	go wait.Until(cache.cleanupExpiredAssumedPods, cache.period, cache.stop)
}

func (cache *schedulerCache) cleanupExpiredAssumedPods() {
	cache.cleanupAssumedPods(time.Now())
}

// cleanupAssumedPods exists for making test deterministic by taking time as input argument.
func (cache *schedulerCache) cleanupAssumedPods(now time.Time) {
	cache.mu.Lock()
	defer cache.mu.Unlock()

	// The size of assumedPods should be small
	for key := range cache.assumedPods {
		ps, ok := cache.podStates[key]
		if !ok {
			panic("Key found in assumed set but not in podStates. Potentially a logical error.")
		}
		if now.After(*ps.deadline) {
			if err := cache.expirePod(key, ps); err != nil {
				glog.Errorf(" expirePod failed for %s: %v", key, err)
			}
		}
	}
}

func (cache *schedulerCache) expirePod(key string, ps *podState) error {
	if err := cache.removePod(ps.pod); err != nil {
		return err
	}
	delete(cache.assumedPods, key)
	delete(cache.podStates, key)
	return nil
}
<commit_msg>Try self-repair scheduler cache or panic<commit_after>/*
Copyright 2015 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 schedulercache

import (
	"fmt"
	"sync"
	"time"

	"github.com/golang/glog"
	"k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/labels"
	"k8s.io/kubernetes/pkg/util/wait"
)

var (
	cleanAssumedPeriod = 1 * time.Second
)

// New returns a Cache implementation.
// It automatically starts a go routine that manages expiration of assumed pods.
// "ttl" is how long the assumed pod will get expired.
// "stop" is the channel that would close the background goroutine.
func New(ttl time.Duration, stop <-chan struct{}) Cache {
	cache := newSchedulerCache(ttl, cleanAssumedPeriod, stop)
	cache.run()
	return cache
}

type schedulerCache struct {
	stop   <-chan struct{}
	ttl    time.Duration
	period time.Duration

	// This mutex guards all fields within this cache struct.
	mu sync.Mutex
	// a set of assumed pod keys.
	// The key could further be used to get an entry in podStates.
	assumedPods map[string]bool
	// a map from pod key to podState.
	podStates map[string]*podState
	nodes     map[string]*NodeInfo
}

type podState struct {
	pod *api.Pod
	// Used by assumedPod to determinate expiration.
	deadline *time.Time
}

func newSchedulerCache(ttl, period time.Duration, stop <-chan struct{}) *schedulerCache {
	return &schedulerCache{
		ttl:    ttl,
		period: period,
		stop:   stop,

		nodes:       make(map[string]*NodeInfo),
		assumedPods: make(map[string]bool),
		podStates:   make(map[string]*podState),
	}
}

func (cache *schedulerCache) UpdateNodeNameToInfoMap(nodeNameToInfo map[string]*NodeInfo) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()
	for name, info := range cache.nodes {
		if current, ok := nodeNameToInfo[name]; !ok || current.generation != info.generation {
			nodeNameToInfo[name] = info.Clone()
		}
	}
	for name := range nodeNameToInfo {
		if _, ok := cache.nodes[name]; !ok {
			delete(nodeNameToInfo, name)
		}
	}
	return nil
}

func (cache *schedulerCache) List(selector labels.Selector) ([]*api.Pod, error) {
	cache.mu.Lock()
	defer cache.mu.Unlock()
	var pods []*api.Pod
	for _, info := range cache.nodes {
		for _, pod := range info.pods {
			if selector.Matches(labels.Set(pod.Labels)) {
				pods = append(pods, pod)
			}
		}
	}
	return pods, nil
}

func (cache *schedulerCache) AssumePod(pod *api.Pod) error {
	return cache.assumePod(pod, time.Now())
}

// assumePod exists for making test deterministic by taking time as input argument.
func (cache *schedulerCache) assumePod(pod *api.Pod, now time.Time) error {
	key, err := getPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()
	if _, ok := cache.podStates[key]; ok {
		return fmt.Errorf("pod %v state wasn't initial but get assumed", key)
	}

	cache.addPod(pod)
	dl := now.Add(cache.ttl)
	ps := &podState{
		pod:      pod,
		deadline: &dl,
	}
	cache.podStates[key] = ps
	cache.assumedPods[key] = true
	return nil
}

func (cache *schedulerCache) ForgetPod(pod *api.Pod) error {
	key, err := getPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()

	currState, ok := cache.podStates[key]
	if currState.pod.Spec.NodeName != pod.Spec.NodeName {
		return fmt.Errorf("pod %v state was assumed on a different node", key)
	}

	switch {
	// Only assumed pod can be forgotten.
	case ok && cache.assumedPods[key]:
		err := cache.removePod(pod)
		if err != nil {
			return err
		}
		delete(cache.assumedPods, key)
		delete(cache.podStates, key)
	default:
		return fmt.Errorf("pod %v state wasn't assumed but get forgotten", key)
	}
	return nil
}

// Assumes that lock is already acquired.
func (cache *schedulerCache) addPod(pod *api.Pod) {
	n, ok := cache.nodes[pod.Spec.NodeName]
	if !ok {
		n = NewNodeInfo()
		cache.nodes[pod.Spec.NodeName] = n
	}
	n.addPod(pod)
}

// Assumes that lock is already acquired.
func (cache *schedulerCache) updatePod(oldPod, newPod *api.Pod) error {
	if err := cache.removePod(oldPod); err != nil {
		return err
	}
	cache.addPod(newPod)
	return nil
}

// Assumes that lock is already acquired.
func (cache *schedulerCache) removePod(pod *api.Pod) error {
	n := cache.nodes[pod.Spec.NodeName]
	if err := n.removePod(pod); err != nil {
		return err
	}
	if len(n.pods) == 0 && n.node == nil {
		delete(cache.nodes, pod.Spec.NodeName)
	}
	return nil
}

func (cache *schedulerCache) AddPod(pod *api.Pod) error {
	key, err := getPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()

	currState, ok := cache.podStates[key]
	switch {
	case ok && cache.assumedPods[key]:
		if currState.pod.Spec.NodeName != pod.Spec.NodeName {
			// The pod was added to a different node than it was assumed to.
			glog.Warningf("Pod %v assumed to a different node than added to.", key)
			// Clean this up.
			cache.removePod(currState.pod)
			cache.addPod(pod)
		}
		delete(cache.assumedPods, key)
		cache.podStates[key].deadline = nil
	case !ok:
		// Pod was expired. We should add it back.
		cache.addPod(pod)
		ps := &podState{
			pod: pod,
		}
		cache.podStates[key] = ps
	default:
		return fmt.Errorf("pod was already in added state. Pod key: %v", key)
	}
	return nil
}

func (cache *schedulerCache) UpdatePod(oldPod, newPod *api.Pod) error {
	key, err := getPodKey(oldPod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()

	currState, ok := cache.podStates[key]
	switch {
	// An assumed pod won't have Update/Remove event. It needs to have Add event
	// before Update event, in which case the state would change from Assumed to Added.
	case ok && !cache.assumedPods[key]:
		if currState.pod.Spec.NodeName != newPod.Spec.NodeName {
			glog.Errorf("Pod %v updated on a different node than previously added to.", key)
			glog.Fatalf("Schedulercache is corrupted and can badly affect scheduling decisions")
		}
		if err := cache.updatePod(oldPod, newPod); err != nil {
			return err
		}
	default:
		return fmt.Errorf("pod %v state wasn't added but get updated", key)
	}
	return nil
}

func (cache *schedulerCache) RemovePod(pod *api.Pod) error {
	key, err := getPodKey(pod)
	if err != nil {
		return err
	}

	cache.mu.Lock()
	defer cache.mu.Unlock()

	currState, ok := cache.podStates[key]
	switch {
	// An assumed pod won't have Delete/Remove event. It needs to have Add event
	// before Remove event, in which case the state would change from Assumed to Added.
	case ok && !cache.assumedPods[key]:
		if currState.pod.Spec.NodeName != pod.Spec.NodeName {
			glog.Errorf("Pod %v removed from a different node than previously added to.", key)
			glog.Fatalf("Schedulercache is corrupted and can badly affect scheduling decisions")
		}
		err := cache.removePod(currState.pod)
		if err != nil {
			return err
		}
		delete(cache.podStates, key)
	default:
		return fmt.Errorf("pod state wasn't added but get removed. Pod key: %v", key)
	}
	return nil
}

func (cache *schedulerCache) AddNode(node *api.Node) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()

	n, ok := cache.nodes[node.Name]
	if !ok {
		n = NewNodeInfo()
		cache.nodes[node.Name] = n
	}
	return n.SetNode(node)
}

func (cache *schedulerCache) UpdateNode(oldNode, newNode *api.Node) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()

	n, ok := cache.nodes[newNode.Name]
	if !ok {
		n = NewNodeInfo()
		cache.nodes[newNode.Name] = n
	}
	return n.SetNode(newNode)
}

func (cache *schedulerCache) RemoveNode(node *api.Node) error {
	cache.mu.Lock()
	defer cache.mu.Unlock()

	n := cache.nodes[node.Name]
	if err := n.RemoveNode(node); err != nil {
		return err
	}
	// We remove NodeInfo for this node only if there aren't any pods on this node.
	// We can't do it unconditionally, because notifications about pods are delivered
	// in a different watch, and thus can potentially be observed later, even though
	// they happened before node removal.
	if len(n.pods) == 0 && n.node == nil {
		delete(cache.nodes, node.Name)
	}
	return nil
}

func (cache *schedulerCache) run() {
	go wait.Until(cache.cleanupExpiredAssumedPods, cache.period, cache.stop)
}

func (cache *schedulerCache) cleanupExpiredAssumedPods() {
	cache.cleanupAssumedPods(time.Now())
}

// cleanupAssumedPods exists for making test deterministic by taking time as input argument.
func (cache *schedulerCache) cleanupAssumedPods(now time.Time) {
	cache.mu.Lock()
	defer cache.mu.Unlock()

	// The size of assumedPods should be small
	for key := range cache.assumedPods {
		ps, ok := cache.podStates[key]
		if !ok {
			panic("Key found in assumed set but not in podStates. Potentially a logical error.")
		}
		if now.After(*ps.deadline) {
			if err := cache.expirePod(key, ps); err != nil {
				glog.Errorf(" expirePod failed for %s: %v", key, err)
			}
		}
	}
}

func (cache *schedulerCache) expirePod(key string, ps *podState) error {
	if err := cache.removePod(ps.pod); err != nil {
		return err
	}
	delete(cache.assumedPods, key)
	delete(cache.podStates, key)
	return nil
}
<|endoftext|>


<commit_before>package shared

import (
	"code.cloudfoundry.org/cli/actor/actionerror"
	"code.cloudfoundry.org/cli/actor/v2action"
	"code.cloudfoundry.org/cli/command"
	"code.cloudfoundry.org/cli/command/translatableerror"
)

func PollStart(ui command.UI, config command.Config, messages <-chan *v2action.LogMessage, logErrs <-chan error, appState <-chan v2action.ApplicationStateChange, apiWarnings <-chan string, apiErrs <-chan error) error {
	var breakAppState, breakWarnings, breakAPIErrs bool
	for {
		select {
		case message, ok := <-messages:
			if !ok {
				break
			}

			if message.Staging() {
				ui.DisplayLogMessage(message, false)
			}
		case state, ok := <-appState:
			if !ok {
				breakAppState = true
				break
			}

			switch state {
			case v2action.ApplicationStateStopping:
				ui.DisplayNewline()
				ui.DisplayText("Stopping app...")

			case v2action.ApplicationStateStaging:
				ui.DisplayNewline()
				ui.DisplayText("Staging app and tracing logs...")

			case v2action.ApplicationStateStarting:
				ui.DisplayNewline()
				ui.DisplayText("Waiting for app to start...")
			}
		case warning, ok := <-apiWarnings:
			if !ok {
				breakWarnings = true
				break
			}

			ui.DisplayWarning(warning)
		case logErr, ok := <-logErrs:
			if !ok {
				break
			}

			switch logErr.(type) {
			case actionerror.NOAATimeoutError:
				ui.DisplayWarning("timeout connecting to log server, no log will be shown")
			default:
				ui.DisplayWarning(logErr.Error())
			}
		case apiErr, ok := <-apiErrs:
			if !ok {
				breakAPIErrs = true
				break
			}

			switch err := apiErr.(type) {
			case actionerror.StagingFailedError:
				return translatableerror.StagingFailedError{Message: err.Error()}
			case actionerror.StagingFailedNoAppDetectedError:
				return translatableerror.StagingFailedNoAppDetectedError{BinaryName: config.BinaryName(), Message: err.Error()}
			case actionerror.StagingTimeoutError:
				return translatableerror.StagingTimeoutError{AppName: err.AppName, Timeout: err.Timeout}
			case actionerror.ApplicationInstanceCrashedError:
				return translatableerror.ApplicationUnableToStartError{AppName: err.Name, BinaryName: config.BinaryName()}
			case actionerror.ApplicationInstanceFlappingError:
				return translatableerror.ApplicationUnableToStartError{AppName: err.Name, BinaryName: config.BinaryName()}
			case actionerror.StartupTimeoutError:
				return translatableerror.StartupTimeoutError{AppName: err.Name, BinaryName: config.BinaryName()}
			default:
				return apiErr
			}
		}

		// only wait for non-nil channels to be closed
		if (appState == nil || breakAppState) &&
			(apiWarnings == nil || breakWarnings) &&
			(apiErrs == nil || breakAPIErrs) {
			return nil
		}
	}
}
<commit_msg>Remove spinlock in PollStart()<commit_after>package shared

import (
	"code.cloudfoundry.org/cli/actor/actionerror"
	"code.cloudfoundry.org/cli/actor/v2action"
	"code.cloudfoundry.org/cli/command"
	"code.cloudfoundry.org/cli/command/translatableerror"
)

func PollStart(ui command.UI, config command.Config, messages <-chan *v2action.LogMessage, logErrs <-chan error, appState <-chan v2action.ApplicationStateChange, apiWarnings <-chan string, apiErrs <-chan error) (apiError error) {
	for appState != nil || apiWarnings != nil || apiErrs != nil {
		select {
		case message, ok := <-messages:
			if !ok {
				messages = nil
				continue
			}

			if message.Staging() {
				ui.DisplayLogMessage(message, false)
			}

		case state, ok := <-appState:
			if !ok {
				appState = nil
				continue
			}

			switch state {
			case v2action.ApplicationStateStopping:
				ui.DisplayNewline()
				ui.DisplayText("Stopping app...")

			case v2action.ApplicationStateStaging:
				ui.DisplayNewline()
				ui.DisplayText("Staging app and tracing logs...")

			case v2action.ApplicationStateStarting:
				ui.DisplayNewline()
				ui.DisplayText("Waiting for app to start...")
			}

		case warning, ok := <-apiWarnings:
			if !ok {
				apiWarnings = nil
				continue
			}

			ui.DisplayWarning(warning)

		case logErr, ok := <-logErrs:
			if !ok {
				logErrs = nil
				continue
			}

			switch logErr.(type) {
			case actionerror.NOAATimeoutError:
				ui.DisplayWarning("timeout connecting to log server, no log will be shown")
			default:
				ui.DisplayWarning(logErr.Error())
			}

		case e, ok := <-apiErrs:
			if !ok {
				apiErrs = nil
				continue
			}
			switch err := e.(type) {
			case actionerror.StagingFailedError:
				apiError = translatableerror.StagingFailedError{Message: err.Error()}
			case actionerror.StagingFailedNoAppDetectedError:
				apiError = translatableerror.StagingFailedNoAppDetectedError{BinaryName: config.BinaryName(), Message: err.Error()}
			case actionerror.StagingTimeoutError:
				apiError = translatableerror.StagingTimeoutError{AppName: err.AppName, Timeout: err.Timeout}
			case actionerror.ApplicationInstanceCrashedError:
				apiError = translatableerror.ApplicationUnableToStartError{AppName: err.Name, BinaryName: config.BinaryName()}
			case actionerror.ApplicationInstanceFlappingError:
				apiError = translatableerror.ApplicationUnableToStartError{AppName: err.Name, BinaryName: config.BinaryName()}
			case actionerror.StartupTimeoutError:
				apiError = translatableerror.StartupTimeoutError{AppName: err.Name, BinaryName: config.BinaryName()}
			default:
				apiError = err
			}
			// if an api error occurred, exit immediately
			return
		}
	}
	return
}
<|endoftext|>


<commit_before>package amazonebsmock_test

import (
	"github.com/Horgix/packer-builder-amazon-ebs-mock/amazon-ebs-mock"
	"github.com/Horgix/packer-builder-amazon-ebs-mock/packer-lib-mock"
	"log"
	"testing"
)

// LoadDefaultConfig
func TestLoadDefaultConfig(t *testing.T) {
	// Expected default values for parameters
	const expectedGenerate = true
	const expectedAmount = 1
	const expectedRegion = ""

	// Initialize the Builder and its config
	builder := new(amazonebsmock.Builder)
	builder.LoadDefaultConfig()

	// Check 'Generate' parameter
	t.Run("Generate", func(t *testing.T) {
		if builder.Config.Generate != expectedGenerate {
			t.Error(
				"LoadDefaultConfig() should return a default "+
					"config with Generate to '%v' but it reported '%v'",
				expectedGenerate,
				builder.Config.Generate,
			)
		}
	})

	// Check 'Amount' parameter
	t.Run("Amount", func(t *testing.T) {
		if builder.Config.Amount != expectedAmount {
			t.Error(
				"LoadDefaultConfig() should return a default "+
					"config with Amount to '%v' but it reported '%v'",
				expectedAmount,
				builder.Config.Amount,
			)
		}
	})

	// Check 'Region' parameter
	t.Run("Region", func(t *testing.T) {
		if builder.Config.Region != expectedRegion {
			t.Error(
				"LoadDefaultConfig() should return a default "+
					"config with Region to '%v' but it reported '%v'",
				expectedRegion,
				builder.Config.Region,
			)
		}
	})
}

// TODO : Test config parsing

func TestPrepare_Rand(t *testing.T) {
	builder := new(amazonebsmock.Builder)
	builder.Prepare()

	if builder.TestMsg != "Rand seeded" {
		t.Errorf("Prepare() should initialize rand but didn't report it")
	}
}

// func TestPrepare(t *testing.T) {
// 	builder := new(amazonebsmock.Builder)
// 	builder.Prepare()
//
// 	if builder.TestMsg != "Rand seeded" {
// 		t.Errorf("Prepare() should initialize rand but didn't report it")
// 	}
// }

// Dumb test, but you know, coverage.
func TestCancel(t *testing.T) {
	builder := new(amazonebsmock.Builder)
	builder.Cancel()
	if builder.TestMsg != "This method is doing nothing" {
		t.Errorf("Cancel() should be doing nothing but it didn't report it")
	}
}

// Check that Run() method notify the user as expected
func TestRun_UiCalls(t *testing.T) {
	// Initialize and Prepare Builder
	builder := new(amazonebsmock.Builder)
	builder.Prepare()

	// Mock the "ui" part so we can count calls to ui.Say()
	ui := &packermock.MockUi{}

	log.Printf("ui.Say call SayCounter pre Run: %v", ui.SayCount)
	builder.Run(ui, nil, nil)
	log.Printf("ui.Say call SayCounter post Run: %v", ui.SayCount)

	// We should have 4 calls to ui.Say()
	const expectedSayCount = 2
	if ui.SayCount != expectedSayCount {
		t.Errorf("Number of calls to ui.Say() was incorrect, "+
			"got %d but expected %d", ui.SayCount,
			expectedSayCount)
	}
}
<commit_msg>tests: small cleanup of builder test<commit_after>package amazonebsmock_test

import (
	"github.com/Horgix/packer-builder-amazon-ebs-mock/amazon-ebs-mock"
	"github.com/Horgix/packer-builder-amazon-ebs-mock/packer-lib-mock"
	"log"
	"testing"
)

// LoadDefaultConfig
func TestLoadDefaultConfig(t *testing.T) {
	// Expected default values for parameters
	const expectedGenerate = true
	const expectedAmount = 1
	const expectedRegion = ""

	// Initialize the Builder and its config
	builder := new(amazonebsmock.Builder)
	builder.LoadDefaultConfig()

	// Check 'Generate' parameter
	t.Run("Generate", func(t *testing.T) {
		if builder.Config.Generate != expectedGenerate {
			t.Error(
				"LoadDefaultConfig() should return a default "+
					"config with Generate to '%v' but it reported '%v'",
				expectedGenerate,
				builder.Config.Generate,
			)
		}
	})

	// Check 'Amount' parameter
	t.Run("Amount", func(t *testing.T) {
		if builder.Config.Amount != expectedAmount {
			t.Error(
				"LoadDefaultConfig() should return a default "+
					"config with Amount to '%v' but it reported '%v'",
				expectedAmount,
				builder.Config.Amount,
			)
		}
	})

	// Check 'Region' parameter
	t.Run("Region", func(t *testing.T) {
		if builder.Config.Region != expectedRegion {
			t.Error(
				"LoadDefaultConfig() should return a default "+
					"config with Region to '%v' but it reported '%v'",
				expectedRegion,
				builder.Config.Region,
			)
		}
	})
}

// TODO : Test config parsing

func TestPrepare_Rand(t *testing.T) {
	builder := new(amazonebsmock.Builder)
	builder.Prepare()

	if builder.TestMsg != "Rand seeded" {
		t.Errorf("Prepare() should initialize rand but didn't report it")
	}
}

// Check that Run() method notify the user as expected
func TestRun_UiCalls(t *testing.T) {
	// Initialize and Prepare Builder
	builder := new(amazonebsmock.Builder)
	builder.Prepare()

	// Mock the "ui" part so we can count calls to ui.Say()
	ui := &packermock.MockUi{}

	log.Printf("ui.Say call SayCounter pre Run: %v", ui.SayCount)
	builder.Run(ui, nil, nil)
	log.Printf("ui.Say call SayCounter post Run: %v", ui.SayCount)

	// We should have 4 calls to ui.Say()
	const expectedSayCount = 2
	if ui.SayCount != expectedSayCount {
		t.Errorf("Number of calls to ui.Say() was incorrect, "+
			"got %d but expected %d", ui.SayCount,
			expectedSayCount)
	}
}

// Dumb test, but you know, coverage.
func TestCancel(t *testing.T) {
	builder := new(amazonebsmock.Builder)
	builder.Cancel()

	if builder.TestMsg != "This method is doing nothing" {
		t.Errorf("Cancel() should be doing nothing but it didn't report it")
	}
}
<|endoftext|>