<commit_before>/*
 * 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 sdk

import (
	"fmt"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/endpoints"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/errors"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses"
	"net"
	"net/http"
	"strconv"
	"sync"
)

// this value will be replaced while build: -ldflags="-X sdk.version=x.x.x"
var Version = "0.0.1"

type Client struct {
	regionId       string
	config         *Config
	signer         auth.Signer
	httpClient     *http.Client
	asyncTaskQueue chan func()

	debug     bool
	isRunning bool
	// void "panic(write to close channel)" cause of addAsync() after Shutdown()
	asyncChanLock *sync.RWMutex
}

func (client *Client) Init() (err error) {
	panic("not support yet")
}

func (client *Client) InitWithOptions(regionId string, config *Config, credential auth.Credential) (err error) {
	client.isRunning = true
	client.asyncChanLock = new(sync.RWMutex)
	client.regionId = regionId
	client.config = config
	if err != nil {
		return
	}
	client.httpClient = &http.Client{}

	if config.HttpTransport != nil {
		client.httpClient.Transport = config.HttpTransport
	}

	if config.Timeout > 0 {
		client.httpClient.Timeout = config.Timeout
	}

	if config.EnableAsync {
		client.EnableAsync(config.GoRoutinePoolSize, config.MaxTaskQueueSize)
	}

	client.signer, err = auth.NewSignerWithCredential(credential, client.ProcessCommonRequestWithSigner)

	return
}

func (client *Client) EnableAsync(routinePoolSize, maxTaskQueueSize int) {
	client.asyncTaskQueue = make(chan func(), maxTaskQueueSize)
	for i := 0; i < routinePoolSize; i++ {
		go func() {
			for client.isRunning {
				select {
				case task, notClosed := <-client.asyncTaskQueue:
					if notClosed {
						task()
					}
				}
			}
		}()
	}
}

func (client *Client) InitWithAccessKey(regionId, accessKeyId, accessKeySecret string) (err error) {
	config := client.InitClientConfig()
	credential := &credentials.BaseCredential{
		AccessKeyId:     accessKeyId,
		AccessKeySecret: accessKeySecret,
	}
	return client.InitWithOptions(regionId, config, credential)
}

func (client *Client) InitWithStsToken(regionId, accessKeyId, accessKeySecret, securityToken string) (err error) {
	config := client.InitClientConfig()
	credential := &credentials.StsTokenCredential{
		AccessKeyId:       accessKeyId,
		AccessKeySecret:   accessKeySecret,
		AccessKeyStsToken: securityToken,
	}
	return client.InitWithOptions(regionId, config, credential)
}

func (client *Client) InitWithRamRoleArn(regionId, accessKeyId, accessKeySecret, roleArn, roleSessionName string) (err error) {
	config := client.InitClientConfig()
	credential := &credentials.RamRoleArnCredential{
		AccessKeyId:     accessKeyId,
		AccessKeySecret: accessKeySecret,
		RoleArn:         roleArn,
		RoleSessionName: roleSessionName,
	}
	return client.InitWithOptions(regionId, config, credential)
}

func (client *Client) InitWithRsaKeyPair(regionId, publicKeyId, privateKey string, sessionExpiration int) (err error) {
	config := client.InitClientConfig()
	credential := &credentials.RsaKeyPairCredential{
		PrivateKey:        privateKey,
		PublicKeyId:       publicKeyId,
		SessionExpiration: sessionExpiration,
	}
	return client.InitWithOptions(regionId, config, credential)
}

func (client *Client) InitWithEcsRamRole(regionId, roleName string) (err error) {
	config := client.InitClientConfig()
	credential := &credentials.EcsRamRoleCredential{
		RoleName: roleName,
	}
	return client.InitWithOptions(regionId, config, credential)
}

func (client *Client) InitClientConfig() (config *Config) {
	if client.config != nil {
		return client.config
	} else {
		return NewConfig()
	}
}

func (client *Client) DoAction(request requests.AcsRequest, response responses.AcsResponse) (err error) {
	return client.DoActionWithSigner(request, response, nil)
}

func (client *Client) BuildRequestWithSigner(request requests.AcsRequest, signer auth.Signer) (err error) {
	// add clientVersion
	request.GetHeaders()["x-sdk-core-version"] = Version

	regionId := client.regionId
	if len(request.GetRegionId()) > 0 {
		regionId = request.GetRegionId()
	}

	// resolve endpoint
	resolveParam := &endpoints.ResolveParam{
		Domain:               request.GetDomain(),
		Product:              request.GetProduct(),
		RegionId:             regionId,
		LocationProduct:      request.GetLocationServiceCode(),
		LocationEndpointType: request.GetLocationEndpointType(),
		CommonApi:            client.ProcessCommonRequest,
	}
	endpoint, err := endpoints.Resolve(resolveParam)
	if err != nil {
		return
	}
	request.SetDomain(endpoint)

	// init request params
	err = requests.InitParams(request)
	if err != nil {
		return
	}

	// signature
	var finalSigner auth.Signer
	if signer != nil {
		finalSigner = signer
	} else {
		finalSigner = client.signer
	}
	httpRequest, err := buildHttpRequest(request, finalSigner, regionId)
	if client.config.UserAgent != "" {
		httpRequest.Header.Set("User-Agent", client.config.UserAgent)
	}
	return err
}

func (client *Client) DoActionWithSigner(request requests.AcsRequest, response responses.AcsResponse, signer auth.Signer) (err error) {

	// add clientVersion
	request.GetHeaders()["x-sdk-core-version"] = Version

	regionId := client.regionId
	if len(request.GetRegionId()) > 0 {
		regionId = request.GetRegionId()
	}

	// resolve endpoint
	resolveParam := &endpoints.ResolveParam{
		Domain:               request.GetDomain(),
		Product:              request.GetProduct(),
		RegionId:             regionId,
		LocationProduct:      request.GetLocationServiceCode(),
		LocationEndpointType: request.GetLocationEndpointType(),
		CommonApi:            client.ProcessCommonRequest,
	}
	endpoint, err := endpoints.Resolve(resolveParam)
	if err != nil {
		return
	}
	request.SetDomain(endpoint)

	if request.GetScheme() == "" {
		request.SetScheme(client.config.Scheme)
	}
	// init request params
	err = requests.InitParams(request)
	if err != nil {
		return
	}

	// signature
	var finalSigner auth.Signer
	if signer != nil {
		finalSigner = signer
	} else {
		finalSigner = client.signer
	}
	httpRequest, err := buildHttpRequest(request, finalSigner, regionId)
	if client.config.UserAgent != "" {
		httpRequest.Header.Set("User-Agent", client.config.UserAgent)
	}
	if err != nil {
		return
	}
	var httpResponse *http.Response
	for retryTimes := 0; retryTimes <= client.config.MaxRetryTime; retryTimes++ {
		httpResponse, err = client.httpClient.Do(httpRequest)

		var timeout bool
		// receive error
		if err != nil {
			if timeout = isTimeout(err); !timeout {
				// if not timeout error, return
				return
			} else if retryTimes >= client.config.MaxRetryTime {
				// timeout but reached the max retry times, return
				timeoutErrorMsg := fmt.Sprintf(errors.TimeoutErrorMessage, strconv.Itoa(retryTimes+1), strconv.Itoa(retryTimes+1))
				err = errors.NewClientError(errors.TimeoutErrorCode, timeoutErrorMsg, err)
				return
			}
		}
		//  if status code >= 500 or timeout, will trigger retry
		if client.config.AutoRetry && (timeout || isServerError(httpResponse)) {
			// rewrite signatureNonce and signature
			httpRequest, err = buildHttpRequest(request, finalSigner, regionId)
			if err != nil {
				return
			}
			continue
		}
		break
	}
	err = responses.Unmarshal(response, httpResponse, request.GetAcceptFormat())
	// wrap server errors
	if serverErr, ok := err.(*errors.ServerError); ok {
		var wrapInfo = map[string]string{}
		wrapInfo["StringToSign"] = request.GetStringToSign()
		err = errors.WrapServerError(serverErr, wrapInfo)
	}
	return
}

func buildHttpRequest(request requests.AcsRequest, singer auth.Signer, regionId string) (httpRequest *http.Request, err error) {
	err = auth.Sign(request, singer, regionId)
	if err != nil {
		return
	}
	requestMethod := request.GetMethod()
	requestUrl := request.BuildUrl()
	body := request.GetBodyReader()
	httpRequest, err = http.NewRequest(requestMethod, requestUrl, body)
	if err != nil {
		return
	}
	for key, value := range request.GetHeaders() {
		httpRequest.Header[key] = []string{value}
	}
	// host is a special case
	if host, containsHost := request.GetHeaders()["Host"]; containsHost {
		httpRequest.Host = host
	}
	return
}

func isTimeout(err error) bool {
	if err == nil {
		return false
	}
	netErr, isNetError := err.(net.Error)
	return isNetError && netErr.Timeout()
}

func isServerError(httpResponse *http.Response) bool {
	return httpResponse.StatusCode >= http.StatusInternalServerError
}

/**
only block when any one of the following occurs:
1. the asyncTaskQueue is full, increase the queue size to avoid this
2. Shutdown() in progressing, the client is being closed
**/
func (client *Client) AddAsyncTask(task func()) (err error) {
	if client.asyncTaskQueue != nil {
		client.asyncChanLock.RLock()
		defer client.asyncChanLock.RUnlock()
		if client.isRunning {
			client.asyncTaskQueue <- task
		}
	} else {
		err = errors.NewClientError(errors.AsyncFunctionNotEnabledCode, errors.AsyncFunctionNotEnabledMessage, nil)
	}
	return
}

func (client *Client) GetConfig() *Config {
	return client.config
}

func NewClient() (client *Client, err error) {
	client = &Client{}
	err = client.Init()
	return
}

func NewClientWithOptions(regionId string, config *Config, credential auth.Credential) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithOptions(regionId, config, credential)
	return
}

func NewClientWithAccessKey(regionId, accessKeyId, accessKeySecret string) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithAccessKey(regionId, accessKeyId, accessKeySecret)
	return
}

func NewClientWithStsToken(regionId, stsAccessKeyId, stsAccessKeySecret, stsToken string) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithStsToken(regionId, stsAccessKeyId, stsAccessKeySecret, stsToken)
	return
}

func NewClientWithRamRoleArn(regionId string, accessKeyId, accessKeySecret, roleArn, roleSessionName string) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithRamRoleArn(regionId, accessKeyId, accessKeySecret, roleArn, roleSessionName)
	return
}

func NewClientWithEcsRamRole(regionId string, roleName string) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithEcsRamRole(regionId, roleName)
	return
}

func NewClientWithRsaKeyPair(regionId string, publicKeyId, privateKey string, sessionExpiration int) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithRsaKeyPair(regionId, publicKeyId, privateKey, sessionExpiration)
	return
}

// Deprecated: Use NewClientWithRamRoleArn in this package instead.
func NewClientWithStsRoleArn(regionId string, accessKeyId, accessKeySecret, roleArn, roleSessionName string) (client *Client, err error) {
	return NewClientWithRamRoleArn(regionId, accessKeyId, accessKeySecret, roleArn, roleSessionName)
}

// Deprecated: Use NewClientWithEcsRamRole in this package instead.
func NewClientWithStsRoleNameOnEcs(regionId string, roleName string) (client *Client, err error) {
	return NewClientWithEcsRamRole(regionId, roleName)
}

func (client *Client) ProcessCommonRequest(request *requests.CommonRequest) (response *responses.CommonResponse, err error) {
	request.TransToAcsRequest()
	response = responses.NewCommonResponse()
	err = client.DoAction(request, response)
	return
}

func (client *Client) ProcessCommonRequestWithSigner(request *requests.CommonRequest, signerInterface interface{}) (response *responses.CommonResponse, err error) {
	if signer, isSigner := signerInterface.(auth.Signer); isSigner {
		request.TransToAcsRequest()
		response = responses.NewCommonResponse()
		err = client.DoActionWithSigner(request, response, signer)
		return
	} else {
		panic("should not be here")
	}
}

func (client *Client) Shutdown() {
	client.signer.Shutdown()
	// lock the addAsync()
	client.asyncChanLock.Lock()
	defer client.asyncChanLock.Unlock()
	client.isRunning = false
	close(client.asyncTaskQueue)
}
<commit_msg>fix error when autoRetry is disabled<commit_after>/*
 * 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 sdk

import (
	"fmt"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/endpoints"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/errors"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses"
	"net"
	"net/http"
	"strconv"
	"sync"
)

// this value will be replaced while build: -ldflags="-X sdk.version=x.x.x"
var Version = "0.0.1"

type Client struct {
	regionId       string
	config         *Config
	signer         auth.Signer
	httpClient     *http.Client
	asyncTaskQueue chan func()

	debug     bool
	isRunning bool
	// void "panic(write to close channel)" cause of addAsync() after Shutdown()
	asyncChanLock *sync.RWMutex
}

func (client *Client) Init() (err error) {
	panic("not support yet")
}

func (client *Client) InitWithOptions(regionId string, config *Config, credential auth.Credential) (err error) {
	client.isRunning = true
	client.asyncChanLock = new(sync.RWMutex)
	client.regionId = regionId
	client.config = config
	if err != nil {
		return
	}
	client.httpClient = &http.Client{}

	if config.HttpTransport != nil {
		client.httpClient.Transport = config.HttpTransport
	}

	if config.Timeout > 0 {
		client.httpClient.Timeout = config.Timeout
	}

	if config.EnableAsync {
		client.EnableAsync(config.GoRoutinePoolSize, config.MaxTaskQueueSize)
	}

	client.signer, err = auth.NewSignerWithCredential(credential, client.ProcessCommonRequestWithSigner)

	return
}

func (client *Client) EnableAsync(routinePoolSize, maxTaskQueueSize int) {
	client.asyncTaskQueue = make(chan func(), maxTaskQueueSize)
	for i := 0; i < routinePoolSize; i++ {
		go func() {
			for client.isRunning {
				select {
				case task, notClosed := <-client.asyncTaskQueue:
					if notClosed {
						task()
					}
				}
			}
		}()
	}
}

func (client *Client) InitWithAccessKey(regionId, accessKeyId, accessKeySecret string) (err error) {
	config := client.InitClientConfig()
	credential := &credentials.BaseCredential{
		AccessKeyId:     accessKeyId,
		AccessKeySecret: accessKeySecret,
	}
	return client.InitWithOptions(regionId, config, credential)
}

func (client *Client) InitWithStsToken(regionId, accessKeyId, accessKeySecret, securityToken string) (err error) {
	config := client.InitClientConfig()
	credential := &credentials.StsTokenCredential{
		AccessKeyId:       accessKeyId,
		AccessKeySecret:   accessKeySecret,
		AccessKeyStsToken: securityToken,
	}
	return client.InitWithOptions(regionId, config, credential)
}

func (client *Client) InitWithRamRoleArn(regionId, accessKeyId, accessKeySecret, roleArn, roleSessionName string) (err error) {
	config := client.InitClientConfig()
	credential := &credentials.RamRoleArnCredential{
		AccessKeyId:     accessKeyId,
		AccessKeySecret: accessKeySecret,
		RoleArn:         roleArn,
		RoleSessionName: roleSessionName,
	}
	return client.InitWithOptions(regionId, config, credential)
}

func (client *Client) InitWithRsaKeyPair(regionId, publicKeyId, privateKey string, sessionExpiration int) (err error) {
	config := client.InitClientConfig()
	credential := &credentials.RsaKeyPairCredential{
		PrivateKey:        privateKey,
		PublicKeyId:       publicKeyId,
		SessionExpiration: sessionExpiration,
	}
	return client.InitWithOptions(regionId, config, credential)
}

func (client *Client) InitWithEcsRamRole(regionId, roleName string) (err error) {
	config := client.InitClientConfig()
	credential := &credentials.EcsRamRoleCredential{
		RoleName: roleName,
	}
	return client.InitWithOptions(regionId, config, credential)
}

func (client *Client) InitClientConfig() (config *Config) {
	if client.config != nil {
		return client.config
	} else {
		return NewConfig()
	}
}

func (client *Client) DoAction(request requests.AcsRequest, response responses.AcsResponse) (err error) {
	return client.DoActionWithSigner(request, response, nil)
}

func (client *Client) BuildRequestWithSigner(request requests.AcsRequest, signer auth.Signer) (err error) {
	// add clientVersion
	request.GetHeaders()["x-sdk-core-version"] = Version

	regionId := client.regionId
	if len(request.GetRegionId()) > 0 {
		regionId = request.GetRegionId()
	}

	// resolve endpoint
	resolveParam := &endpoints.ResolveParam{
		Domain:               request.GetDomain(),
		Product:              request.GetProduct(),
		RegionId:             regionId,
		LocationProduct:      request.GetLocationServiceCode(),
		LocationEndpointType: request.GetLocationEndpointType(),
		CommonApi:            client.ProcessCommonRequest,
	}
	endpoint, err := endpoints.Resolve(resolveParam)
	if err != nil {
		return
	}
	request.SetDomain(endpoint)

	// init request params
	err = requests.InitParams(request)
	if err != nil {
		return
	}

	// signature
	var finalSigner auth.Signer
	if signer != nil {
		finalSigner = signer
	} else {
		finalSigner = client.signer
	}
	httpRequest, err := buildHttpRequest(request, finalSigner, regionId)
	if client.config.UserAgent != "" {
		httpRequest.Header.Set("User-Agent", client.config.UserAgent)
	}
	return err
}

func (client *Client) DoActionWithSigner(request requests.AcsRequest, response responses.AcsResponse, signer auth.Signer) (err error) {

	// add clientVersion
	request.GetHeaders()["x-sdk-core-version"] = Version

	regionId := client.regionId
	if len(request.GetRegionId()) > 0 {
		regionId = request.GetRegionId()
	}

	// resolve endpoint
	resolveParam := &endpoints.ResolveParam{
		Domain:               request.GetDomain(),
		Product:              request.GetProduct(),
		RegionId:             regionId,
		LocationProduct:      request.GetLocationServiceCode(),
		LocationEndpointType: request.GetLocationEndpointType(),
		CommonApi:            client.ProcessCommonRequest,
	}
	endpoint, err := endpoints.Resolve(resolveParam)
	if err != nil {
		return
	}
	request.SetDomain(endpoint)

	if request.GetScheme() == "" {
		request.SetScheme(client.config.Scheme)
	}
	// init request params
	err = requests.InitParams(request)
	if err != nil {
		return
	}

	// signature
	var finalSigner auth.Signer
	if signer != nil {
		finalSigner = signer
	} else {
		finalSigner = client.signer
	}
	httpRequest, err := buildHttpRequest(request, finalSigner, regionId)
	if client.config.UserAgent != "" {
		httpRequest.Header.Set("User-Agent", client.config.UserAgent)
	}
	if err != nil {
		return
	}
	var httpResponse *http.Response
	for retryTimes := 0; retryTimes <= client.config.MaxRetryTime; retryTimes++ {
		httpResponse, err = client.httpClient.Do(httpRequest)

		var timeout bool
		// receive error
		if err != nil {
			if !client.config.AutoRetry {
				return
			} else if timeout = isTimeout(err); !timeout {
				// if not timeout error, return
				return
			} else if retryTimes >= client.config.MaxRetryTime {
				// timeout but reached the max retry times, return
				timeoutErrorMsg := fmt.Sprintf(errors.TimeoutErrorMessage, strconv.Itoa(retryTimes+1), strconv.Itoa(retryTimes+1))
				err = errors.NewClientError(errors.TimeoutErrorCode, timeoutErrorMsg, err)
				return
			}
		}
		//  if status code >= 500 or timeout, will trigger retry
		if client.config.AutoRetry && (timeout || isServerError(httpResponse)) {
			// rewrite signatureNonce and signature
			httpRequest, err = buildHttpRequest(request, finalSigner, regionId)
			if err != nil {
				return
			}
			continue
		}
		break
	}
	err = responses.Unmarshal(response, httpResponse, request.GetAcceptFormat())
	// wrap server errors
	if serverErr, ok := err.(*errors.ServerError); ok {
		var wrapInfo = map[string]string{}
		wrapInfo["StringToSign"] = request.GetStringToSign()
		err = errors.WrapServerError(serverErr, wrapInfo)
	}
	return
}

func buildHttpRequest(request requests.AcsRequest, singer auth.Signer, regionId string) (httpRequest *http.Request, err error) {
	err = auth.Sign(request, singer, regionId)
	if err != nil {
		return
	}
	requestMethod := request.GetMethod()
	requestUrl := request.BuildUrl()
	body := request.GetBodyReader()
	httpRequest, err = http.NewRequest(requestMethod, requestUrl, body)
	if err != nil {
		return
	}
	for key, value := range request.GetHeaders() {
		httpRequest.Header[key] = []string{value}
	}
	// host is a special case
	if host, containsHost := request.GetHeaders()["Host"]; containsHost {
		httpRequest.Host = host
	}
	return
}

func isTimeout(err error) bool {
	if err == nil {
		return false
	}
	netErr, isNetError := err.(net.Error)
	return isNetError && netErr.Timeout()
}

func isServerError(httpResponse *http.Response) bool {
	return httpResponse.StatusCode >= http.StatusInternalServerError
}

/**
only block when any one of the following occurs:
1. the asyncTaskQueue is full, increase the queue size to avoid this
2. Shutdown() in progressing, the client is being closed
**/
func (client *Client) AddAsyncTask(task func()) (err error) {
	if client.asyncTaskQueue != nil {
		client.asyncChanLock.RLock()
		defer client.asyncChanLock.RUnlock()
		if client.isRunning {
			client.asyncTaskQueue <- task
		}
	} else {
		err = errors.NewClientError(errors.AsyncFunctionNotEnabledCode, errors.AsyncFunctionNotEnabledMessage, nil)
	}
	return
}

func (client *Client) GetConfig() *Config {
	return client.config
}

func NewClient() (client *Client, err error) {
	client = &Client{}
	err = client.Init()
	return
}

func NewClientWithOptions(regionId string, config *Config, credential auth.Credential) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithOptions(regionId, config, credential)
	return
}

func NewClientWithAccessKey(regionId, accessKeyId, accessKeySecret string) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithAccessKey(regionId, accessKeyId, accessKeySecret)
	return
}

func NewClientWithStsToken(regionId, stsAccessKeyId, stsAccessKeySecret, stsToken string) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithStsToken(regionId, stsAccessKeyId, stsAccessKeySecret, stsToken)
	return
}

func NewClientWithRamRoleArn(regionId string, accessKeyId, accessKeySecret, roleArn, roleSessionName string) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithRamRoleArn(regionId, accessKeyId, accessKeySecret, roleArn, roleSessionName)
	return
}

func NewClientWithEcsRamRole(regionId string, roleName string) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithEcsRamRole(regionId, roleName)
	return
}

func NewClientWithRsaKeyPair(regionId string, publicKeyId, privateKey string, sessionExpiration int) (client *Client, err error) {
	client = &Client{}
	err = client.InitWithRsaKeyPair(regionId, publicKeyId, privateKey, sessionExpiration)
	return
}

// Deprecated: Use NewClientWithRamRoleArn in this package instead.
func NewClientWithStsRoleArn(regionId string, accessKeyId, accessKeySecret, roleArn, roleSessionName string) (client *Client, err error) {
	return NewClientWithRamRoleArn(regionId, accessKeyId, accessKeySecret, roleArn, roleSessionName)
}

// Deprecated: Use NewClientWithEcsRamRole in this package instead.
func NewClientWithStsRoleNameOnEcs(regionId string, roleName string) (client *Client, err error) {
	return NewClientWithEcsRamRole(regionId, roleName)
}

func (client *Client) ProcessCommonRequest(request *requests.CommonRequest) (response *responses.CommonResponse, err error) {
	request.TransToAcsRequest()
	response = responses.NewCommonResponse()
	err = client.DoAction(request, response)
	return
}

func (client *Client) ProcessCommonRequestWithSigner(request *requests.CommonRequest, signerInterface interface{}) (response *responses.CommonResponse, err error) {
	if signer, isSigner := signerInterface.(auth.Signer); isSigner {
		request.TransToAcsRequest()
		response = responses.NewCommonResponse()
		err = client.DoActionWithSigner(request, response, signer)
		return
	} else {
		panic("should not be here")
	}
}

func (client *Client) Shutdown() {
	client.signer.Shutdown()
	// lock the addAsync()
	client.asyncChanLock.Lock()
	defer client.asyncChanLock.Unlock()
	client.isRunning = false
	close(client.asyncTaskQueue)
}
<|endoftext|>


<commit_before>// Copyright © 2016 Nagy Károly Gábriel <karasz@jpi.io>
// This file, part of genet, is free and unencumbered software
// released into the public domain.
// For more information, please refer to <http://unlicense.org/>
//

package genetlib

import (
	"fmt"
	"strconv"
	"strings"
	"syscall"
	"time"
)

type cpustats struct {
	User       int64
	Nice       int64
	System     int64
	Idle       int64
	Iowait     int64
	Irq        int64
	Softirq    int64
	Steal      int64
	Guest      int64
	Guest_nice int64
}

type procstats struct {
	Cpu           cpustats
	Intr          string
	Ctxt          int64
	Btime         time.Time
	Processes     int64
	Procs_running int64
	Procs_blocked int64
	Softirq       string
}

type diskstats struct {
	Major             int64
	Minor             int64
	Name              string
	Reads_ok          int64
	Reads_m           int64
	Sectors_read      int64
	Time_reading      time.Duration
	Writes_ok         int64
	Writes_m          int64
	Sectors_written   int64
	Time_writting     time.Duration
	IOs_current       int64
	Time_IOs          time.Duration
	Weighted_time_IOs time.Duration
}

type mntent struct {
	fsname  string
	dir     string
	mnttype string
	opts    string
	freq    int64
	passno  int64
}

type strucdf struct {
	Name  string
	Total int64
	Free  int64
}

func (p procstats) String() string {
	return fmt.Sprintf("Cpu: %v Ctxt: %v Btime: %v Processes: %v Procs_running: %v Procs_blocked: %v SoftIRQs: %v", p.Cpu, p.Ctxt, p.Btime, p.Processes, p.Procs_running, p.Procs_blocked, p.Softirq)

}

func (d diskstats) String() string {
	return fmt.Sprintf("Name: %v Major: %v Minor: %v reads completed successfully: %v reads merged: %v sectors read: %v time spent reading: %v writes completed: %v writes merged: %v sectors written: %v time spent writing: %v I/Os currently in progress: %v time spent doing I/Os: %v weighted time spent doing I/Os: %v", d.Name, d.Major, d.Minor, d.Reads_ok, d.Reads_m, d.Sectors_read, d.Time_reading, d.Writes_ok, d.Writes_m, d.Sectors_written, d.Time_writting, d.IOs_current, d.Time_IOs, d.Weighted_time_IOs)
}

func stringtoint64(s string) int64 {
	result, _ := strconv.ParseInt(s, 10, 64)
	return result

}
func GetUptime() (time.Duration, error) {
	lines, err := ReadLines("/proc/uptime")
	if err != nil {
		return -1 * time.Second, err
	}
	uptime := strings.Fields(lines[0])[0]
	return time.ParseDuration(uptime + "s")
}

func GetStatsSys() (procstats, error) {
	var result procstats

	lines, err := ReadLines("/proc/stat")
	if err != nil {
		return result, err
	}
	for _, line := range lines {
		switch strings.Fields(line)[0] {
		case "cpu":
			result.Cpu = fillcpustats(line)
		case "intr":
			result.Intr = line[5:]
		case "ctxt":
			result.Ctxt = stringtoint64(line[5:])
		case "btime":
			result.Btime = time.Unix(stringtoint64(line[6:]), 0)
		case "processes":
			result.Processes = stringtoint64(line[10:])
		case "procs_running":
			result.Procs_running = stringtoint64(line[14:])
		case "procs_blocked":
			result.Procs_blocked = stringtoint64(line[14:])
		case "softirq":
			result.Softirq = line[8:]
		default:
			continue
		}
	}
	return result, err

}

func fillcpustats(str string) cpustats {
	var result cpustats
	result.User = stringtoint64(strings.Fields(str)[1])
	result.Nice = stringtoint64(strings.Fields(str)[2])
	result.System = stringtoint64(strings.Fields(str)[3])
	result.Idle = stringtoint64(strings.Fields(str)[4])
	result.Iowait = stringtoint64(strings.Fields(str)[5])
	result.Irq = stringtoint64(strings.Fields(str)[6])
	result.Softirq = stringtoint64(strings.Fields(str)[7])
	result.Steal = stringtoint64(strings.Fields(str)[8])
	result.Guest = stringtoint64(strings.Fields(str)[9])
	result.Guest_nice = stringtoint64(strings.Fields(str)[10])

	return result
}

func GetStatsNet() (string, error) {
	return "Not implemented", nil
}
func GetStatsAll() (string, error) {
	return "Not implemented", nil
}
func GetStatsDisk() ([]diskstats, error) {
	var result []diskstats

	lines, err := ReadLines("/proc/diskstats")
	if err != nil {
		return result, err
	}

	for _, line := range lines {
		// filter out loop devices and ram
		if !strings.HasPrefix(strings.Fields(line)[2], "ram") && !strings.HasPrefix(strings.Fields(line)[2], "loop") {
			var res diskstats
			res.Major = stringtoint64(strings.Fields(line)[0])
			res.Minor = stringtoint64(strings.Fields(line)[1])
			res.Name = strings.Fields(line)[2]
			res.Reads_ok = stringtoint64(strings.Fields(line)[3])
			res.Reads_m = stringtoint64(strings.Fields(line)[4])
			res.Sectors_read = stringtoint64(strings.Fields(line)[5])
			res.Time_reading = time.Duration(stringtoint64(strings.Fields(line)[6]) * 1000000)
			res.Writes_ok = stringtoint64(strings.Fields(line)[7])
			res.Writes_m = stringtoint64(strings.Fields(line)[8])
			res.Sectors_written = stringtoint64(strings.Fields(line)[9])
			res.Time_writting = time.Duration(stringtoint64(strings.Fields(line)[10]) * 1000000)
			res.IOs_current = stringtoint64(strings.Fields(line)[11])
			res.Time_IOs = time.Duration(stringtoint64(strings.Fields(line)[12]) * 1000000)
			res.Weighted_time_IOs = time.Duration(stringtoint64(strings.Fields(line)[13]) * 100000)
			result = append(result, res)

		}
	}

	return result, nil
}

func GetMountSpace(path string) (int64, int64, error) {

	struc := syscall.Statfs_t{}

	err := syscall.Statfs(path, &struc)
	if err != nil {
		return 0, 0, err
	}

	return int64(struc.Bsize) * int64(struc.Blocks), int64(struc.Bsize) * int64(struc.Bfree), nil
}

func parseMounts() ([]mntent, error) {
	result := []mntent{}

	lines, err := ReadLines("/proc/mounts")
	if err != nil {
		return result, err
	}
	for _, line := range lines {
		var lmnt mntent
		lmnt.fsname = strings.Fields(line)[0]
		lmnt.dir = strings.Fields(line)[1]
		lmnt.mnttype = strings.Fields(line)[2]
		lmnt.opts = strings.Fields(line)[3]
		lmnt.freq = stringtoint64(strings.Fields(line)[4])
		lmnt.passno = stringtoint64(strings.Fields(line)[5])
		result = append(result, lmnt)
	}
	return result, nil
}

func GetDF() []strucdf {
	var result []strucdf
	mounts, _ := parseMounts()
	for _, mount := range mounts {
		var st strucdf
		st.Name = mount.dir
		st.Total, st.Free, _ = GetMountSpace(mount.dir)
		//filter out devices with 0 blocks
		if st.Total != 0 {
			result = append(result, st)
		}
	}
	return result
}
<commit_msg>genet: enhanced stats command to provide net stats by parsing /proc/net/{tcp.udp}<commit_after>// Copyright © 2016 Nagy Károly Gábriel <karasz@jpi.io>
// This file, part of genet, is free and unencumbered software
// released into the public domain.
// For more information, please refer to <http://unlicense.org/>
//

package genetlib

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"syscall"
	"time"
)

var tcpState = map[string]string{
	"01": "ESTABLISHED",
	"02": "SYN_SENT",
	"03": "SYN_RECV",
	"04": "FIN_WAIT1",
	"05": "FIN_WAIT2",
	"06": "TIME_WAIT",
	"07": "CLOSE",
	"08": "CLOSE_WAIT",
	"09": "LAST_ACK",
	"0A": "LISTEN",
	"0B": "CLOSING",
}

type cpustats struct {
	User       int64
	Nice       int64
	System     int64
	Idle       int64
	Iowait     int64
	Irq        int64
	Softirq    int64
	Steal      int64
	Guest      int64
	Guest_nice int64
}

type netstats struct {
	Prot       string
	User       string
	Name       string
	Pid        string
	State      string
	LocalIp    string
	LocalPort  string
	RemoteIp   string
	RemotePort string
}

type procstats struct {
	Cpu           cpustats
	Intr          string
	Ctxt          int64
	Btime         time.Time
	Processes     int64
	Procs_running int64
	Procs_blocked int64
	Softirq       string
}

type diskstats struct {
	Major             int64
	Minor             int64
	Name              string
	Reads_ok          int64
	Reads_m           int64
	Sectors_read      int64
	Time_reading      time.Duration
	Writes_ok         int64
	Writes_m          int64
	Sectors_written   int64
	Time_writting     time.Duration
	IOs_current       int64
	Time_IOs          time.Duration
	Weighted_time_IOs time.Duration
}

type mntent struct {
	fsname  string
	dir     string
	mnttype string
	opts    string
	freq    int64
	passno  int64
}

type strucdf struct {
	Name  string
	Total int64
	Free  int64
}

func (p procstats) String() string {
	return fmt.Sprintf("Cpu: %v Ctxt: %v Btime: %v Processes: %v Procs_running: %v Procs_blocked: %v SoftIRQs: %v", p.Cpu, p.Ctxt, p.Btime, p.Processes, p.Procs_running, p.Procs_blocked, p.Softirq)

}

func (d diskstats) String() string {
	return fmt.Sprintf("Name: %v Major: %v Minor: %v reads completed successfully: %v reads merged: %v sectors read: %v time spent reading: %v writes completed: %v writes merged: %v sectors written: %v time spent writing: %v I/Os currently in progress: %v time spent doing I/Os: %v weighted time spent doing I/Os: %v", d.Name, d.Major, d.Minor, d.Reads_ok, d.Reads_m, d.Sectors_read, d.Time_reading, d.Writes_ok, d.Writes_m, d.Sectors_written, d.Time_writting, d.IOs_current, d.Time_IOs, d.Weighted_time_IOs)
}

func stringtoint64(s string) int64 {
	result, _ := strconv.ParseInt(s, 10, 64)
	return result

}

func hextodec(s string) string {
	var result string

	d, err := strconv.ParseInt(s, 16, 32)
	if err != nil {
		fmt.Println(err)
		result = ""
	}

	result = strconv.FormatInt(d, 10)
	return result
}

func getIPfromHex(ipport string) string {
	hip := strings.Split(ipport, ":")[0]
	port := hextodec(strings.Split(ipport, ":")[1])
	ip := fmt.Sprintf("%v.%v.%v.%v", hextodec(hip[6:8]), hextodec(hip[4:6]), hextodec(hip[2:4]), hextodec(hip[0:2]))
	return fmt.Sprintf("%s:%s", ip, port)
}
func getNamefromPid(pid string) string {
	exe := fmt.Sprintf("/proc/%s/exe", pid)
	path, _ := os.Readlink(exe)
	n := strings.Split(path, "/")
	name := n[len(n)-1]
	return strings.Title(name)
}

func GetUptime() (time.Duration, error) {
	lines, err := ReadLines("/proc/uptime")
	if err != nil {
		return -1 * time.Second, err
	}
	uptime := strings.Fields(lines[0])[0]
	return time.ParseDuration(uptime + "s")
}

func GetStatsSys() (procstats, error) {
	var result procstats

	lines, err := ReadLines("/proc/stat")
	if err != nil {
		return result, err
	}
	for _, line := range lines {
		switch strings.Fields(line)[0] {
		case "cpu":
			result.Cpu = fillcpustats(line)
		case "intr":
			result.Intr = line[5:]
		case "ctxt":
			result.Ctxt = stringtoint64(line[5:])
		case "btime":
			result.Btime = time.Unix(stringtoint64(line[6:]), 0)
		case "processes":
			result.Processes = stringtoint64(line[10:])
		case "procs_running":
			result.Procs_running = stringtoint64(line[14:])
		case "procs_blocked":
			result.Procs_blocked = stringtoint64(line[14:])
		case "softirq":
			result.Softirq = line[8:]
		default:
			continue
		}
	}
	return result, err

}

func fillcpustats(str string) cpustats {
	var result cpustats
	result.User = stringtoint64(strings.Fields(str)[1])
	result.Nice = stringtoint64(strings.Fields(str)[2])
	result.System = stringtoint64(strings.Fields(str)[3])
	result.Idle = stringtoint64(strings.Fields(str)[4])
	result.Iowait = stringtoint64(strings.Fields(str)[5])
	result.Irq = stringtoint64(strings.Fields(str)[6])
	result.Softirq = stringtoint64(strings.Fields(str)[7])
	result.Steal = stringtoint64(strings.Fields(str)[8])
	result.Guest = stringtoint64(strings.Fields(str)[9])
	result.Guest_nice = stringtoint64(strings.Fields(str)[10])

	return result
}

func findPid(inode string) string {
	pid := "-"

	d, err := filepath.Glob("/proc/[0-9]*/fd/[0-9]*")
	if err != nil {
		fmt.Println(err)
		return pid
	}

	re := regexp.MustCompile(inode)
	for _, item := range d {
		path, _ := os.Readlink(item)
		out := re.FindString(path)
		if len(out) != 0 {
			pid = strings.Split(item, "/")[2]
		}
	}
	return pid
}

func GetStatsNet() ([]netstats, error) {
	var result []netstats

	lines, err := ReadLinesNoFrills("/proc/net/tcp", 1, "")
	if err != nil {
		return result, err
	}

	for _, line := range lines {
		res := tcpudpFillLine(line, "tcp")
		result = append(result, res)
	}

	lines, err = ReadLinesNoFrills("/proc/net/udp", 1, "")
	if err != nil {
		return result, err
	}

	for _, line := range lines {
		res := tcpudpFillLine(line, "udp")
		result = append(result, res)
	}

	return result, nil
}

func tcpudpFillLine(line string, prot string) netstats {
	var res netstats
	us, _ := strconv.Atoi(strings.Fields(line)[7])
	res.Prot = prot
	res.User, _ = LookupLinuxUserById(us)
	res.Pid = findPid(strings.Fields(line)[9])
	res.Name = getNamefromPid(res.Pid)
	res.State = tcpState[strings.Fields(line)[3]]
	res.LocalIp = strings.Split(getIPfromHex(strings.Fields(line)[1]), ":")[0]
	res.LocalPort = strings.Split(getIPfromHex(strings.Fields(line)[1]), ":")[1]
	res.RemoteIp = strings.Split(getIPfromHex(strings.Fields(line)[2]), ":")[0]
	res.RemotePort = strings.Split(getIPfromHex(strings.Fields(line)[2]), ":")[1]
	return res
}

func GetStatsAll() (string, error) {
	return "Not implemented", nil
}
func GetStatsDisk() ([]diskstats, error) {
	var result []diskstats

	lines, err := ReadLines("/proc/diskstats")
	if err != nil {
		return result, err
	}

	for _, line := range lines {
		// filter out loop devices and ram
		if !strings.HasPrefix(strings.Fields(line)[2], "ram") && !strings.HasPrefix(strings.Fields(line)[2], "loop") {
			var res diskstats
			res.Major = stringtoint64(strings.Fields(line)[0])
			res.Minor = stringtoint64(strings.Fields(line)[1])
			res.Name = strings.Fields(line)[2]
			res.Reads_ok = stringtoint64(strings.Fields(line)[3])
			res.Reads_m = stringtoint64(strings.Fields(line)[4])
			res.Sectors_read = stringtoint64(strings.Fields(line)[5])
			res.Time_reading = time.Duration(stringtoint64(strings.Fields(line)[6]) * 1000000)
			res.Writes_ok = stringtoint64(strings.Fields(line)[7])
			res.Writes_m = stringtoint64(strings.Fields(line)[8])
			res.Sectors_written = stringtoint64(strings.Fields(line)[9])
			res.Time_writting = time.Duration(stringtoint64(strings.Fields(line)[10]) * 1000000)
			res.IOs_current = stringtoint64(strings.Fields(line)[11])
			res.Time_IOs = time.Duration(stringtoint64(strings.Fields(line)[12]) * 1000000)
			res.Weighted_time_IOs = time.Duration(stringtoint64(strings.Fields(line)[13]) * 100000)
			result = append(result, res)

		}
	}

	return result, nil
}

func GetMountSpace(path string) (int64, int64, error) {

	struc := syscall.Statfs_t{}

	err := syscall.Statfs(path, &struc)
	if err != nil {
		return 0, 0, err
	}

	return int64(struc.Bsize) * int64(struc.Blocks), int64(struc.Bsize) * int64(struc.Bfree), nil
}

func parseMounts() ([]mntent, error) {
	result := []mntent{}

	lines, err := ReadLines("/proc/mounts")
	if err != nil {
		return result, err
	}
	for _, line := range lines {
		var lmnt mntent
		lmnt.fsname = strings.Fields(line)[0]
		lmnt.dir = strings.Fields(line)[1]
		lmnt.mnttype = strings.Fields(line)[2]
		lmnt.opts = strings.Fields(line)[3]
		lmnt.freq = stringtoint64(strings.Fields(line)[4])
		lmnt.passno = stringtoint64(strings.Fields(line)[5])
		result = append(result, lmnt)
	}
	return result, nil
}

func GetDF() []strucdf {
	var result []strucdf
	mounts, _ := parseMounts()
	for _, mount := range mounts {
		var st strucdf
		st.Name = mount.dir
		st.Total, st.Free, _ = GetMountSpace(mount.dir)
		//filter out devices with 0 blocks
		if st.Total != 0 {
			result = append(result, st)
		}
	}
	return result
}
<|endoftext|>


<commit_before>package send

import (
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/bluele/slack"
	"github.com/tychoish/grip/level"
	"github.com/tychoish/grip/message"
)

const (
	slackClientToken = "GRIP_SLACK_CLIENT_TOKEN"
)

type slackJournal struct {
	channel  string
	hostName string
	fallback *log.Logger
	client   *slack.Slack
	*base
}

// NewSlackLogger constructs a Sender that posts messages to a slack,
// given a slack API token.
//
// You must specify the channel that will receive the messages, and
// the hostname of the current machine, which is added to the logging
// metadata.
func NewSlackLogger(name, token, channel, hostname string, l LevelInfo) (Sender, error) {
	s := &slackJournal{
		hostName: hostname,
		client:   slack.New(token),
		base:     newBase(name),
	}

	if !strings.HasPrefix(channel, "#") {
		s.channel = "#" + channel
	} else {
		s.channel = channel
	}

	if err := s.SetLevel(l); err != nil {
		return nil, err
	}

	if _, err := s.client.AuthTest(); err != nil {
		return nil, fmt.Errorf("slack authentication error: %v", err)
	}

	s.reset = func() {
		s.fallback = log.New(os.Stdout, strings.Join([]string{"[", s.Name(), "] "}, ""), log.LstdFlags)
	}

	s.reset()

	return s, nil
}

// MakeSlackLogger is equivalent to NewSlackLogger, but constructs a
// Sender reading the slack token from the environment variable
// "GRIP_SLACK_CLIENT_TOKEN".
func MakeSlackLogger(channel string) (Sender, error) {
	token := os.Getenv(slackClientToken)
	if token == "" {
		return nil, fmt.Errorf("environment variable %s not defined, cannot create slack client",
			"foo")
	}

	hostname, err := os.Hostname()
	if err != nil {
		return nil, fmt.Errorf("error resolving hostname for slack logger: %v", err)
	}

	return NewSlackLogger("", token, channel, hostname, LevelInfo{level.Trace, level.Trace})
}

func (s *slackJournal) Type() SenderType { return Slack }

func (s *slackJournal) Send(m message.Composer) {
	if !s.level.ShouldLog(m) {
		return
	}

	if fallback, err := s.doSend(m); err != nil {
		s.fallback.Println("slack error:", err.Error())
		s.fallback.Println(fallback)
	}
}

func (s *slackJournal) doSend(m message.Composer) (string, error) {
	msg := m.String()

	s.RLock()
	var channel []byte
	copy(channel, s.channel)
	params := getParams(s.name, s.hostName, m.Priority())
	s.RUnlock()

	if err := s.client.ChatPostMessage(string(channel), msg, params); err != nil {
		return fmt.Sprintf("%s: %s\n", params.Attachments[0].Fallback, msg), err
	}

	return "", nil
}

func getParams(log, host string, p level.Priority) *slack.ChatPostMessageOpt {
	params := slack.ChatPostMessageOpt{
		Attachments: []*slack.Attachment{
			{
				Fallback: fmt.Sprintf("[level=%s, process=%s, host=%s]",
					p, log, host),
				Fields: []*slack.AttachmentField{
					{
						Title: "Host",
						Value: host,
						Short: true,
					},
					{
						Title: "Process",
						Value: log,
						Short: true,
					},
					{
						Title: "Priority",
						Value: p.String(),
						Short: true,
					},
				},
			},
		},
	}

	switch p {
	case level.Emergency, level.Alert, level.Critical:
		params.Attachments[0].Color = "danger"
	case level.Warning, level.Notice:
		params.Attachments[0].Color = "warning"
	default: // includes info/debug
		params.Attachments[0].Color = "good"

	}

	return &params
}
<commit_msg>make slack sender configurable<commit_after>package send

import (
	"errors"
	"fmt"
	"log"
	"os"
	"strings"
	"sync"

	"github.com/bluele/slack"
	"github.com/tychoish/grip/level"
	"github.com/tychoish/grip/message"
)

const (
	slackClientToken = "GRIP_SLACK_CLIENT_TOKEN"
)

type slackJournal struct {
	opts     *SlackOptions
	fallback *log.Logger
	client   *slack.Slack
	*base
}

// NewSlackLogger constructs a Sender that posts messages to a slack,
// given a slack API token. Configure the slack sender using a SlackOptions struct.
func NewSlackLogger(opts *SlackOptions, token string, l LevelInfo) (Sender, error) {
	if err := opts.Validate(); err != nil {
		return nil, err
	}

	s := &slackJournal{
		opts:   opts,
		client: slack.New(token),
		base:   newBase(opts.Name),
	}

	if err := s.SetLevel(l); err != nil {
		return nil, err
	}

	s.SetName(opts.Name)

	if _, err := s.client.AuthTest(); err != nil {
		return nil, fmt.Errorf("slack authentication error: %v", err)
	}

	s.reset = func() {
		s.fallback = log.New(os.Stdout, strings.Join([]string{"[", s.Name(), "] "}, ""), log.LstdFlags)
	}

	s.reset()

	return s, nil
}

// MakeSlackLogger is equivalent to NewSlackLogger, but constructs a
// Sender reading the slack token from the environment variable
// "GRIP_SLACK_CLIENT_TOKEN".
func MakeSlackLogger(opts *SlackOptions) (Sender, error) {
	token := os.Getenv(slackClientToken)
	if token == "" {
		return nil, fmt.Errorf("environment variable %s not defined, cannot create slack client",
			"foo")
	}

	return NewSlackLogger(opts, token, LevelInfo{level.Trace, level.Trace})
}

func (s *slackJournal) Type() SenderType { return Slack }

func (s *slackJournal) Send(m message.Composer) {
	if !s.level.ShouldLog(m) {
		return
	}

	if fallback, err := s.doSend(m); err != nil {
		s.fallback.Println("slack error:", err.Error())
		s.fallback.Println(fallback)
	}
}

func (s *slackJournal) doSend(m message.Composer) (string, error) {
	msg := m.String()

	s.RLock()
	defer s.RUnlock()

	params := s.opts.getParams(m)

	if err := s.client.ChatPostMessage(s.opts.Channel, msg, params); err != nil {
		return fmt.Sprintf("%s: %s\n", params.Attachments[0].Fallback, msg), err
	}

	return "", nil
}

// SlackOptions configures the behavior for constructing messages sent
// to slack.
type SlackOptions struct {
	// Channel, Name, and Hostname are basic configurations
	// options for the sender, and control where the messages are
	// sent, which hostname the sender reports itself as, and the
	// name of the journaler.
	Channel  string
	Hostname string
	Name     string

	// Configuration options for appending structured data to the
	// message sent to slack. The BasicMetadata option appends
	// priority and host information to the message. The Fields
	// option appends structured data if the Raw method of a
	// Composer returns a message.Fields map. If you specify a set
	// of fields in the FieldsSet value, only those fields will be
	// attached to the message.
	BasicMetadata bool
	Fields        bool
	FieldsSet     map[string]struct{}
	mutex         sync.RWMutex
}

func (o *SlackOptions) fieldSetShouldInclude(name string) bool {
	if name == "time" {
		return false
	}

	o.mutex.RLock()
	defer o.mutex.RUnlock()

	if o.FieldsSet == nil {
		return true
	}

	_, ok := o.FieldsSet[name]

	return ok
}

// Validate inspects the contents SlackOptions struct and returns an
// error that reports on missing required fields (e.g. Channel and
// Name); if the hostname is missing and the os.Hostname() syscall
// fails (but supplies the Hostname as reported by Hostname if there is
// no Hostname is specified). Validate also prepends a missing "#" to
// the channel setting if the "#" character is not set.
func (o *SlackOptions) Validate() error {
	errs := []string{}
	if o.Channel == "" {
		errs = append(errs, "no channel specified")
	}

	if o.Name == "" {
		errs = append(errs, "no logger/journal name specified")
	}

	if o.FieldsSet == nil {
		o.FieldsSet = map[string]struct{}{}
	}

	if o.Hostname == "" {
		hostname, err := os.Hostname()
		if err != nil {
			errs = append(errs, err.Error())
		} else {
			o.Hostname = hostname
		}
	}

	if !strings.HasPrefix(o.Channel, "#") {
		o.Channel = "#" + o.Channel
	}

	if len(errs) > 0 {
		return errors.New(strings.Join(errs, "; "))
	}
	return nil
}

func (o *SlackOptions) getParams(m message.Composer) *slack.ChatPostMessageOpt {
	attachment := slack.Attachment{}
	fallbacks := []string{}

	p := m.Priority()
	if o.BasicMetadata {
		if o.Name != "" {
			fallbacks = append(fallbacks, fmt.Sprintf("journal=%s", o.Name))
			attachment.Fields = append(attachment.Fields,
				&slack.AttachmentField{
					Title: "Journal",
					Value: o.Name,
					Short: true,
				})

		}

		if o.Hostname != "!" && o.Hostname != "" {
			fallbacks = append(fallbacks, fmt.Sprintf("host=%s", o.Hostname))
			attachment.Fields = append(attachment.Fields,
				&slack.AttachmentField{
					Title: "Host",
					Value: o.Hostname,
					Short: true,
				})
		}

		fallbacks = append(fallbacks, fmt.Sprintf("priority=%s", p))
		attachment.Fields = append(attachment.Fields,
			&slack.AttachmentField{
				Title: "Priority",
				Value: p.String(),
				Short: true,
			})
	}

	if o.Fields {
		fields, ok := m.Raw().(message.Fields)

		if ok {
			for k, v := range fields {
				if k == "msg" && v == m.String() {
					continue
				}

				if o.fieldSetShouldInclude(k) {
					continue
				}

				fallbacks = append(fallbacks, fmt.Sprintf("%s=%v", k, v))

				attachment.Fields = append(attachment.Fields,
					&slack.AttachmentField{
						Title: k,
						Value: fmt.Sprintf("%v", v),
						Short: true,
					})
			}
		}
	}

	if len(fallbacks) > 0 {
		attachment.Fallback = fmt.Sprintf("[%s]", strings.Join(fallbacks, ", "))
	}

	switch p {
	case level.Emergency, level.Alert, level.Critical:
		attachment.Color = "danger"
	case level.Warning, level.Notice:
		attachment.Color = "warning"
	default: // includes info/debug
		attachment.Color = "good"

	}

	return &slack.ChatPostMessageOpt{
		Attachments: []*slack.Attachment{&attachment},
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2015 The Kubernetes Authors All rights reserved.

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 mungers

import (
	"bufio"
	"fmt"
	"os"
	"regexp"
	"strings"

	"k8s.io/contrib/mungegithub/features"
	"k8s.io/contrib/mungegithub/github"
	"k8s.io/kubernetes/pkg/util/sets"

	"github.com/golang/glog"
	githubapi "github.com/google/go-github/github"
	"github.com/spf13/cobra"
)

const (
	labelSizePrefix = "size/"
)

var (
	sizeRE = regexp.MustCompile("Labelling this PR as " + labelSizePrefix + "(XS|S|M|L|XL|XXL)")
)

// SizeMunger will update a label on a PR based on how many lines are changed.
// It will exclude certain files in it's calculations based on the config
// file provided in --generated-files-config
type SizeMunger struct {
	generatedFilesFile string
	genFiles           *sets.String
	genPrefixes        *[]string
}

func init() {
	s := &SizeMunger{}
	RegisterMungerOrDie(s)
	RegisterStaleComments(s)
}

// Name is the name usable in --pr-mungers
func (SizeMunger) Name() string { return "size" }

// RequiredFeatures is a slice of 'features' that must be provided
func (SizeMunger) RequiredFeatures() []string { return []string{} }

// Initialize will initialize the munger
func (s *SizeMunger) Initialize(config *github.Config, features *features.Features) error {
	return nil
}

// EachLoop is called at the start of every munge loop
func (SizeMunger) EachLoop() error { return nil }

// AddFlags will add any request flags to the cobra `cmd`
func (s *SizeMunger) AddFlags(cmd *cobra.Command, config *github.Config) {
	cmd.Flags().StringVar(&s.generatedFilesFile, "generated-files-config", "generated-files.txt", "file containing the pathname to label mappings")
}

// getGeneratedFiles returns a list of all automatically generated files in the repo. These include
// docs, deep_copy, and conversions
//
// It would be 'better' to call this for every commit but that takes
// a whole lot of time for almost always the same information, and if
// our results are slightly wrong, who cares? Instead look for the
// generated files once and if someone changed what files are generated
// we'll size slightly wrong. No biggie.
func (s *SizeMunger) getGeneratedFiles(obj *github.MungeObject) {
	if s.genFiles != nil {
		return
	}
	files := sets.NewString()
	prefixes := []string{}
	s.genFiles = &files
	s.genPrefixes = &prefixes

	file := s.generatedFilesFile
	if len(file) == 0 {
		glog.Infof("No --generated-files-config= supplied, applying no labels")
		return
	}
	fp, err := os.Open(file)
	if err != nil {
		glog.Errorf("Unable to open %q: %v", file, err)
		return
	}

	defer fp.Close()
	scanner := bufio.NewScanner(fp)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "#") || line == "" {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) != 2 {
			glog.Errorf("Invalid line in generated docs config %s: %q", file, line)
			continue
		}
		eType := fields[0]
		file := fields[1]
		if eType == "prefix" {
			prefixes = append(prefixes, file)
		} else if eType == "path" {
			files.Insert(file)
		} else if eType == "paths-from-repo" {
			docs, err := obj.GetFileContents(file, "")
			if err != nil {
				continue
			}
			docSlice := strings.Split(docs, "\n")
			files.Insert(docSlice...)
		} else {
			glog.Errorf("Invalid line in generated docs config, unknown type: %s, %q", eType, line)
			continue
		}
	}
	if scanner.Err() != nil {
		glog.Errorf("Error scanning %s: %v", file, err)
		return
	}
	s.genFiles = &files
	s.genPrefixes = &prefixes

	return
}

// Munge is the workhorse the will actually make updates to the PR
func (s *SizeMunger) Munge(obj *github.MungeObject) {
	if !obj.IsPR() {
		return
	}

	issue := obj.Issue
	pr, err := obj.GetPR()
	if err != nil {
		return
	}

	s.getGeneratedFiles(obj)
	genFiles := *s.genFiles
	genPrefixes := *s.genPrefixes

	if pr.Additions == nil {
		glog.Warningf("PR %d has nil Additions", *pr.Number)
		return
	}
	adds := *pr.Additions
	if pr.Deletions == nil {
		glog.Warningf("PR %d has nil Deletions", *pr.Number)
		return
	}
	dels := *pr.Deletions

	commits, err := obj.GetCommits()
	if err != nil {
		return
	}

	for _, c := range commits {
		for _, f := range c.Files {
			for _, p := range genPrefixes {
				if strings.HasPrefix(*f.Filename, p) {
					adds = adds - *f.Additions
					dels = dels - *f.Deletions
					continue
				}
			}
			if genFiles.Has(*f.Filename) {
				adds = adds - *f.Additions
				dels = dels - *f.Deletions
				continue
			}
		}
	}

	newSize := calculateSize(adds, dels)
	newLabel := labelSizePrefix + newSize

	existing := github.GetLabelsWithPrefix(issue.Labels, labelSizePrefix)
	needsUpdate := true
	for _, l := range existing {
		if l == newLabel {
			needsUpdate = false
			continue
		}
		obj.RemoveLabel(l)
	}
	if needsUpdate {
		obj.AddLabels([]string{newLabel})

		body := fmt.Sprintf("Labelling this PR as %s", newLabel)
		obj.WriteComment(body)
	}
}

const (
	sizeXS  = "XS"
	sizeS   = "S"
	sizeM   = "M"
	sizeL   = "L"
	sizeXL  = "XL"
	sizeXXL = "XXL"
)

func calculateSize(adds, dels int) string {
	lines := adds + dels

	// This is a totally arbitrary heuristic and is open for tweaking.
	if lines < 10 {
		return sizeXS
	}
	if lines < 30 {
		return sizeS
	}
	if lines < 100 {
		return sizeM
	}
	if lines < 500 {
		return sizeL
	}
	if lines < 1000 {
		return sizeXL
	}
	return sizeXXL
}

func (s *SizeMunger) isStaleComment(obj *github.MungeObject, comment githubapi.IssueComment) bool {
	if !mergeBotComment(comment) {
		return false
	}
	stale := sizeRE.MatchString(*comment.Body)
	if stale {
		glog.V(6).Infof("Found stale SizeMunger comment")
	}
	return stale
}

// StaleComments returns a slice of stale comments
func (s *SizeMunger) StaleComments(obj *github.MungeObject, comments []githubapi.IssueComment) []githubapi.IssueComment {
	return forEachCommentTest(obj, comments, s.isStaleComment)
}
<commit_msg>Use ListFiles to calculate pull-request size<commit_after>/*
Copyright 2015 The Kubernetes Authors All rights reserved.

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 mungers

import (
	"bufio"
	"fmt"
	"os"
	"regexp"
	"strings"

	"k8s.io/contrib/mungegithub/features"
	"k8s.io/contrib/mungegithub/github"
	"k8s.io/kubernetes/pkg/util/sets"

	"github.com/golang/glog"
	githubapi "github.com/google/go-github/github"
	"github.com/spf13/cobra"
)

const (
	labelSizePrefix = "size/"
)

var (
	sizeRE = regexp.MustCompile("Labelling this PR as " + labelSizePrefix + "(XS|S|M|L|XL|XXL)")
)

// SizeMunger will update a label on a PR based on how many lines are changed.
// It will exclude certain files in it's calculations based on the config
// file provided in --generated-files-config
type SizeMunger struct {
	generatedFilesFile string
	genFiles           *sets.String
	genPrefixes        *[]string
}

func init() {
	s := &SizeMunger{}
	RegisterMungerOrDie(s)
	RegisterStaleComments(s)
}

// Name is the name usable in --pr-mungers
func (SizeMunger) Name() string { return "size" }

// RequiredFeatures is a slice of 'features' that must be provided
func (SizeMunger) RequiredFeatures() []string { return []string{} }

// Initialize will initialize the munger
func (s *SizeMunger) Initialize(config *github.Config, features *features.Features) error {
	return nil
}

// EachLoop is called at the start of every munge loop
func (SizeMunger) EachLoop() error { return nil }

// AddFlags will add any request flags to the cobra `cmd`
func (s *SizeMunger) AddFlags(cmd *cobra.Command, config *github.Config) {
	cmd.Flags().StringVar(&s.generatedFilesFile, "generated-files-config", "generated-files.txt", "file containing the pathname to label mappings")
}

// getGeneratedFiles returns a list of all automatically generated files in the repo. These include
// docs, deep_copy, and conversions
//
// It would be 'better' to call this for every commit but that takes
// a whole lot of time for almost always the same information, and if
// our results are slightly wrong, who cares? Instead look for the
// generated files once and if someone changed what files are generated
// we'll size slightly wrong. No biggie.
func (s *SizeMunger) getGeneratedFiles(obj *github.MungeObject) {
	if s.genFiles != nil {
		return
	}
	files := sets.NewString()
	prefixes := []string{}
	s.genFiles = &files
	s.genPrefixes = &prefixes

	file := s.generatedFilesFile
	if len(file) == 0 {
		glog.Infof("No --generated-files-config= supplied, applying no labels")
		return
	}
	fp, err := os.Open(file)
	if err != nil {
		glog.Errorf("Unable to open %q: %v", file, err)
		return
	}

	defer fp.Close()
	scanner := bufio.NewScanner(fp)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "#") || line == "" {
			continue
		}
		fields := strings.Fields(line)
		if len(fields) != 2 {
			glog.Errorf("Invalid line in generated docs config %s: %q", file, line)
			continue
		}
		eType := fields[0]
		file := fields[1]
		if eType == "prefix" {
			prefixes = append(prefixes, file)
		} else if eType == "path" {
			files.Insert(file)
		} else if eType == "paths-from-repo" {
			docs, err := obj.GetFileContents(file, "")
			if err != nil {
				continue
			}
			docSlice := strings.Split(docs, "\n")
			files.Insert(docSlice...)
		} else {
			glog.Errorf("Invalid line in generated docs config, unknown type: %s, %q", eType, line)
			continue
		}
	}
	if scanner.Err() != nil {
		glog.Errorf("Error scanning %s: %v", file, err)
		return
	}
	s.genFiles = &files
	s.genPrefixes = &prefixes

	return
}

// Munge is the workhorse the will actually make updates to the PR
func (s *SizeMunger) Munge(obj *github.MungeObject) {
	if !obj.IsPR() {
		return
	}

	issue := obj.Issue
	pr, err := obj.GetPR()
	if err != nil {
		return
	}

	s.getGeneratedFiles(obj)
	genFiles := *s.genFiles
	genPrefixes := *s.genPrefixes

	if pr.Additions == nil {
		glog.Warningf("PR %d has nil Additions", *pr.Number)
		return
	}
	adds := *pr.Additions
	if pr.Deletions == nil {
		glog.Warningf("PR %d has nil Deletions", *pr.Number)
		return
	}
	dels := *pr.Deletions

	files, err := obj.ListFiles()
	if err != nil {
		return
	}

	for _, f := range files {
		for _, p := range genPrefixes {
			if strings.HasPrefix(*f.Filename, p) {
				adds = adds - *f.Additions
				dels = dels - *f.Deletions
				continue
			}
		}
		if genFiles.Has(*f.Filename) {
			adds = adds - *f.Additions
			dels = dels - *f.Deletions
			continue
		}
	}

	newSize := calculateSize(adds, dels)
	newLabel := labelSizePrefix + newSize

	existing := github.GetLabelsWithPrefix(issue.Labels, labelSizePrefix)
	needsUpdate := true
	for _, l := range existing {
		if l == newLabel {
			needsUpdate = false
			continue
		}
		obj.RemoveLabel(l)
	}
	if needsUpdate {
		obj.AddLabels([]string{newLabel})

		body := fmt.Sprintf("Labelling this PR as %s", newLabel)
		obj.WriteComment(body)
	}
}

const (
	sizeXS  = "XS"
	sizeS   = "S"
	sizeM   = "M"
	sizeL   = "L"
	sizeXL  = "XL"
	sizeXXL = "XXL"
)

func calculateSize(adds, dels int) string {
	lines := adds + dels

	// This is a totally arbitrary heuristic and is open for tweaking.
	if lines < 10 {
		return sizeXS
	}
	if lines < 30 {
		return sizeS
	}
	if lines < 100 {
		return sizeM
	}
	if lines < 500 {
		return sizeL
	}
	if lines < 1000 {
		return sizeXL
	}
	return sizeXXL
}

func (s *SizeMunger) isStaleComment(obj *github.MungeObject, comment githubapi.IssueComment) bool {
	if !mergeBotComment(comment) {
		return false
	}
	stale := sizeRE.MatchString(*comment.Body)
	if stale {
		glog.V(6).Infof("Found stale SizeMunger comment")
	}
	return stale
}

// StaleComments returns a slice of stale comments
func (s *SizeMunger) StaleComments(obj *github.MungeObject, comments []githubapi.IssueComment) []githubapi.IssueComment {
	return forEachCommentTest(obj, comments, s.isStaleComment)
}
<|endoftext|>


<commit_before>package wrapper

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/octoblu/go-logentry/logentry"
)

// Wrapper wraps in silence
type Wrapper struct {
	rw           http.ResponseWriter
	redisChannel chan []byte
	startTime    time.Time
	backendName  string
}

// New constructs a new Wrapper instance
func New(rw http.ResponseWriter, redisChannel chan []byte, startTime time.Time, backendName string) *Wrapper {
	return &Wrapper{rw, redisChannel, startTime, backendName}
}

// Header returns the header map that will be sent by
// WriteHeader
func (wrapper *Wrapper) Header() http.Header {
	return wrapper.rw.Header()
}

// Write writes the data to the connection as part of an HTTP reply.
func (wrapper *Wrapper) Write(data []byte) (int, error) {
	return wrapper.rw.Write(data)
}

// WriteHeader sends an HTTP response header with status code.
func (wrapper *Wrapper) WriteHeader(statusCode int) {
	wrapper.logTheEntry(statusCode)
	wrapper.rw.WriteHeader(statusCode)
}

func (wrapper *Wrapper) logTheEntry(statusCode int) {
	elapsedTimeNano := time.Now().UnixNano() - wrapper.startTime.UnixNano()
	elapsedTime := int(elapsedTimeNano / 1000000)

	logEntry := logentry.New("metric:traefik", "http", wrapper.backendName, "anonymous", statusCode, elapsedTime)
	logEntryBytes, err := json.Marshal(logEntry)
	logError("NewJobLogger failed: %v\n", err)

	if err != nil {
		return
	}

	select {
	case wrapper.redisChannel <- logEntryBytes:
	default:
		fmt.Fprintln(os.Stderr, "Redis not ready, skipping logging")
	}
}

func logError(fmtMessage string, err error) {
	if err == nil {
		return
	}
	fmt.Fprintf(os.Stderr, fmtMessage, err.Error())
}
<commit_msg>Log to vulcand instead of traefik<commit_after>package wrapper

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/octoblu/go-logentry/logentry"
)

// Wrapper wraps in silence
type Wrapper struct {
	rw           http.ResponseWriter
	redisChannel chan []byte
	startTime    time.Time
	backendName  string
}

// New constructs a new Wrapper instance
func New(rw http.ResponseWriter, redisChannel chan []byte, startTime time.Time, backendName string) *Wrapper {
	return &Wrapper{rw, redisChannel, startTime, backendName}
}

// Header returns the header map that will be sent by
// WriteHeader
func (wrapper *Wrapper) Header() http.Header {
	return wrapper.rw.Header()
}

// Write writes the data to the connection as part of an HTTP reply.
func (wrapper *Wrapper) Write(data []byte) (int, error) {
	return wrapper.rw.Write(data)
}

// WriteHeader sends an HTTP response header with status code.
func (wrapper *Wrapper) WriteHeader(statusCode int) {
	wrapper.logTheEntry(statusCode)
	wrapper.rw.WriteHeader(statusCode)
}

func (wrapper *Wrapper) logTheEntry(statusCode int) {
	elapsedTimeNano := time.Now().UnixNano() - wrapper.startTime.UnixNano()
	elapsedTime := int(elapsedTimeNano / 1000000)

	logEntry := logentry.New("metric:vulcand", "http", wrapper.backendName, "anonymous", statusCode, elapsedTime)
	logEntryBytes, err := json.Marshal(logEntry)
	logError("NewJobLogger failed: %v\n", err)

	if err != nil {
		return
	}

	select {
	case wrapper.redisChannel <- logEntryBytes:
	default:
		fmt.Fprintln(os.Stderr, "Redis not ready, skipping logging")
	}
}

func logError(fmtMessage string, err error) {
	if err == nil {
		return
	}
	fmt.Fprintf(os.Stderr, fmtMessage, err.Error())
}
<|endoftext|>


<commit_before>package wsproxy

import (
	"context"
	"encoding/binary"
	"io"
	"net/http"
	"strings"

	"github.com/gorilla/websocket"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/status"
	"google.golang.org/grpc/transport"

	"github.com/johanbrandhorst/protobuf/internal"
)

// Logger is the interface used by the proxy to log events
type Logger interface {
	Debugln(...interface{})
	Warnln(...interface{})
}

type noopLogger struct{}

func (n noopLogger) Debugln(_ ...interface{}) {}
func (n noopLogger) Warnln(_ ...interface{})  {}

// proxy wraps a handler with a websocket to perform
// bidirectional messaging between a gRPC backend and a web frontend.
type proxy struct {
	h      http.Handler
	logger Logger
	creds  credentials.TransportCredentials
}

// WrapServer wraps the input handler with a Websocket-to-Bidi-Streaming proxy.
func WrapServer(h http.Handler, opts ...Option) http.Handler {
	p := &proxy{
		h:      h,
		logger: noopLogger{},
	}

	for _, opt := range opts {
		opt(p)
	}

	return p
}

// Option specifies the type of function that can be used to configure the server.
type Option func(p *proxy)

// WithTransportCredentials specifies credentials to use for the transport.
func WithTransportCredentials(creds credentials.TransportCredentials) Option {
	return func(p *proxy) {
		p.creds = creds
	}
}

// WithLogger configures the proxy to use the logger for logging.
func WithLogger(logger Logger) Option {
	return func(p *proxy) {
		p.logger = logger
	}
}

// TODO: allow modification of upgrader settings?
var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		// TODO: Enforce only local origins
		return true
	},
}

func isClosedConnError(err error) bool {
	str := err.Error()
	if strings.Contains(str, "use of closed network connection") {
		return true
	}
	return websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway)
}

func (p *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if !websocket.IsWebSocketUpgrade(r) {
		p.h.ServeHTTP(w, r)
		return
	}

	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		p.logger.Warnln("Failed to upgrade Websocket:", err)
		return
	}

	defer func() {
		err = conn.Close()
		if err != nil {
			p.logger.Warnln("Failed to close connection:", err)
			return
		}
		p.logger.Debugln("Closed connection")
	}()

	ctx, cancelFn := context.WithCancel(r.Context())
	defer cancelFn()

	p.logger.Debugln("Creating new transport with addr:", r.Host)
	t, err := transport.NewClientTransport(ctx,
		transport.TargetInfo{Addr: r.Host},
		transport.ConnectOptions{
			TransportCredentials: p.creds,
		})
	if err != nil {
		closeMsg := formatCloseMessage(websocket.CloseInternalServerErr, err.Error())
		_ = conn.WriteMessage(websocket.CloseMessage, closeMsg)
		p.logger.Warnln("Failed to create transport:", err)
		return
	}
	defer func() {
		err = t.GracefulClose()
		if err != nil {
			p.logger.Warnln("Failed to close transport:", err)
		}
	}()

	p.logger.Debugln("Creating new stream with host:", r.RemoteAddr, " and method:", r.RequestURI)
	s, err := t.NewStream(ctx, &transport.CallHdr{
		Host:   r.RemoteAddr,
		Method: r.RequestURI,
	})
	if err != nil {
		closeMsg := formatCloseMessage(websocket.CloseInternalServerErr, err.Error())
		_ = conn.WriteMessage(websocket.CloseMessage, closeMsg)
		p.logger.Warnln("Failed to create stream:", err)
		return
	}

	// Read loop - reads from websocket and puts it on the stream
	go func() {
		for {
			select {
			case <-s.Context().Done():
				p.logger.Debugln("[READ] Context canceled, returning")
				return
			default:
			}
			_, payload, err := conn.ReadMessage()
			if err != nil {
				cancelFn()
				if isClosedConnError(err) {
					p.logger.Warnln("[READ] Websocket closed")
					return
				}
				p.logger.Warnln("[READ] Failed to read Websocket message:", err)
				return
			}
			p.logger.Debugln("[READ] Read payload:", payload)
			if internal.IsCloseMessage(payload) {
				err = t.Write(s, nil, &transport.Options{Last: true})
				if err == io.EOF || err == nil {
					// Do not want to cancel context here, want
					// Writer to read io.EOF then exit.
					return
				}
			} else {
				// Append header
				payload = append(make([]byte, 5), payload...)
				// Skip first byte to indicate no compression
				// TODO: Add compression?
				// Encode size of payload to byte 1-4
				binary.BigEndian.PutUint32(payload[1:5], uint32(len(payload)-5))
				err = t.Write(s, payload, &transport.Options{Last: false})
			}

			if err != nil {
				cancelFn()
				p.logger.Warnln("[READ] Failed to write message to transport:", err)
				if _, ok := err.(transport.ConnectionError); !ok {
					t.CloseStream(s, err)
				}
				return
			}
		}
	}()

	// Write loop -- take messages from stream and write to websocket
	var header [5]byte
	var msg []byte
	for {
		// Read header
		_, err := s.Read(header[:])
		if err != nil {
			if err == io.EOF {
				p.logger.Debugln("[WRITE] Stream closed")
				// Wait for status to be received
				<-s.Done()
				p.sendStatus(conn, s.Status())
			} else {
				p.logger.Warnln("[WRITE] Failed to read header:", err)
				if se, ok := err.(transport.StreamError); ok {
					p.sendStatus(conn, status.New(se.Code, se.Desc))
				} else {
					p.sendStatus(conn, status.New(codes.Internal, err.Error()))
				}
			}

			return
		}

		// TODO: Add compression?
		isCompressed := uint8(header[0]) != 0
		if isCompressed {
			// If payload is compressed, bail out
			p.logger.Warnln("[WRITE] Reply was compressed, bailing")
			p.sendStatus(conn, status.New(codes.FailedPrecondition, "Server sent compressed data"))
			return
		}
		len := int(binary.BigEndian.Uint32(header[1:]))

		// TODO: Reuse buffer and resize as necessary instead
		msg = make([]byte, int(len))
		if n, err := s.Read(msg); err != nil || n != len {
			p.logger.Warnln("[WRITE] Failed to read message:", err)
			// Wait for status to be received
			<-s.Done()
			p.sendStatus(conn, s.Status())
			return
		}

		if err = conn.WriteMessage(websocket.BinaryMessage, msg); err != nil {
			p.logger.Warnln("[WRITE] Failed to write message:", err)
			return
		}
		p.logger.Debugln("[WRITE] Sent payload:", msg)
	}

}

func formatCloseMessage(code int, message string) []byte {
	closeMsg := websocket.FormatCloseMessage(code, message)
	if len(closeMsg) > 125 {
		t := []byte("[truncated]")
		closeMsg = append(closeMsg[:125-len(t)], t...)
	}
	return closeMsg
}

func (p *proxy) sendStatus(conn *websocket.Conn, st *status.Status) {
	p.logger.Debugln("[WRITE] Sending status: Msg:", st.Message(), ", Code:", st.Code().String())

	closeMsg := formatCloseMessage(internal.FormatErrorCode(st.Code()), st.Message())
	err := conn.WriteMessage(websocket.CloseMessage, closeMsg)
	if err != nil {
		p.logger.Warnln("[WRITE] Failed to write Websocket trailer:", err)
	}

	p.logger.Debugln("[WRITE] Sent close message")
	return
}
<commit_msg>Add implicit https port when none provided<commit_after>package wsproxy

import (
	"context"
	"encoding/binary"
	"io"
	"net"
	"net/http"
	"strings"

	"github.com/gorilla/websocket"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/status"
	"google.golang.org/grpc/transport"

	"github.com/johanbrandhorst/protobuf/internal"
)

// Logger is the interface used by the proxy to log events
type Logger interface {
	Debugln(...interface{})
	Warnln(...interface{})
}

type noopLogger struct{}

func (n noopLogger) Debugln(_ ...interface{}) {}
func (n noopLogger) Warnln(_ ...interface{})  {}

// proxy wraps a handler with a websocket to perform
// bidirectional messaging between a gRPC backend and a web frontend.
type proxy struct {
	h      http.Handler
	logger Logger
	creds  credentials.TransportCredentials
}

// WrapServer wraps the input handler with a Websocket-to-Bidi-Streaming proxy.
func WrapServer(h http.Handler, opts ...Option) http.Handler {
	p := &proxy{
		h:      h,
		logger: noopLogger{},
	}

	for _, opt := range opts {
		opt(p)
	}

	return p
}

// Option specifies the type of function that can be used to configure the server.
type Option func(p *proxy)

// WithTransportCredentials specifies credentials to use for the transport.
func WithTransportCredentials(creds credentials.TransportCredentials) Option {
	return func(p *proxy) {
		p.creds = creds
	}
}

// WithLogger configures the proxy to use the logger for logging.
func WithLogger(logger Logger) Option {
	return func(p *proxy) {
		p.logger = logger
	}
}

// TODO: allow modification of upgrader settings?
var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		// TODO: Enforce only local origins
		return true
	},
}

func isClosedConnError(err error) bool {
	str := err.Error()
	if strings.Contains(str, "use of closed network connection") {
		return true
	}
	return websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway)
}

func (p *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if !websocket.IsWebSocketUpgrade(r) {
		p.h.ServeHTTP(w, r)
		return
	}

	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		p.logger.Warnln("Failed to upgrade Websocket:", err)
		return
	}

	defer func() {
		err = conn.Close()
		if err != nil {
			p.logger.Warnln("Failed to close connection:", err)
			return
		}
		p.logger.Debugln("Closed connection")
	}()

	ctx, cancelFn := context.WithCancel(r.Context())
	defer cancelFn()

	host := withPort(r.Host)
	p.logger.Debugln("Creating new transport with addr:", host)
	t, err := transport.NewClientTransport(ctx,
		transport.TargetInfo{Addr: host},
		transport.ConnectOptions{
			TransportCredentials: p.creds,
		})
	if err != nil {
		closeMsg := formatCloseMessage(websocket.CloseInternalServerErr, err.Error())
		_ = conn.WriteMessage(websocket.CloseMessage, closeMsg)
		p.logger.Warnln("Failed to create transport:", err)
		return
	}
	defer func() {
		err = t.GracefulClose()
		if err != nil {
			p.logger.Warnln("Failed to close transport:", err)
		}
	}()

	p.logger.Debugln("Creating new stream with host:", r.RemoteAddr, " and method:", r.RequestURI)
	s, err := t.NewStream(ctx, &transport.CallHdr{
		Host:   r.RemoteAddr,
		Method: r.RequestURI,
	})
	if err != nil {
		closeMsg := formatCloseMessage(websocket.CloseInternalServerErr, err.Error())
		_ = conn.WriteMessage(websocket.CloseMessage, closeMsg)
		p.logger.Warnln("Failed to create stream:", err)
		return
	}

	// Read loop - reads from websocket and puts it on the stream
	go func() {
		for {
			select {
			case <-s.Context().Done():
				p.logger.Debugln("[READ] Context canceled, returning")
				return
			default:
			}
			_, payload, err := conn.ReadMessage()
			if err != nil {
				cancelFn()
				if isClosedConnError(err) {
					p.logger.Warnln("[READ] Websocket closed")
					return
				}
				p.logger.Warnln("[READ] Failed to read Websocket message:", err)
				return
			}
			p.logger.Debugln("[READ] Read payload:", payload)
			if internal.IsCloseMessage(payload) {
				err = t.Write(s, nil, &transport.Options{Last: true})
				if err == io.EOF || err == nil {
					// Do not want to cancel context here, want
					// Writer to read io.EOF then exit.
					return
				}
			} else {
				// Append header
				payload = append(make([]byte, 5), payload...)
				// Skip first byte to indicate no compression
				// TODO: Add compression?
				// Encode size of payload to byte 1-4
				binary.BigEndian.PutUint32(payload[1:5], uint32(len(payload)-5))
				err = t.Write(s, payload, &transport.Options{Last: false})
			}

			if err != nil {
				cancelFn()
				p.logger.Warnln("[READ] Failed to write message to transport:", err)
				if _, ok := err.(transport.ConnectionError); !ok {
					t.CloseStream(s, err)
				}
				return
			}
		}
	}()

	// Write loop -- take messages from stream and write to websocket
	var header [5]byte
	var msg []byte
	for {
		// Read header
		_, err := s.Read(header[:])
		if err != nil {
			if err == io.EOF {
				p.logger.Debugln("[WRITE] Stream closed")
				// Wait for status to be received
				<-s.Done()
				p.sendStatus(conn, s.Status())
			} else {
				p.logger.Warnln("[WRITE] Failed to read header:", err)
				if se, ok := err.(transport.StreamError); ok {
					p.sendStatus(conn, status.New(se.Code, se.Desc))
				} else {
					p.sendStatus(conn, status.New(codes.Internal, err.Error()))
				}
			}

			return
		}

		// TODO: Add compression?
		isCompressed := uint8(header[0]) != 0
		if isCompressed {
			// If payload is compressed, bail out
			p.logger.Warnln("[WRITE] Reply was compressed, bailing")
			p.sendStatus(conn, status.New(codes.FailedPrecondition, "Server sent compressed data"))
			return
		}
		len := int(binary.BigEndian.Uint32(header[1:]))

		// TODO: Reuse buffer and resize as necessary instead
		msg = make([]byte, int(len))
		if n, err := s.Read(msg); err != nil || n != len {
			p.logger.Warnln("[WRITE] Failed to read message:", err)
			// Wait for status to be received
			<-s.Done()
			p.sendStatus(conn, s.Status())
			return
		}

		if err = conn.WriteMessage(websocket.BinaryMessage, msg); err != nil {
			p.logger.Warnln("[WRITE] Failed to write message:", err)
			return
		}
		p.logger.Debugln("[WRITE] Sent payload:", msg)
	}

}

func formatCloseMessage(code int, message string) []byte {
	closeMsg := websocket.FormatCloseMessage(code, message)
	if len(closeMsg) > 125 {
		t := []byte("[truncated]")
		closeMsg = append(closeMsg[:125-len(t)], t...)
	}
	return closeMsg
}

func (p *proxy) sendStatus(conn *websocket.Conn, st *status.Status) {
	p.logger.Debugln("[WRITE] Sending status: Msg:", st.Message(), ", Code:", st.Code().String())

	closeMsg := formatCloseMessage(internal.FormatErrorCode(st.Code()), st.Message())
	err := conn.WriteMessage(websocket.CloseMessage, closeMsg)
	if err != nil {
		p.logger.Warnln("[WRITE] Failed to write Websocket trailer:", err)
	}

	p.logger.Debugln("[WRITE] Sent close message")
	return
}

// withPort adds ":443" if another port isn't already present.
func withPort(host string) string {
	if _, _, err := net.SplitHostPort(host); err != nil {
		return net.JoinHostPort(host, "443")
	}
	return host
}
<|endoftext|>


<commit_before>package main

import (
	"context"
	"fmt"
	"regexp"
	"strings"

	"github.com/ericchiang/k8s"
	apiv1 "github.com/ericchiang/k8s/api/v1"
	batchv1 "github.com/ericchiang/k8s/apis/batch/v1"
	metav1 "github.com/ericchiang/k8s/apis/meta/v1"
	"github.com/rs/zerolog/log"
)

// CiBuilder wraps the k8s client
type CiBuilder struct {
	KubeClient *k8s.Client
}

// CiBuilderClient is the interface for running kubernetes commands specific to this application
type CiBuilderClient interface {
	CreateCiBuilderJob(CiBuilderParams) (*batchv1.Job, error)
}

// CiBuilderParams contains the parameters required to create a ci builder job
type CiBuilderParams struct {
	RepoFullName string
	RepoURL      string
	RepoBranch   string
	RepoRevision string
}

// CreateCiBuilderClient return a estafette ci builder client
func CreateCiBuilderClient() (ciBuilderClient CiBuilderClient, err error) {

	kubeClient, err := k8s.NewInClusterClient()
	if err != nil {
		log.Error().Err(err).Msg("Creating k8s client failed")
		return
	}

	ciBuilderClient = &CiBuilder{
		KubeClient: kubeClient,
	}

	return
}

// CreateCiBuilderJob creates an estafette-ci-builder job in Kubernetes to run the estafette build
func (cbc *CiBuilder) CreateCiBuilderJob(ciBuilderParams CiBuilderParams) (job *batchv1.Job, err error) {

	// create job name of max 63 chars
	re := regexp.MustCompile("[^a-zA-Z0-9]+")
	repoName := re.ReplaceAllString(ciBuilderParams.RepoFullName, "-")
	if len(repoName) > 50 {
		repoName = repoName[:50]
	}
	jobName := strings.ToLower(fmt.Sprintf("build-%v-%v", repoName, ciBuilderParams.RepoRevision[:6]))

	// create envvars for job
	estafetteGitURLName := "ESTAFETTE_GIT_URL"
	estafetteGitURLValue := ciBuilderParams.RepoURL
	estafetteGitBranchName := "ESTAFETTE_GIT_BRANCH"
	estafetteGitBranchValue := ciBuilderParams.RepoBranch
	estafetteGitRevisionName := "ESTAFETTE_GIT_REVISION"
	estafetteGitRevisionValue := ciBuilderParams.RepoRevision

	// other job config
	containerName := "estafette-ci-builder"
	image := fmt.Sprintf("estafette/estafette-ci-builder:%v", *estafetteCiBuilderVersion)
	restartPolicy := "Never"

	job = &batchv1.Job{
		Metadata: &metav1.ObjectMeta{
			Name:      &jobName,
			Namespace: &cbc.KubeClient.Namespace,
			Labels: map[string]string{
				"createdBy": "estafette",
			},
		},
		Spec: &batchv1.JobSpec{
			Template: &apiv1.PodTemplateSpec{
				Metadata: &metav1.ObjectMeta{
					Labels: map[string]string{
						"createdBy": "estafette",
					},
				},
				Spec: &apiv1.PodSpec{
					Containers: []*apiv1.Container{
						&apiv1.Container{
							Name:  &containerName,
							Image: &image,
							Env: []*apiv1.EnvVar{
								&apiv1.EnvVar{
									Name:  &estafetteGitURLName,
									Value: &estafetteGitURLValue,
								},
								&apiv1.EnvVar{
									Name:  &estafetteGitBranchName,
									Value: &estafetteGitBranchValue,
								},
								&apiv1.EnvVar{
									Name:  &estafetteGitRevisionName,
									Value: &estafetteGitRevisionValue,
								},
							},
						},
					},
					RestartPolicy: &restartPolicy,
				},
			},
		},
	}

	job, err = cbc.KubeClient.BatchV1().CreateJob(context.Background(), job)

	return
}
<commit_msg>track kubernetes api requests<commit_after>package main

import (
	"context"
	"fmt"
	"regexp"
	"strings"

	"github.com/ericchiang/k8s"
	apiv1 "github.com/ericchiang/k8s/api/v1"
	batchv1 "github.com/ericchiang/k8s/apis/batch/v1"
	metav1 "github.com/ericchiang/k8s/apis/meta/v1"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/rs/zerolog/log"
)

// CiBuilder wraps the k8s client
type CiBuilder struct {
	KubeClient *k8s.Client
}

// CiBuilderClient is the interface for running kubernetes commands specific to this application
type CiBuilderClient interface {
	CreateCiBuilderJob(CiBuilderParams) (*batchv1.Job, error)
}

// CiBuilderParams contains the parameters required to create a ci builder job
type CiBuilderParams struct {
	RepoFullName string
	RepoURL      string
	RepoBranch   string
	RepoRevision string
}

// CreateCiBuilderClient return a estafette ci builder client
func CreateCiBuilderClient() (ciBuilderClient CiBuilderClient, err error) {

	kubeClient, err := k8s.NewInClusterClient()
	if err != nil {
		log.Error().Err(err).Msg("Creating k8s client failed")
		return
	}

	ciBuilderClient = &CiBuilder{
		KubeClient: kubeClient,
	}

	return
}

// CreateCiBuilderJob creates an estafette-ci-builder job in Kubernetes to run the estafette build
func (cbc *CiBuilder) CreateCiBuilderJob(ciBuilderParams CiBuilderParams) (job *batchv1.Job, err error) {

	// create job name of max 63 chars
	re := regexp.MustCompile("[^a-zA-Z0-9]+")
	repoName := re.ReplaceAllString(ciBuilderParams.RepoFullName, "-")
	if len(repoName) > 50 {
		repoName = repoName[:50]
	}
	jobName := strings.ToLower(fmt.Sprintf("build-%v-%v", repoName, ciBuilderParams.RepoRevision[:6]))

	// create envvars for job
	estafetteGitURLName := "ESTAFETTE_GIT_URL"
	estafetteGitURLValue := ciBuilderParams.RepoURL
	estafetteGitBranchName := "ESTAFETTE_GIT_BRANCH"
	estafetteGitBranchValue := ciBuilderParams.RepoBranch
	estafetteGitRevisionName := "ESTAFETTE_GIT_REVISION"
	estafetteGitRevisionValue := ciBuilderParams.RepoRevision

	// other job config
	containerName := "estafette-ci-builder"
	image := fmt.Sprintf("estafette/estafette-ci-builder:%v", *estafetteCiBuilderVersion)
	restartPolicy := "Never"

	job = &batchv1.Job{
		Metadata: &metav1.ObjectMeta{
			Name:      &jobName,
			Namespace: &cbc.KubeClient.Namespace,
			Labels: map[string]string{
				"createdBy": "estafette",
			},
		},
		Spec: &batchv1.JobSpec{
			Template: &apiv1.PodTemplateSpec{
				Metadata: &metav1.ObjectMeta{
					Labels: map[string]string{
						"createdBy": "estafette",
					},
				},
				Spec: &apiv1.PodSpec{
					Containers: []*apiv1.Container{
						&apiv1.Container{
							Name:  &containerName,
							Image: &image,
							Env: []*apiv1.EnvVar{
								&apiv1.EnvVar{
									Name:  &estafetteGitURLName,
									Value: &estafetteGitURLValue,
								},
								&apiv1.EnvVar{
									Name:  &estafetteGitBranchName,
									Value: &estafetteGitBranchValue,
								},
								&apiv1.EnvVar{
									Name:  &estafetteGitRevisionName,
									Value: &estafetteGitRevisionValue,
								},
							},
						},
					},
					RestartPolicy: &restartPolicy,
				},
			},
		},
	}

	outgoingAPIRequestTotal.With(prometheus.Labels{"target": "kubernetes"}).Inc()

	job, err = cbc.KubeClient.BatchV1().CreateJob(context.Background(), job)

	return
}
<|endoftext|>


<commit_before>package deploy

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path/filepath"

	"github.com/pkg/errors"
	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
)

type RemoteDeployer struct {
	ssh             *ssh.Client
	targetDirectory string
}

func NewRemoteDeployer(s *ssh.Client, target string) (*RemoteDeployer, error) {
	d := &RemoteDeployer{
		ssh:             s,
		targetDirectory: target,
	}

	client, err := sftp.NewClient(d.ssh)
	if err != nil {
		return nil, errors.Wrap(err, "Failed to create new sftp client")
	}
	defer client.Close()

	if err := client.MkdirAll(d.targetDirectory); err != nil {
		return nil, errors.Wrap(err, "Failed to make target directory")
	}

	return d, nil
}

// targetDirectory にファイルを転送し、 path にシンボリックリンクを貼る
func (d RemoteDeployer) SendFile(body []byte, path string, permission os.FileMode) error {
	client, err := sftp.NewClient(d.ssh)
	if err != nil {
		return errors.Wrap(err, "Failed to create new sftp client")
	}
	defer client.Close()

	var target string
	if !filepath.IsAbs(path) {
		target = filepath.Join(d.targetDirectory, path)
	} else {
		filename := filepath.Base(path)
		target = filepath.Join(d.targetDirectory, filename)

		if _, err := client.Lstat(path); err == nil {
			if err := client.Remove(path); err != nil {
				return errors.Wrap(err, "Failed to remove old symbolic link")
			}
		}

		if err := client.Symlink(target, path); err != nil {
			return errors.Wrap(err, "Failed to create symbolic link")
		}
	}

	if _, err := client.Stat(target); err == nil {
		if err := client.Remove(target); err != nil {
			return errors.Wrap(err, "Failed to delete target")
		}
	}

	file, err := client.Create(target)
	if err != nil {
		return errors.Wrap(err, "Failed to create new file")
	}
	defer file.Close()

	if err := file.Chmod(permission); err != nil {
		return errors.Wrap(err, "Failed to change permission")
	}

	if _, err := file.Write(body); err != nil {
		return errors.Wrap(err, "Failed to write to file")
	}

	return nil
}

func (d RemoteDeployer) ReadSelf() ([]byte, error) {
	path, err := filepath.Abs(os.Args[0])
	if err != nil {
		return nil, errors.Wrap(err, "Failed to get absolute path")
	}

	file, err := os.Open(path)
	if err != nil {
		return nil, errors.Wrap(err, "Failed to open self")
	}
	defer file.Close()

	buf := new(bytes.Buffer)
	buf.ReadFrom(file)
	return buf.Bytes(), nil
}

func (d RemoteDeployer) Command(command string, stdout, stderr io.Writer) error {
	sess, err := d.ssh.NewSession()
	if err != nil {
		return errors.Wrap(err, "Failed to create new session")
	}
	defer sess.Close()

	sess.Stdout = stdout
	sess.Stderr = stderr

	if err := sess.Run(command); err != nil {
		if ee, ok := err.(*ssh.ExitError); ok {
			return fmt.Errorf("'%s' exit status is not 0: code=%d", command, ee.ExitStatus())
		}

		return errors.Wrapf(err, "Failed to command '%s'", command)
	}

	return nil
}
<commit_msg>check os.Args[0] is absolute path, because filepath.Abs() returns $PWD/$binary_path when relative path is setted<commit_after>package deploy

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path/filepath"

	"github.com/pkg/errors"
	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
)

type RemoteDeployer struct {
	ssh             *ssh.Client
	targetDirectory string
}

func NewRemoteDeployer(s *ssh.Client, target string) (*RemoteDeployer, error) {
	d := &RemoteDeployer{
		ssh:             s,
		targetDirectory: target,
	}

	client, err := sftp.NewClient(d.ssh)
	if err != nil {
		return nil, errors.Wrap(err, "Failed to create new sftp client")
	}
	defer client.Close()

	if err := client.MkdirAll(d.targetDirectory); err != nil {
		return nil, errors.Wrap(err, "Failed to make target directory")
	}

	return d, nil
}

// targetDirectory にファイルを転送し、 path にシンボリックリンクを貼る
func (d RemoteDeployer) SendFile(body []byte, path string, permission os.FileMode) error {
	client, err := sftp.NewClient(d.ssh)
	if err != nil {
		return errors.Wrap(err, "Failed to create new sftp client")
	}
	defer client.Close()

	var target string
	if !filepath.IsAbs(path) {
		target = filepath.Join(d.targetDirectory, path)
	} else {
		filename := filepath.Base(path)
		target = filepath.Join(d.targetDirectory, filename)

		if _, err := client.Lstat(path); err == nil {
			if err := client.Remove(path); err != nil {
				return errors.Wrap(err, "Failed to remove old symbolic link")
			}
		}

		if err := client.Symlink(target, path); err != nil {
			return errors.Wrap(err, "Failed to create symbolic link")
		}
	}

	if _, err := client.Stat(target); err == nil {
		if err := client.Remove(target); err != nil {
			return errors.Wrap(err, "Failed to delete target")
		}
	}

	file, err := client.Create(target)
	if err != nil {
		return errors.Wrap(err, "Failed to create new file")
	}
	defer file.Close()

	if err := file.Chmod(permission); err != nil {
		return errors.Wrap(err, "Failed to change permission")
	}

	if _, err := file.Write(body); err != nil {
		return errors.Wrap(err, "Failed to write to file")
	}

	return nil
}

func (d RemoteDeployer) ReadSelf() ([]byte, error) {
	if !filepath.IsAbs(os.Args[0]) {
		return nil, fmt.Errorf("Use binary with absolute path to read self")
	}

	path, err := filepath.Abs(os.Args[0])
	if err != nil {
		return nil, errors.Wrap(err, "Failed to get absolute path")
	}

	file, err := os.Open(path)
	if err != nil {
		return nil, errors.Wrap(err, "Failed to open self")
	}
	defer file.Close()

	buf := new(bytes.Buffer)
	buf.ReadFrom(file)
	return buf.Bytes(), nil
}

func (d RemoteDeployer) Command(command string, stdout, stderr io.Writer) error {
	sess, err := d.ssh.NewSession()
	if err != nil {
		return errors.Wrap(err, "Failed to create new session")
	}
	defer sess.Close()

	sess.Stdout = stdout
	sess.Stderr = stderr

	if err := sess.Run(command); err != nil {
		if ee, ok := err.(*ssh.ExitError); ok {
			return fmt.Errorf("'%s' exit status is not 0: code=%d", command, ee.ExitStatus())
		}

		return errors.Wrapf(err, "Failed to command '%s'", command)
	}

	return nil
}
<|endoftext|>