<commit_before>package calcium

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/pkg/errors"
	"github.com/projecteru2/core/cluster"
	enginetypes "github.com/projecteru2/core/engine/types"
	"github.com/projecteru2/core/metrics"
	resourcetypes "github.com/projecteru2/core/resources/types"

	"github.com/projecteru2/core/log"
	"github.com/projecteru2/core/types"
	"github.com/projecteru2/core/utils"
	"github.com/sanity-io/litter"
)

// CreateWorkload use options to create workloads
func (c *Calcium) CreateWorkload(ctx context.Context, opts *types.DeployOptions) (chan *types.CreateWorkloadMessage, error) {
	if err := opts.Validate(); err != nil {
		return nil, err
	}

	opts.ProcessIdent = utils.RandomString(16)
	log.Infof("[CreateWorkload %s] Creating workload with options:", opts.ProcessIdent)
	litter.Dump(opts)
	// Count 要大于0
	if opts.Count <= 0 {
		return nil, errors.WithStack(types.NewDetailedErr(types.ErrBadCount, opts.Count))
	}

	ch, err := c.doCreateWorkloads(ctx, opts)
	return ch, errors.WithStack(err)
}

// transaction: resource metadata consistency
func (c *Calcium) doCreateWorkloads(ctx context.Context, opts *types.DeployOptions) (chan *types.CreateWorkloadMessage, error) {
	ch := make(chan *types.CreateWorkloadMessage)
	// RFC 计算当前 app 部署情况的时候需要保证同一时间只有这个 app 的这个 entrypoint 在跑
	// 因此需要在这里加个全局锁，直到部署完毕才释放
	// 通过 Processing 状态跟踪达成 18 Oct, 2018

	var (
		err         error
		plans       []resourcetypes.ResourcePlans
		deployMap   map[string]int
		rollbackMap map[string][]int
	)

	go func() {
		defer func() {
			for nodename := range deployMap {
				if e := c.store.DeleteProcessing(ctx, opts, nodename); e != nil {
					err = e
					log.Errorf("[Calcium.doCreateWorkloads] delete processing failed for %s: %+v", nodename, err)
				}
			}
			close(ch)
		}()

		if err := utils.Txn(
			ctx,

			// if: alloc resources
			func(ctx context.Context) error {
				return c.withNodesLocked(ctx, opts.Podname, opts.Nodenames, opts.NodeLabels, false, func(ctx context.Context, nodeMap map[string]*types.Node) (err error) {
					defer func() {
						if err != nil {
							ch <- &types.CreateWorkloadMessage{Error: err}
						}
					}()

					// calculate plans
					if plans, deployMap, err = c.doAllocResource(ctx, nodeMap, opts); err != nil {
						return errors.WithStack(err)
					}

					// commit changes
					nodes := []*types.Node{}
					for nodename, deploy := range deployMap {
						for _, plan := range plans {
							plan.ApplyChangesOnNode(nodeMap[nodename], utils.Range(deploy)...)
						}
						nodes = append(nodes, nodeMap[nodename])
						if err = c.store.SaveProcessing(ctx, opts, nodename, deploy); err != nil {
							return errors.WithStack(err)
						}
					}
					return errors.WithStack(c.store.UpdateNodes(ctx, nodes...))
				})
			},

			// then: deploy workloads
			func(ctx context.Context) error {
				rollbackMap, err = c.doDeployWorkloads(ctx, ch, opts, plans, deployMap)
				return errors.WithStack(err)
			},

			// rollback: give back resources
			func(ctx context.Context, _ bool) (err error) {
				for nodename, rollbackIndices := range rollbackMap {
					if e := c.withNodeLocked(ctx, nodename, func(ctx context.Context, node *types.Node) error {
						for _, plan := range plans {
							plan.RollbackChangesOnNode(node, rollbackIndices...) // nolint:scopelint
						}
						return errors.WithStack(c.store.UpdateNodes(ctx, node))
					}); e != nil {
						err = e
					}
				}
				return errors.WithStack(err)
			},

			c.config.GlobalTimeout,
		); err != nil {
			log.Errorf("[Calcium.doCreateWorkloads] %+v", err)
		}
	}()

	return ch, errors.WithStack(err)
}

func (c *Calcium) doDeployWorkloads(ctx context.Context, ch chan *types.CreateWorkloadMessage, opts *types.DeployOptions, plans []resourcetypes.ResourcePlans, deployMap map[string]int) (_ map[string][]int, err error) {
	wg := sync.WaitGroup{}
	wg.Add(len(deployMap))

	seq := 0
	rollbackMap := make(map[string][]int)
	for nodename, deploy := range deployMap {
		go metrics.Client.SendDeployCount(deploy)
		go func(nodename string, deploy, seq int) {
			defer wg.Done()
			if indices, e := c.doDeployWorkloadsOnNode(ctx, ch, nodename, opts, deploy, plans, seq); e != nil {
				err = e
				rollbackMap[nodename] = indices
			}
		}(nodename, deploy, seq)
		seq += deploy
	}

	wg.Wait()
	return rollbackMap, errors.WithStack(err)
}

// deploy scheduled workloads on one node
func (c *Calcium) doDeployWorkloadsOnNode(ctx context.Context, ch chan *types.CreateWorkloadMessage, nodename string, opts *types.DeployOptions, deploy int, plans []resourcetypes.ResourcePlans, seq int) (indices []int, err error) {
	node, err := c.doGetAndPrepareNode(ctx, nodename, opts.Image)
	if err != nil {
		for i := 0; i < deploy; i++ {
			ch <- &types.CreateWorkloadMessage{Error: err}
		}
		return utils.Range(deploy), errors.WithStack(err)
	}

	for idx := 0; idx < deploy; idx++ {
		createMsg := &types.CreateWorkloadMessage{
			Podname:  opts.Podname,
			Nodename: nodename,
			Publish:  map[string][]string{},
		}

		do := func(idx int) (e error) {
			defer func() {
				if e != nil {
					err = e
					createMsg.Error = e
					indices = append(indices, idx)
				}
				ch <- createMsg
			}()

			r := &types.ResourceMeta{}
			o := resourcetypes.DispenseOptions{
				Node:  node,
				Index: idx,
			}
			for _, plan := range plans {
				if r, e = plan.Dispense(o, r); e != nil {
					return
				}
			}

			createMsg.ResourceMeta = *r
			createOpts := c.doMakeWorkloadOptions(seq+idx, createMsg, opts, node)
			return c.doDeployOneWorkload(ctx, node, opts, createMsg, createOpts, deploy-1-idx)
		}
		_ = do(idx)
	}

	return indices, errors.WithStack(err)
}

func (c *Calcium) doGetAndPrepareNode(ctx context.Context, nodename, image string) (*types.Node, error) {
	node, err := c.GetNode(ctx, nodename)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	return node, errors.WithStack(pullImage(ctx, node, image))
}

// transaction: workload metadata consistency
func (c *Calcium) doDeployOneWorkload(
	ctx context.Context,
	node *types.Node,
	opts *types.DeployOptions,
	msg *types.CreateWorkloadMessage,
	config *enginetypes.VirtualizationCreateOptions,
	processingCount int,
) (err error) {
	workload := &types.Workload{
		ResourceMeta: types.ResourceMeta{
			CPU:               msg.CPU,
			CPUQuotaRequest:   msg.CPUQuotaRequest,
			CPUQuotaLimit:     msg.CPUQuotaLimit,
			MemoryRequest:     msg.MemoryRequest,
			MemoryLimit:       msg.MemoryLimit,
			StorageRequest:    msg.StorageRequest,
			StorageLimit:      msg.StorageLimit,
			VolumeRequest:     msg.VolumeRequest,
			VolumeLimit:       msg.VolumeLimit,
			VolumePlanRequest: msg.VolumePlanRequest,
			VolumePlanLimit:   msg.VolumePlanLimit,
		},
		Name:       config.Name,
		Labels:     config.Labels,
		Podname:    opts.Podname,
		Nodename:   node.Name,
		Hook:       opts.Entrypoint.Hook,
		Privileged: opts.Entrypoint.Privileged,
		Engine:     node.Engine,
		Image:      opts.Image,
		Env:        opts.Env,
		User:       opts.User,
		CreateTime: time.Now().Unix(),
	}
	return utils.Txn(
		ctx,
		// create workload
		func(ctx context.Context) error {
			created, err := node.Engine.VirtualizationCreate(ctx, config)
			if err != nil {
				return errors.WithStack(err)
			}
			workload.ID = created.ID
			return nil
		},

		func(ctx context.Context) (err error) {
			// Copy data to workload
			if len(opts.Data) > 0 {
				for dst, readerManager := range opts.Data {
					reader, err := readerManager.GetReader()
					if err != nil {
						return errors.WithStack(err)
					}
					if err = c.doSendFileToWorkload(ctx, node.Engine, workload.ID, dst, reader, true, false); err != nil {
						return errors.WithStack(err)
					}
				}
			}

			// deal with hook
			if len(opts.AfterCreate) > 0 && workload.Hook != nil {
				workload.Hook = &types.Hook{
					AfterStart: append(opts.AfterCreate, workload.Hook.AfterStart...),
					Force:      workload.Hook.Force,
				}
			}

			// start first
			msg.Hook, err = c.doStartWorkload(ctx, workload, opts.IgnoreHook)
			if err != nil {
				return errors.WithStack(err)
			}

			// inspect real meta
			var workloadInfo *enginetypes.VirtualizationInfo
			workloadInfo, err = workload.Inspect(ctx) // 补充静态元数据
			if err != nil {
				return errors.WithStack(err)
			}

			// update meta
			if workloadInfo.Networks != nil {
				msg.Publish = utils.MakePublishInfo(workloadInfo.Networks, opts.Entrypoint.Publish)
			}
			// reset users
			if workloadInfo.User != workload.User {
				workload.User = workloadInfo.User
			}
			// reset workload.hook
			workload.Hook = opts.Entrypoint.Hook

			// update processing
			if processingCount >= 0 {
				if err := c.store.UpdateProcessing(ctx, opts, node.Name, processingCount); err != nil {
					return errors.WithStack(err)
				}
			}

			if err := c.store.AddWorkload(ctx, workload); err != nil {
				return errors.WithStack(err)
			}
			msg.WorkloadID = workload.ID
			return nil
		},

		// remove workload
		func(ctx context.Context, _ bool) error {
			return errors.WithStack(c.doRemoveWorkload(ctx, workload, true))
		},
		c.config.GlobalTimeout,
	)
}

func (c *Calcium) doMakeWorkloadOptions(no int, msg *types.CreateWorkloadMessage, opts *types.DeployOptions, node *types.Node) *enginetypes.VirtualizationCreateOptions {
	config := &enginetypes.VirtualizationCreateOptions{}
	// general
	config.CPU = msg.CPU
	config.Quota = msg.CPUQuotaLimit
	config.Memory = msg.MemoryLimit
	config.Storage = msg.StorageLimit
	config.NUMANode = msg.NUMANode
	config.RawArgs = opts.RawArgs
	config.Lambda = opts.Lambda
	config.User = opts.User
	config.DNS = opts.DNS
	config.Image = opts.Image
	config.Stdin = opts.OpenStdin
	config.Hosts = opts.ExtraHosts
	config.Volumes = msg.VolumeLimit.ApplyPlan(msg.VolumePlanLimit).ToStringSlice(false, true)
	config.VolumePlan = msg.VolumePlanLimit.ToLiteral()
	config.Debug = opts.Debug
	config.Networks = opts.Networks

	// entry
	entry := opts.Entrypoint
	config.WorkingDir = entry.Dir
	config.Privileged = entry.Privileged
	config.RestartPolicy = entry.RestartPolicy
	config.Sysctl = entry.Sysctls
	config.Publish = entry.Publish
	if entry.Log != nil {
		config.LogType = entry.Log.Type
		config.LogConfig = entry.Log.Config
	}
	// name
	suffix := utils.RandomString(6)
	config.Name = utils.MakeWorkloadName(opts.Name, opts.Entrypoint.Name, suffix)
	msg.WorkloadName = config.Name
	// command and user
	// extra args is dynamically
	slices := utils.MakeCommandLineArgs(fmt.Sprintf("%s %s", entry.Command, opts.ExtraArgs))
	config.Cmd = slices
	// env
	env := append(opts.Env, fmt.Sprintf("APP_NAME=%s", opts.Name))
	env = append(env, fmt.Sprintf("ERU_POD=%s", opts.Podname))
	env = append(env, fmt.Sprintf("ERU_NODE_NAME=%s", node.Name))
	env = append(env, fmt.Sprintf("ERU_WORKLOAD_SEQ=%d", no))
	env = append(env, fmt.Sprintf("ERU_MEMORY=%d", msg.MemoryLimit))
	env = append(env, fmt.Sprintf("ERU_STORAGE=%d", msg.StorageLimit))
	config.Env = env
	// basic labels, bind to LabelMeta
	config.Labels = map[string]string{
		cluster.ERUMark: "1",
		cluster.LabelMeta: utils.EncodeMetaInLabel(&types.LabelMeta{
			Publish:     opts.Entrypoint.Publish,
			HealthCheck: entry.HealthCheck,
		}),
	}
	for key, value := range opts.Labels {
		config.Labels[key] = value
	}

	return config
}
<commit_msg>fill missing attributes (#329)<commit_after>package calcium

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/pkg/errors"
	"github.com/projecteru2/core/cluster"
	enginetypes "github.com/projecteru2/core/engine/types"
	"github.com/projecteru2/core/metrics"
	resourcetypes "github.com/projecteru2/core/resources/types"

	"github.com/projecteru2/core/log"
	"github.com/projecteru2/core/types"
	"github.com/projecteru2/core/utils"
	"github.com/sanity-io/litter"
)

// CreateWorkload use options to create workloads
func (c *Calcium) CreateWorkload(ctx context.Context, opts *types.DeployOptions) (chan *types.CreateWorkloadMessage, error) {
	if err := opts.Validate(); err != nil {
		return nil, err
	}

	opts.ProcessIdent = utils.RandomString(16)
	log.Infof("[CreateWorkload %s] Creating workload with options:", opts.ProcessIdent)
	litter.Dump(opts)
	// Count 要大于0
	if opts.Count <= 0 {
		return nil, errors.WithStack(types.NewDetailedErr(types.ErrBadCount, opts.Count))
	}

	ch, err := c.doCreateWorkloads(ctx, opts)
	return ch, errors.WithStack(err)
}

// transaction: resource metadata consistency
func (c *Calcium) doCreateWorkloads(ctx context.Context, opts *types.DeployOptions) (chan *types.CreateWorkloadMessage, error) {
	ch := make(chan *types.CreateWorkloadMessage)
	// RFC 计算当前 app 部署情况的时候需要保证同一时间只有这个 app 的这个 entrypoint 在跑
	// 因此需要在这里加个全局锁，直到部署完毕才释放
	// 通过 Processing 状态跟踪达成 18 Oct, 2018

	var (
		err         error
		plans       []resourcetypes.ResourcePlans
		deployMap   map[string]int
		rollbackMap map[string][]int
	)

	go func() {
		defer func() {
			for nodename := range deployMap {
				if e := c.store.DeleteProcessing(ctx, opts, nodename); e != nil {
					err = e
					log.Errorf("[Calcium.doCreateWorkloads] delete processing failed for %s: %+v", nodename, err)
				}
			}
			close(ch)
		}()

		if err := utils.Txn(
			ctx,

			// if: alloc resources
			func(ctx context.Context) error {
				return c.withNodesLocked(ctx, opts.Podname, opts.Nodenames, opts.NodeLabels, false, func(ctx context.Context, nodeMap map[string]*types.Node) (err error) {
					defer func() {
						if err != nil {
							ch <- &types.CreateWorkloadMessage{Error: err}
						}
					}()

					// calculate plans
					if plans, deployMap, err = c.doAllocResource(ctx, nodeMap, opts); err != nil {
						return errors.WithStack(err)
					}

					// commit changes
					nodes := []*types.Node{}
					for nodename, deploy := range deployMap {
						for _, plan := range plans {
							plan.ApplyChangesOnNode(nodeMap[nodename], utils.Range(deploy)...)
						}
						nodes = append(nodes, nodeMap[nodename])
						if err = c.store.SaveProcessing(ctx, opts, nodename, deploy); err != nil {
							return errors.WithStack(err)
						}
					}
					return errors.WithStack(c.store.UpdateNodes(ctx, nodes...))
				})
			},

			// then: deploy workloads
			func(ctx context.Context) error {
				rollbackMap, err = c.doDeployWorkloads(ctx, ch, opts, plans, deployMap)
				return errors.WithStack(err)
			},

			// rollback: give back resources
			func(ctx context.Context, _ bool) (err error) {
				for nodename, rollbackIndices := range rollbackMap {
					if e := c.withNodeLocked(ctx, nodename, func(ctx context.Context, node *types.Node) error {
						for _, plan := range plans {
							plan.RollbackChangesOnNode(node, rollbackIndices...) // nolint:scopelint
						}
						return errors.WithStack(c.store.UpdateNodes(ctx, node))
					}); e != nil {
						err = e
					}
				}
				return errors.WithStack(err)
			},

			c.config.GlobalTimeout,
		); err != nil {
			log.Errorf("[Calcium.doCreateWorkloads] %+v", err)
		}
	}()

	return ch, errors.WithStack(err)
}

func (c *Calcium) doDeployWorkloads(ctx context.Context, ch chan *types.CreateWorkloadMessage, opts *types.DeployOptions, plans []resourcetypes.ResourcePlans, deployMap map[string]int) (_ map[string][]int, err error) {
	wg := sync.WaitGroup{}
	wg.Add(len(deployMap))

	seq := 0
	rollbackMap := make(map[string][]int)
	for nodename, deploy := range deployMap {
		go metrics.Client.SendDeployCount(deploy)
		go func(nodename string, deploy, seq int) {
			defer wg.Done()
			if indices, e := c.doDeployWorkloadsOnNode(ctx, ch, nodename, opts, deploy, plans, seq); e != nil {
				err = e
				rollbackMap[nodename] = indices
			}
		}(nodename, deploy, seq)
		seq += deploy
	}

	wg.Wait()
	return rollbackMap, errors.WithStack(err)
}

// deploy scheduled workloads on one node
func (c *Calcium) doDeployWorkloadsOnNode(ctx context.Context, ch chan *types.CreateWorkloadMessage, nodename string, opts *types.DeployOptions, deploy int, plans []resourcetypes.ResourcePlans, seq int) (indices []int, err error) {
	node, err := c.doGetAndPrepareNode(ctx, nodename, opts.Image)
	if err != nil {
		for i := 0; i < deploy; i++ {
			ch <- &types.CreateWorkloadMessage{Error: err}
		}
		return utils.Range(deploy), errors.WithStack(err)
	}

	for idx := 0; idx < deploy; idx++ {
		createMsg := &types.CreateWorkloadMessage{
			Podname:  opts.Podname,
			Nodename: nodename,
			Publish:  map[string][]string{},
		}

		do := func(idx int) (e error) {
			defer func() {
				if e != nil {
					err = e
					createMsg.Error = e
					indices = append(indices, idx)
				}
				ch <- createMsg
			}()

			r := &types.ResourceMeta{}
			o := resourcetypes.DispenseOptions{
				Node:  node,
				Index: idx,
			}
			for _, plan := range plans {
				if r, e = plan.Dispense(o, r); e != nil {
					return
				}
			}

			createMsg.ResourceMeta = *r
			createOpts := c.doMakeWorkloadOptions(seq+idx, createMsg, opts, node)
			return c.doDeployOneWorkload(ctx, node, opts, createMsg, createOpts, deploy-1-idx)
		}
		_ = do(idx)
	}

	return indices, errors.WithStack(err)
}

func (c *Calcium) doGetAndPrepareNode(ctx context.Context, nodename, image string) (*types.Node, error) {
	node, err := c.GetNode(ctx, nodename)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	return node, errors.WithStack(pullImage(ctx, node, image))
}

// transaction: workload metadata consistency
func (c *Calcium) doDeployOneWorkload(
	ctx context.Context,
	node *types.Node,
	opts *types.DeployOptions,
	msg *types.CreateWorkloadMessage,
	config *enginetypes.VirtualizationCreateOptions,
	processingCount int,
) (err error) {
	workload := &types.Workload{
		ResourceMeta: types.ResourceMeta{
			CPU:               msg.CPU,
			CPUQuotaRequest:   msg.CPUQuotaRequest,
			CPUQuotaLimit:     msg.CPUQuotaLimit,
			MemoryRequest:     msg.MemoryRequest,
			MemoryLimit:       msg.MemoryLimit,
			StorageRequest:    msg.StorageRequest,
			StorageLimit:      msg.StorageLimit,
			VolumeRequest:     msg.VolumeRequest,
			VolumeLimit:       msg.VolumeLimit,
			VolumePlanRequest: msg.VolumePlanRequest,
			VolumePlanLimit:   msg.VolumePlanLimit,
		},
		Name:       config.Name,
		Labels:     config.Labels,
		Podname:    opts.Podname,
		Nodename:   node.Name,
		Hook:       opts.Entrypoint.Hook,
		Privileged: opts.Entrypoint.Privileged,
		Engine:     node.Engine,
		Image:      opts.Image,
		Env:        opts.Env,
		User:       opts.User,
		CreateTime: time.Now().Unix(),
	}
	return utils.Txn(
		ctx,
		// create workload
		func(ctx context.Context) error {
			created, err := node.Engine.VirtualizationCreate(ctx, config)
			if err != nil {
				return errors.WithStack(err)
			}
			workload.ID = created.ID
			return nil
		},

		func(ctx context.Context) (err error) {
			// Copy data to workload
			if len(opts.Data) > 0 {
				for dst, readerManager := range opts.Data {
					reader, err := readerManager.GetReader()
					if err != nil {
						return errors.WithStack(err)
					}
					if err = c.doSendFileToWorkload(ctx, node.Engine, workload.ID, dst, reader, true, false); err != nil {
						return errors.WithStack(err)
					}
				}
			}

			// deal with hook
			if len(opts.AfterCreate) > 0 && workload.Hook != nil {
				workload.Hook = &types.Hook{
					AfterStart: append(opts.AfterCreate, workload.Hook.AfterStart...),
					Force:      workload.Hook.Force,
				}
			}

			// start first
			msg.Hook, err = c.doStartWorkload(ctx, workload, opts.IgnoreHook)
			if err != nil {
				return errors.WithStack(err)
			}

			// inspect real meta
			var workloadInfo *enginetypes.VirtualizationInfo
			workloadInfo, err = workload.Inspect(ctx) // 补充静态元数据
			if err != nil {
				return errors.WithStack(err)
			}

			// update meta
			if workloadInfo.Networks != nil {
				msg.Publish = utils.MakePublishInfo(workloadInfo.Networks, opts.Entrypoint.Publish)
			}
			// reset users
			if workloadInfo.User != workload.User {
				workload.User = workloadInfo.User
			}
			// reset workload.hook
			workload.Hook = opts.Entrypoint.Hook

			// update processing
			if processingCount >= 0 {
				if err := c.store.UpdateProcessing(ctx, opts, node.Name, processingCount); err != nil {
					return errors.WithStack(err)
				}
			}

			if err := c.store.AddWorkload(ctx, workload); err != nil {
				return errors.WithStack(err)
			}
			msg.WorkloadID = workload.ID
			msg.WorkloadName = workload.Name
			msg.Podname = workload.Podname
			msg.Nodename = workload.Nodename
			return nil
		},

		// remove workload
		func(ctx context.Context, _ bool) error {
			return errors.WithStack(c.doRemoveWorkload(ctx, workload, true))
		},
		c.config.GlobalTimeout,
	)
}

func (c *Calcium) doMakeWorkloadOptions(no int, msg *types.CreateWorkloadMessage, opts *types.DeployOptions, node *types.Node) *enginetypes.VirtualizationCreateOptions {
	config := &enginetypes.VirtualizationCreateOptions{}
	// general
	config.CPU = msg.CPU
	config.Quota = msg.CPUQuotaLimit
	config.Memory = msg.MemoryLimit
	config.Storage = msg.StorageLimit
	config.NUMANode = msg.NUMANode
	config.RawArgs = opts.RawArgs
	config.Lambda = opts.Lambda
	config.User = opts.User
	config.DNS = opts.DNS
	config.Image = opts.Image
	config.Stdin = opts.OpenStdin
	config.Hosts = opts.ExtraHosts
	config.Volumes = msg.VolumeLimit.ApplyPlan(msg.VolumePlanLimit).ToStringSlice(false, true)
	config.VolumePlan = msg.VolumePlanLimit.ToLiteral()
	config.Debug = opts.Debug
	config.Networks = opts.Networks

	// entry
	entry := opts.Entrypoint
	config.WorkingDir = entry.Dir
	config.Privileged = entry.Privileged
	config.RestartPolicy = entry.RestartPolicy
	config.Sysctl = entry.Sysctls
	config.Publish = entry.Publish
	if entry.Log != nil {
		config.LogType = entry.Log.Type
		config.LogConfig = entry.Log.Config
	}
	// name
	suffix := utils.RandomString(6)
	config.Name = utils.MakeWorkloadName(opts.Name, opts.Entrypoint.Name, suffix)
	msg.WorkloadName = config.Name
	// command and user
	// extra args is dynamically
	slices := utils.MakeCommandLineArgs(fmt.Sprintf("%s %s", entry.Command, opts.ExtraArgs))
	config.Cmd = slices
	// env
	env := append(opts.Env, fmt.Sprintf("APP_NAME=%s", opts.Name))
	env = append(env, fmt.Sprintf("ERU_POD=%s", opts.Podname))
	env = append(env, fmt.Sprintf("ERU_NODE_NAME=%s", node.Name))
	env = append(env, fmt.Sprintf("ERU_WORKLOAD_SEQ=%d", no))
	env = append(env, fmt.Sprintf("ERU_MEMORY=%d", msg.MemoryLimit))
	env = append(env, fmt.Sprintf("ERU_STORAGE=%d", msg.StorageLimit))
	config.Env = env
	// basic labels, bind to LabelMeta
	config.Labels = map[string]string{
		cluster.ERUMark: "1",
		cluster.LabelMeta: utils.EncodeMetaInLabel(&types.LabelMeta{
			Publish:     opts.Entrypoint.Publish,
			HealthCheck: entry.HealthCheck,
		}),
	}
	for key, value := range opts.Labels {
		config.Labels[key] = value
	}

	return config
}
<|endoftext|>


<commit_before>// +build js

package main

import (
	"github.com/shurcooL/go/github_flavored_markdown/sanitized_anchor_name"
	"honnef.co/go/js/dom"
)

var document = dom.GetWindow().Document().(dom.HTMLDocument)

var headers []dom.Element
var results *dom.HTMLDivElement

func main() {
	headers = document.Body().GetElementsByTagName("h2")
	if len(headers) == 0 {
		return
	}

	overlay := document.CreateElement("div")
	overlay.SetID("toc-overlay")

	results = document.CreateElement("div").(*dom.HTMLDivElement)

	for _, header := range headers {
		element := document.CreateElement("a").(*dom.HTMLAnchorElement)
		element.Class().Add("toc-entry")
		element.SetTextContent(header.TextContent())
		element.Href = "#" + sanitized_anchor_name.Create(header.TextContent())

		results.AppendChild(element)
	}

	overlay.AppendChild(results)
	document.Body().AppendChild(overlay)

	dom.GetWindow().AddEventListener("scroll", false, func(event dom.Event) {
		updateToc()
	})

	updateToc()
}

func updateToc() {
	// Clear all past highlighted.
	for _, node := range results.ChildNodes() {
		node.(dom.Element).Class().Remove("toc-highlighted")
	}

	// Highlight one entry.
	for i := len(headers) - 1; i >= 0; i-- {
		header := headers[i]
		if header.GetBoundingClientRect().Top <= 0 || i == 0 {
			results.ChildNodes()[i].(dom.Element).Class().Add("toc-highlighted")
			break
		}
	}
}
<commit_msg>Use window half height for table of content current section.<commit_after>// +build js

package main

import (
	"github.com/shurcooL/go/github_flavored_markdown/sanitized_anchor_name"
	"honnef.co/go/js/dom"
)

var document = dom.GetWindow().Document().(dom.HTMLDocument)

var headers []dom.Element
var results *dom.HTMLDivElement

func main() {
	headers = document.Body().GetElementsByTagName("h2")
	if len(headers) == 0 {
		return
	}

	overlay := document.CreateElement("div")
	overlay.SetID("toc-overlay")

	results = document.CreateElement("div").(*dom.HTMLDivElement)

	for _, header := range headers {
		element := document.CreateElement("a").(*dom.HTMLAnchorElement)
		element.Class().Add("toc-entry")
		element.SetTextContent(header.TextContent())
		element.Href = "#" + sanitized_anchor_name.Create(header.TextContent())

		results.AppendChild(element)
	}

	overlay.AppendChild(results)
	document.Body().AppendChild(overlay)

	dom.GetWindow().AddEventListener("scroll", false, func(event dom.Event) {
		updateToc()
	})

	updateToc()
}

func updateToc() {
	// Clear all past highlighted.
	for _, node := range results.ChildNodes() {
		node.(dom.Element).Class().Remove("toc-highlighted")
	}

	// Highlight one entry.
	windowHalfHeight := dom.GetWindow().InnerHeight() / 2
	for i := len(headers) - 1; i >= 0; i-- {
		header := headers[i]
		if header.GetBoundingClientRect().Top <= windowHalfHeight || i == 0 {
			results.ChildNodes()[i].(dom.Element).Class().Add("toc-highlighted")
			break
		}
	}
}
<|endoftext|>


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

package cmd

import (
	"fmt"
	"strings"

	"github.com/fatih/color"
	"github.com/minio/cli"
	json "github.com/minio/colorjson"
	"github.com/minio/mc/pkg/probe"
	"github.com/minio/pkg/console"
)

var adminConfigEnvFlags = []cli.Flag{
	cli.BoolFlag{
		Name:  "env",
		Usage: "list all the env only help",
	},
}

var adminConfigResetCmd = cli.Command{
	Name:         "reset",
	Usage:        "interactively reset a config key parameters",
	Before:       setGlobalsFromContext,
	Action:       mainAdminConfigReset,
	OnUsageError: onUsageError,
	Flags:        append(adminConfigEnvFlags, globalFlags...),
	CustomHelpTemplate: `NAME:
  {{.HelpName}} - {{.Usage}}

USAGE:
  {{.HelpName}} TARGET

FLAGS:
  {{range .VisibleFlags}}{{.}}
  {{end}}
EXAMPLES:
  1. Reset MQTT notifcation target 'name1' settings to default values.
     {{.Prompt}} {{.HelpName}} myminio/ notify_mqtt:name1
`,
}

// configResetMessage container to hold locks information.
type configResetMessage struct {
	Status      string `json:"status"`
	targetAlias string
	restart     bool
}

// String colorized service status message.
func (u configResetMessage) String() (msg string) {
	msg += console.Colorize("ResetConfigSuccess",
		"Key is successfully reset.\n")
	suggestion := fmt.Sprintf("mc admin service restart %s", u.targetAlias)
	if u.restart {
		msg += console.Colorize("ResetConfigSuccess",
			fmt.Sprintf("Please restart your server with `%s`.\n", suggestion))
	}
	return
}

// JSON jsonified service status message.
func (u configResetMessage) JSON() string {
	u.Status = "success"
	statusJSONBytes, e := json.MarshalIndent(u, "", " ")
	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")

	return string(statusJSONBytes)
}

// checkAdminConfigResetSyntax - validate all the passed arguments
func checkAdminConfigResetSyntax(ctx *cli.Context) {
	if !ctx.Args().Present() {
		cli.ShowCommandHelpAndExit(ctx, "reset", 1) // last argument is exit code
	}
}

// main config set function
func mainAdminConfigReset(ctx *cli.Context) error {
	// Check command arguments
	checkAdminConfigResetSyntax(ctx)

	// Reset color preference of command outputs
	console.SetColor("ResetConfigSuccess", color.New(color.FgGreen, color.Bold))
	console.SetColor("ResetConfigFailure", color.New(color.FgRed, color.Bold))

	// Get the alias parameter from cli
	args := ctx.Args()
	aliasedURL := args.Get(0)

	// Create a new MinIO Admin Client
	client, err := newAdminClient(aliasedURL)
	fatalIf(err, "Unable to initialize admin connection.")

	if len(ctx.Args()) == 1 {
		// Call get config API
		hr, e := client.HelpConfigKV(globalContext, "", "", ctx.IsSet("env"))
		fatalIf(probe.NewError(e), "Unable to get help for the sub-system")

		// Print
		printMsg(configHelpMessage{
			Value:   hr,
			envOnly: ctx.IsSet("env"),
		})

		return nil
	}

	// Call reset config API
	input := strings.Join(args.Tail(), " ")
	restart, e := client.DelConfigKV(globalContext, input)
	fatalIf(probe.NewError(e), "Unable to reset '%s' on the server", input)

	// Print set config result
	printMsg(configResetMessage{
		targetAlias: aliasedURL,
		restart:     restart,
	})

	return nil
}
<commit_msg>Update examples for admin-config-reset (#4160)<commit_after>// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package cmd

import (
	"fmt"
	"strings"

	"github.com/fatih/color"
	"github.com/minio/cli"
	json "github.com/minio/colorjson"
	"github.com/minio/mc/pkg/probe"
	"github.com/minio/pkg/console"
)

var adminConfigEnvFlags = []cli.Flag{
	cli.BoolFlag{
		Name:  "env",
		Usage: "list all the env only help",
	},
}

var adminConfigResetCmd = cli.Command{
	Name:         "reset",
	Usage:        "interactively reset a config key parameters",
	Before:       setGlobalsFromContext,
	Action:       mainAdminConfigReset,
	OnUsageError: onUsageError,
	Flags:        append(adminConfigEnvFlags, globalFlags...),
	CustomHelpTemplate: `NAME:
  {{.HelpName}} - {{.Usage}}

USAGE:
  {{.HelpName}} TARGET [CONFIG-KEY...]

FLAGS:
  {{range .VisibleFlags}}{{.}}
  {{end}}
EXAMPLES:
  1. Reset MQTT notifcation target 'name1' settings to default values.
     {{.Prompt}} {{.HelpName}} myminio/ notify_mqtt:name1
  2. Reset compression's 'extensions' setting to default value.
     {{.Prompt}} {{.HelpName}} myminio/ compression extensions
  3. Reset site name and site region to default values.
     {{.Prompt}} {{.HelpName}} myminio/ site name region
`,
}

// configResetMessage container to hold locks information.
type configResetMessage struct {
	Status      string `json:"status"`
	targetAlias string
	restart     bool
}

// String colorized service status message.
func (u configResetMessage) String() (msg string) {
	msg += console.Colorize("ResetConfigSuccess",
		"Key is successfully reset.\n")
	suggestion := fmt.Sprintf("mc admin service restart %s", u.targetAlias)
	if u.restart {
		msg += console.Colorize("ResetConfigSuccess",
			fmt.Sprintf("Please restart your server with `%s`.\n", suggestion))
	}
	return
}

// JSON jsonified service status message.
func (u configResetMessage) JSON() string {
	u.Status = "success"
	statusJSONBytes, e := json.MarshalIndent(u, "", " ")
	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")

	return string(statusJSONBytes)
}

// checkAdminConfigResetSyntax - validate all the passed arguments
func checkAdminConfigResetSyntax(ctx *cli.Context) {
	if !ctx.Args().Present() {
		cli.ShowCommandHelpAndExit(ctx, "reset", 1) // last argument is exit code
	}
}

// main config set function
func mainAdminConfigReset(ctx *cli.Context) error {
	// Check command arguments
	checkAdminConfigResetSyntax(ctx)

	// Reset color preference of command outputs
	console.SetColor("ResetConfigSuccess", color.New(color.FgGreen, color.Bold))
	console.SetColor("ResetConfigFailure", color.New(color.FgRed, color.Bold))

	// Get the alias parameter from cli
	args := ctx.Args()
	aliasedURL := args.Get(0)

	// Create a new MinIO Admin Client
	client, err := newAdminClient(aliasedURL)
	fatalIf(err, "Unable to initialize admin connection.")

	if len(ctx.Args()) == 1 {
		// Call get config API
		hr, e := client.HelpConfigKV(globalContext, "", "", ctx.IsSet("env"))
		fatalIf(probe.NewError(e), "Unable to get help for the sub-system")

		// Print
		printMsg(configHelpMessage{
			Value:   hr,
			envOnly: ctx.IsSet("env"),
		})

		return nil
	}

	input := strings.Join(args.Tail(), " ")
	// Check if user has attempted to set values
	for _, k := range args.Tail() {
		if strings.Contains(k, "=") {
			e := fmt.Errorf("new settings may not be provided for sub-system keys")
			fatalIf(probe.NewError(e), "Unable to reset '%s' on the server", args.Tail()[0])
		}
	}

	// Call reset config API
	restart, e := client.DelConfigKV(globalContext, input)
	fatalIf(probe.NewError(e), "Unable to reset '%s' on the server", input)

	// Print set config result
	printMsg(configResetMessage{
		targetAlias: aliasedURL,
		restart:     restart,
	})

	return nil
}
<|endoftext|>


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

package bytes

// Simple byte buffer for marshaling data.

import (
	"errors"
	"io"
	"unicode/utf8"
)

// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
	buf       []byte            // contents are the bytes buf[off : len(buf)]
	off       int               // read at &buf[off], write at &buf[len(buf)]
	runeBytes [utf8.UTFMax]byte // avoid allocation of slice on each WriteByte or Rune
	bootstrap [64]byte          // memory to hold first slice; helps small buffers (Printf) avoid allocation.
	lastRead  readOp            // last read operation, so that Unread* can work correctly.
}

// The readOp constants describe the last action performed on
// the buffer, so that UnreadRune and UnreadByte can
// check for invalid usage.
type readOp int

const (
	opInvalid  readOp = iota // Non-read operation.
	opReadRune               // Read rune.
	opRead                   // Any other read operation.
)

// ErrTooLarge is passed to panic if memory cannot be allocated to store data in a buffer.
var ErrTooLarge = errors.New("bytes.Buffer: too large")

// Bytes returns a slice of the contents of the unread portion of the buffer;
// len(b.Bytes()) == b.Len().  If the caller changes the contents of the
// returned slice, the contents of the buffer will change provided there
// are no intervening method calls on the Buffer.
func (b *Buffer) Bytes() []byte { return b.buf[b.off:] }

// String returns the contents of the unread portion of the buffer
// as a string.  If the Buffer is a nil pointer, it returns "<nil>".
func (b *Buffer) String() string {
	if b == nil {
		// Special case, useful in debugging.
		return "<nil>"
	}
	return string(b.buf[b.off:])
}

// Len returns the number of bytes of the unread portion of the buffer;
// b.Len() == len(b.Bytes()).
func (b *Buffer) Len() int { return len(b.buf) - b.off }

// Truncate discards all but the first n unread bytes from the buffer.
// It panics if n is negative or greater than the length of the buffer.
func (b *Buffer) Truncate(n int) {
	b.lastRead = opInvalid
	switch {
	case n < 0 || n > b.Len():
		panic("bytes.Buffer: truncation out of range")
	case n == 0:
		// Reuse buffer space.
		b.off = 0
	}
	b.buf = b.buf[0 : b.off+n]
}

// Reset resets the buffer so it has no content.
// b.Reset() is the same as b.Truncate(0).
func (b *Buffer) Reset() { b.Truncate(0) }

// grow grows the buffer to guarantee space for n more bytes.
// It returns the index where bytes should be written.
// If the buffer can't grow it will panic with ErrTooLarge.
func (b *Buffer) grow(n int) int {
	m := b.Len()
	// If buffer is empty, reset to recover space.
	if m == 0 && b.off != 0 {
		b.Truncate(0)
	}
	if len(b.buf)+n > cap(b.buf) {
		var buf []byte
		if b.buf == nil && n <= len(b.bootstrap) {
			buf = b.bootstrap[0:]
		} else {
			// not enough space anywhere
			buf = makeSlice(2*cap(b.buf) + n)
			copy(buf, b.buf[b.off:])
		}
		b.buf = buf
		b.off = 0
	}
	b.buf = b.buf[0 : b.off+m+n]
	return b.off + m
}

// Write appends the contents of p to the buffer.  The return
// value n is the length of p; err is always nil.
// If the buffer becomes too large, Write will panic with
// ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
	b.lastRead = opInvalid
	m := b.grow(len(p))
	return copy(b.buf[m:], p), nil
}

// WriteString appends the contents of s to the buffer.  The return
// value n is the length of s; err is always nil.
// If the buffer becomes too large, WriteString will panic with
// ErrTooLarge.
func (b *Buffer) WriteString(s string) (n int, err error) {
	b.lastRead = opInvalid
	m := b.grow(len(s))
	return copy(b.buf[m:], s), nil
}

// MinRead is the minimum slice size passed to a Read call by
// Buffer.ReadFrom.  As long as the Buffer has at least MinRead bytes beyond
// what is required to hold the contents of r, ReadFrom will not grow the
// underlying buffer.
const MinRead = 512

// ReadFrom reads data from r until EOF and appends it to the buffer.
// The return value n is the number of bytes read.
// Any error except io.EOF encountered during the read
// is also returned.
// If the buffer becomes too large, ReadFrom will panic with
// ErrTooLarge.
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
	b.lastRead = opInvalid
	// If buffer is empty, reset to recover space.
	if b.off >= len(b.buf) {
		b.Truncate(0)
	}
	for {
		if free := cap(b.buf) - len(b.buf); free < MinRead {
			// not enough space at end
			newBuf := b.buf
			if b.off+free < MinRead {
				// not enough space using beginning of buffer;
				// double buffer capacity
				newBuf = makeSlice(2*cap(b.buf) + MinRead)
			}
			copy(newBuf, b.buf[b.off:])
			b.buf = newBuf[:len(b.buf)-b.off]
			b.off = 0
		}
		m, e := r.Read(b.buf[len(b.buf):cap(b.buf)])
		b.buf = b.buf[0 : len(b.buf)+m]
		n += int64(m)
		if e == io.EOF {
			break
		}
		if e != nil {
			return n, e
		}
	}
	return n, nil // err is EOF, so return nil explicitly
}

// makeSlice allocates a slice of size n. If the allocation fails, it panics
// with ErrTooLarge.
func makeSlice(n int) []byte {
	// If the make fails, give a known error.
	defer func() {
		if recover() != nil {
			panic(ErrTooLarge)
		}
	}()
	return make([]byte, n)
}

// WriteTo writes data to w until the buffer is drained or an error
// occurs. The return value n is the number of bytes written; it always
// fits into an int, but it is int64 to match the io.WriterTo interface.
// Any error encountered during the write is also returned.
func (b *Buffer) WriteTo(w io.Writer) (n int64, err error) {
	b.lastRead = opInvalid
	if b.off < len(b.buf) {
		m, e := w.Write(b.buf[b.off:])
		b.off += m
		n = int64(m)
		if e != nil {
			return n, e
		}
		// otherwise all bytes were written, by definition of
		// Write method in io.Writer
	}
	// Buffer is now empty; reset.
	b.Truncate(0)
	return
}

// WriteByte appends the byte c to the buffer.
// The returned error is always nil, but is included
// to match bufio.Writer's WriteByte.
// If the buffer becomes too large, WriteByte will panic with
// ErrTooLarge.
func (b *Buffer) WriteByte(c byte) error {
	b.lastRead = opInvalid
	m := b.grow(1)
	b.buf[m] = c
	return nil
}

// WriteRune appends the UTF-8 encoding of Unicode
// code point r to the buffer, returning its length and
// an error, which is always nil but is included
// to match bufio.Writer's WriteRune.
// If the buffer becomes too large, WriteRune will panic with
// ErrTooLarge.
func (b *Buffer) WriteRune(r rune) (n int, err error) {
	if r < utf8.RuneSelf {
		b.WriteByte(byte(r))
		return 1, nil
	}
	n = utf8.EncodeRune(b.runeBytes[0:], r)
	b.Write(b.runeBytes[0:n])
	return n, nil
}

// Read reads the next len(p) bytes from the buffer or until the buffer
// is drained.  The return value n is the number of bytes read.  If the
// buffer has no data to return, err is io.EOF (unless len(p) is zero);
// otherwise it is nil.
func (b *Buffer) Read(p []byte) (n int, err error) {
	b.lastRead = opInvalid
	if b.off >= len(b.buf) {
		// Buffer is empty, reset to recover space.
		b.Truncate(0)
		if len(p) == 0 {
			return
		}
		return 0, io.EOF
	}
	n = copy(p, b.buf[b.off:])
	b.off += n
	if n > 0 {
		b.lastRead = opRead
	}
	return
}

// Next returns a slice containing the next n bytes from the buffer,
// advancing the buffer as if the bytes had been returned by Read.
// If there are fewer than n bytes in the buffer, Next returns the entire buffer.
// The slice is only valid until the next call to a read or write method.
func (b *Buffer) Next(n int) []byte {
	b.lastRead = opInvalid
	m := b.Len()
	if n > m {
		n = m
	}
	data := b.buf[b.off : b.off+n]
	b.off += n
	if n > 0 {
		b.lastRead = opRead
	}
	return data
}

// ReadByte reads and returns the next byte from the buffer.
// If no byte is available, it returns error io.EOF.
func (b *Buffer) ReadByte() (c byte, err error) {
	b.lastRead = opInvalid
	if b.off >= len(b.buf) {
		// Buffer is empty, reset to recover space.
		b.Truncate(0)
		return 0, io.EOF
	}
	c = b.buf[b.off]
	b.off++
	b.lastRead = opRead
	return c, nil
}

// ReadRune reads and returns the next UTF-8-encoded
// Unicode code point from the buffer.
// If no bytes are available, the error returned is io.EOF.
// If the bytes are an erroneous UTF-8 encoding, it
// consumes one byte and returns U+FFFD, 1.
func (b *Buffer) ReadRune() (r rune, size int, err error) {
	b.lastRead = opInvalid
	if b.off >= len(b.buf) {
		// Buffer is empty, reset to recover space.
		b.Truncate(0)
		return 0, 0, io.EOF
	}
	b.lastRead = opReadRune
	c := b.buf[b.off]
	if c < utf8.RuneSelf {
		b.off++
		return rune(c), 1, nil
	}
	r, n := utf8.DecodeRune(b.buf[b.off:])
	b.off += n
	return r, n, nil
}

// UnreadRune unreads the last rune returned by ReadRune.
// If the most recent read or write operation on the buffer was
// not a ReadRune, UnreadRune returns an error.  (In this regard
// it is stricter than UnreadByte, which will unread the last byte
// from any read operation.)
func (b *Buffer) UnreadRune() error {
	if b.lastRead != opReadRune {
		return errors.New("bytes.Buffer: UnreadRune: previous operation was not ReadRune")
	}
	b.lastRead = opInvalid
	if b.off > 0 {
		_, n := utf8.DecodeLastRune(b.buf[0:b.off])
		b.off -= n
	}
	return nil
}

// UnreadByte unreads the last byte returned by the most recent
// read operation.  If write has happened since the last read, UnreadByte
// returns an error.
func (b *Buffer) UnreadByte() error {
	if b.lastRead != opReadRune && b.lastRead != opRead {
		return errors.New("bytes.Buffer: UnreadByte: previous operation was not a read")
	}
	b.lastRead = opInvalid
	if b.off > 0 {
		b.off--
	}
	return nil
}

// ReadBytes reads until the first occurrence of delim in the input,
// returning a slice containing the data up to and including the delimiter.
// If ReadBytes encounters an error before finding a delimiter,
// it returns the data read before the error and the error itself (often io.EOF).
// ReadBytes returns err != nil if and only if the returned data does not end in
// delim.
func (b *Buffer) ReadBytes(delim byte) (line []byte, err error) {
	i := IndexByte(b.buf[b.off:], delim)
	size := i + 1
	if i < 0 {
		size = len(b.buf) - b.off
		err = io.EOF
	}
	line = make([]byte, size)
	copy(line, b.buf[b.off:])
	b.off += size
	return
}

// ReadString reads until the first occurrence of delim in the input,
// returning a string containing the data up to and including the delimiter.
// If ReadString encounters an error before finding a delimiter,
// it returns the data read before the error and the error itself (often io.EOF).
// ReadString returns err != nil if and only if the returned data does not end
// in delim.
func (b *Buffer) ReadString(delim byte) (line string, err error) {
	bytes, err := b.ReadBytes(delim)
	return string(bytes), err
}

// NewBuffer creates and initializes a new Buffer using buf as its initial
// contents.  It is intended to prepare a Buffer to read existing data.  It
// can also be used to size the internal buffer for writing. To do that,
// buf should have the desired capacity but a length of zero.
//
// In most cases, new(Buffer) (or just declaring a Buffer variable) is
// sufficient to initialize a Buffer.
func NewBuffer(buf []byte) *Buffer { return &Buffer{buf: buf} }

// NewBufferString creates and initializes a new Buffer using string s as its
// initial contents. It is intended to prepare a buffer to read an existing
// string.
//
// In most cases, new(Buffer) (or just declaring a Buffer variable) is
// sufficient to initialize a Buffer.
func NewBufferString(s string) *Buffer {
	return &Buffer{buf: []byte(s)}
}
<commit_msg>bytes.Buffer: return error in WriteTo if buffer is not drained<commit_after>// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package bytes

// Simple byte buffer for marshaling data.

import (
	"errors"
	"io"
	"unicode/utf8"
)

// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
	buf       []byte            // contents are the bytes buf[off : len(buf)]
	off       int               // read at &buf[off], write at &buf[len(buf)]
	runeBytes [utf8.UTFMax]byte // avoid allocation of slice on each WriteByte or Rune
	bootstrap [64]byte          // memory to hold first slice; helps small buffers (Printf) avoid allocation.
	lastRead  readOp            // last read operation, so that Unread* can work correctly.
}

// The readOp constants describe the last action performed on
// the buffer, so that UnreadRune and UnreadByte can
// check for invalid usage.
type readOp int

const (
	opInvalid  readOp = iota // Non-read operation.
	opReadRune               // Read rune.
	opRead                   // Any other read operation.
)

// ErrTooLarge is passed to panic if memory cannot be allocated to store data in a buffer.
var ErrTooLarge = errors.New("bytes.Buffer: too large")

// Bytes returns a slice of the contents of the unread portion of the buffer;
// len(b.Bytes()) == b.Len().  If the caller changes the contents of the
// returned slice, the contents of the buffer will change provided there
// are no intervening method calls on the Buffer.
func (b *Buffer) Bytes() []byte { return b.buf[b.off:] }

// String returns the contents of the unread portion of the buffer
// as a string.  If the Buffer is a nil pointer, it returns "<nil>".
func (b *Buffer) String() string {
	if b == nil {
		// Special case, useful in debugging.
		return "<nil>"
	}
	return string(b.buf[b.off:])
}

// Len returns the number of bytes of the unread portion of the buffer;
// b.Len() == len(b.Bytes()).
func (b *Buffer) Len() int { return len(b.buf) - b.off }

// Truncate discards all but the first n unread bytes from the buffer.
// It panics if n is negative or greater than the length of the buffer.
func (b *Buffer) Truncate(n int) {
	b.lastRead = opInvalid
	switch {
	case n < 0 || n > b.Len():
		panic("bytes.Buffer: truncation out of range")
	case n == 0:
		// Reuse buffer space.
		b.off = 0
	}
	b.buf = b.buf[0 : b.off+n]
}

// Reset resets the buffer so it has no content.
// b.Reset() is the same as b.Truncate(0).
func (b *Buffer) Reset() { b.Truncate(0) }

// grow grows the buffer to guarantee space for n more bytes.
// It returns the index where bytes should be written.
// If the buffer can't grow it will panic with ErrTooLarge.
func (b *Buffer) grow(n int) int {
	m := b.Len()
	// If buffer is empty, reset to recover space.
	if m == 0 && b.off != 0 {
		b.Truncate(0)
	}
	if len(b.buf)+n > cap(b.buf) {
		var buf []byte
		if b.buf == nil && n <= len(b.bootstrap) {
			buf = b.bootstrap[0:]
		} else {
			// not enough space anywhere
			buf = makeSlice(2*cap(b.buf) + n)
			copy(buf, b.buf[b.off:])
		}
		b.buf = buf
		b.off = 0
	}
	b.buf = b.buf[0 : b.off+m+n]
	return b.off + m
}

// Write appends the contents of p to the buffer.  The return
// value n is the length of p; err is always nil.
// If the buffer becomes too large, Write will panic with
// ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
	b.lastRead = opInvalid
	m := b.grow(len(p))
	return copy(b.buf[m:], p), nil
}

// WriteString appends the contents of s to the buffer.  The return
// value n is the length of s; err is always nil.
// If the buffer becomes too large, WriteString will panic with
// ErrTooLarge.
func (b *Buffer) WriteString(s string) (n int, err error) {
	b.lastRead = opInvalid
	m := b.grow(len(s))
	return copy(b.buf[m:], s), nil
}

// MinRead is the minimum slice size passed to a Read call by
// Buffer.ReadFrom.  As long as the Buffer has at least MinRead bytes beyond
// what is required to hold the contents of r, ReadFrom will not grow the
// underlying buffer.
const MinRead = 512

// ReadFrom reads data from r until EOF and appends it to the buffer.
// The return value n is the number of bytes read.
// Any error except io.EOF encountered during the read
// is also returned.
// If the buffer becomes too large, ReadFrom will panic with
// ErrTooLarge.
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
	b.lastRead = opInvalid
	// If buffer is empty, reset to recover space.
	if b.off >= len(b.buf) {
		b.Truncate(0)
	}
	for {
		if free := cap(b.buf) - len(b.buf); free < MinRead {
			// not enough space at end
			newBuf := b.buf
			if b.off+free < MinRead {
				// not enough space using beginning of buffer;
				// double buffer capacity
				newBuf = makeSlice(2*cap(b.buf) + MinRead)
			}
			copy(newBuf, b.buf[b.off:])
			b.buf = newBuf[:len(b.buf)-b.off]
			b.off = 0
		}
		m, e := r.Read(b.buf[len(b.buf):cap(b.buf)])
		b.buf = b.buf[0 : len(b.buf)+m]
		n += int64(m)
		if e == io.EOF {
			break
		}
		if e != nil {
			return n, e
		}
	}
	return n, nil // err is EOF, so return nil explicitly
}

// makeSlice allocates a slice of size n. If the allocation fails, it panics
// with ErrTooLarge.
func makeSlice(n int) []byte {
	// If the make fails, give a known error.
	defer func() {
		if recover() != nil {
			panic(ErrTooLarge)
		}
	}()
	return make([]byte, n)
}

// WriteTo writes data to w until the buffer is drained or an error
// occurs. The return value n is the number of bytes written; it always
// fits into an int, but it is int64 to match the io.WriterTo interface.
// Any error encountered during the write is also returned.
func (b *Buffer) WriteTo(w io.Writer) (n int64, err error) {
	b.lastRead = opInvalid
	if b.off < len(b.buf) {
		nBytes := b.Len()
		m, e := w.Write(b.buf[b.off:])
		if m > nBytes {
			panic("bytes.Buffer.WriteTo: invalid Write count")
		}
		b.off += m
		n = int64(m)
		if e != nil {
			return n, e
		}
		// all bytes should have been written, by definition of
		// Write method in io.Writer
		if m != nBytes {
			return n, io.ErrShortWrite
		}
	}
	// Buffer is now empty; reset.
	b.Truncate(0)
	return
}

// WriteByte appends the byte c to the buffer.
// The returned error is always nil, but is included
// to match bufio.Writer's WriteByte.
// If the buffer becomes too large, WriteByte will panic with
// ErrTooLarge.
func (b *Buffer) WriteByte(c byte) error {
	b.lastRead = opInvalid
	m := b.grow(1)
	b.buf[m] = c
	return nil
}

// WriteRune appends the UTF-8 encoding of Unicode
// code point r to the buffer, returning its length and
// an error, which is always nil but is included
// to match bufio.Writer's WriteRune.
// If the buffer becomes too large, WriteRune will panic with
// ErrTooLarge.
func (b *Buffer) WriteRune(r rune) (n int, err error) {
	if r < utf8.RuneSelf {
		b.WriteByte(byte(r))
		return 1, nil
	}
	n = utf8.EncodeRune(b.runeBytes[0:], r)
	b.Write(b.runeBytes[0:n])
	return n, nil
}

// Read reads the next len(p) bytes from the buffer or until the buffer
// is drained.  The return value n is the number of bytes read.  If the
// buffer has no data to return, err is io.EOF (unless len(p) is zero);
// otherwise it is nil.
func (b *Buffer) Read(p []byte) (n int, err error) {
	b.lastRead = opInvalid
	if b.off >= len(b.buf) {
		// Buffer is empty, reset to recover space.
		b.Truncate(0)
		if len(p) == 0 {
			return
		}
		return 0, io.EOF
	}
	n = copy(p, b.buf[b.off:])
	b.off += n
	if n > 0 {
		b.lastRead = opRead
	}
	return
}

// Next returns a slice containing the next n bytes from the buffer,
// advancing the buffer as if the bytes had been returned by Read.
// If there are fewer than n bytes in the buffer, Next returns the entire buffer.
// The slice is only valid until the next call to a read or write method.
func (b *Buffer) Next(n int) []byte {
	b.lastRead = opInvalid
	m := b.Len()
	if n > m {
		n = m
	}
	data := b.buf[b.off : b.off+n]
	b.off += n
	if n > 0 {
		b.lastRead = opRead
	}
	return data
}

// ReadByte reads and returns the next byte from the buffer.
// If no byte is available, it returns error io.EOF.
func (b *Buffer) ReadByte() (c byte, err error) {
	b.lastRead = opInvalid
	if b.off >= len(b.buf) {
		// Buffer is empty, reset to recover space.
		b.Truncate(0)
		return 0, io.EOF
	}
	c = b.buf[b.off]
	b.off++
	b.lastRead = opRead
	return c, nil
}

// ReadRune reads and returns the next UTF-8-encoded
// Unicode code point from the buffer.
// If no bytes are available, the error returned is io.EOF.
// If the bytes are an erroneous UTF-8 encoding, it
// consumes one byte and returns U+FFFD, 1.
func (b *Buffer) ReadRune() (r rune, size int, err error) {
	b.lastRead = opInvalid
	if b.off >= len(b.buf) {
		// Buffer is empty, reset to recover space.
		b.Truncate(0)
		return 0, 0, io.EOF
	}
	b.lastRead = opReadRune
	c := b.buf[b.off]
	if c < utf8.RuneSelf {
		b.off++
		return rune(c), 1, nil
	}
	r, n := utf8.DecodeRune(b.buf[b.off:])
	b.off += n
	return r, n, nil
}

// UnreadRune unreads the last rune returned by ReadRune.
// If the most recent read or write operation on the buffer was
// not a ReadRune, UnreadRune returns an error.  (In this regard
// it is stricter than UnreadByte, which will unread the last byte
// from any read operation.)
func (b *Buffer) UnreadRune() error {
	if b.lastRead != opReadRune {
		return errors.New("bytes.Buffer: UnreadRune: previous operation was not ReadRune")
	}
	b.lastRead = opInvalid
	if b.off > 0 {
		_, n := utf8.DecodeLastRune(b.buf[0:b.off])
		b.off -= n
	}
	return nil
}

// UnreadByte unreads the last byte returned by the most recent
// read operation.  If write has happened since the last read, UnreadByte
// returns an error.
func (b *Buffer) UnreadByte() error {
	if b.lastRead != opReadRune && b.lastRead != opRead {
		return errors.New("bytes.Buffer: UnreadByte: previous operation was not a read")
	}
	b.lastRead = opInvalid
	if b.off > 0 {
		b.off--
	}
	return nil
}

// ReadBytes reads until the first occurrence of delim in the input,
// returning a slice containing the data up to and including the delimiter.
// If ReadBytes encounters an error before finding a delimiter,
// it returns the data read before the error and the error itself (often io.EOF).
// ReadBytes returns err != nil if and only if the returned data does not end in
// delim.
func (b *Buffer) ReadBytes(delim byte) (line []byte, err error) {
	i := IndexByte(b.buf[b.off:], delim)
	size := i + 1
	if i < 0 {
		size = len(b.buf) - b.off
		err = io.EOF
	}
	line = make([]byte, size)
	copy(line, b.buf[b.off:])
	b.off += size
	return
}

// ReadString reads until the first occurrence of delim in the input,
// returning a string containing the data up to and including the delimiter.
// If ReadString encounters an error before finding a delimiter,
// it returns the data read before the error and the error itself (often io.EOF).
// ReadString returns err != nil if and only if the returned data does not end
// in delim.
func (b *Buffer) ReadString(delim byte) (line string, err error) {
	bytes, err := b.ReadBytes(delim)
	return string(bytes), err
}

// NewBuffer creates and initializes a new Buffer using buf as its initial
// contents.  It is intended to prepare a Buffer to read existing data.  It
// can also be used to size the internal buffer for writing. To do that,
// buf should have the desired capacity but a length of zero.
//
// In most cases, new(Buffer) (or just declaring a Buffer variable) is
// sufficient to initialize a Buffer.
func NewBuffer(buf []byte) *Buffer { return &Buffer{buf: buf} }

// NewBufferString creates and initializes a new Buffer using string s as its
// initial contents. It is intended to prepare a buffer to read an existing
// string.
//
// In most cases, new(Buffer) (or just declaring a Buffer variable) is
// sufficient to initialize a Buffer.
func NewBufferString(s string) *Buffer {
	return &Buffer{buf: []byte(s)}
}
<|endoftext|>


<commit_before>package util

import (
	"fmt"
	"net"
	"net/url"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"syscall"

	"github.com/BytemarkHosting/bytemark-client/lib"
	"github.com/BytemarkHosting/bytemark-client/util/log"
	auth3 "gitlab.bytemark.co.uk/auth/client"
)

// UserRequestedExit is returned when the user said 'No' to a 'yes/no' prompt.
type UserRequestedExit struct{}

func (e UserRequestedExit) Error() string {
	return "User requested exit"
}

// ExitCode is a named type for the E_* constants which are used as exit codes.
type ExitCode int

const (
	// ExitCodeUsageDisplayed is returned when some usage info / help page was displayed. Unsure whether it should == E_SUCCESS or not
	ExitCodeUsageDisplayed ExitCode = 0
	// ExitCodeSuccess is used to say everything went well
	ExitCodeSuccess = 0
	// ExitCodeTrappedInterrupt is the exit code returned when an unexpected interrupt like SIGUSR1 was trapped
	ExitCodeTrappedInterrupt = -1
	// ExitCodeClientBug is the exit code returned when bytemark-client knows it's faulty
	ExitCodeClientBug = 1
	// ExitCodeCantReadConfig is the exit code returned when we couldn't read a config variable from the disk for some reason
	ExitCodeCantReadConfig = 3
	// ExitCodeCantWriteConfig is the exit code returned when we couldn't write a config variable to the disk for some reason
	ExitCodeCantWriteConfig = 4
	// ExitCodeUserExit is the exit code returned when the user's action caused the program to terminate (usually by saying no to a prompt)
	ExitCodeUserExit = 5
	// ExitCodeWontDeletePopulated is the exit code returned when the user's requested that a group be deleted when it still had servers in
	ExitCodeWontDeletePopulated = 6
	// ExitCodeBadInput is the exit code returned when the user entered a malformed command, name, or flag.
	ExitCodeBadInput = 7
	// ExitCodeSubprocessFailed is the exit code returned when the client attempted to run a subprocess (e.g. ssh, a browser or a vpn client) but couldn't
	ExitCodeSubprocessFailed = 8

	// ExitCodeNoDefaultAccount is the exit code returned when the client couldn't determine a default account. In this situation, the user should manually specify the account to use with the --account flag or using `bytemark config set account`
	ExitCodeNoDefaultAccount = 9

	// ExitCodeUnknownError is the exit code returned when we got an error we couldn't deal with.
	ExitCodeUnknownError = 49

	// ExitCodeCantConnectAuth is the exit code returned when we were unable to establish an HTTP connection to the auth endpoint.
	ExitCodeCantConnectAuth = 50
	// ExitCodeCantConnectAPI is the exit code returned when we were unable to establish an HTTP connection to the API endpoint.
	ExitCodeCantConnectAPI = 150

	// ExitCodeAuthInternalError is the exit code returned when the auth server reported an internal error.
	ExitCodeAuthInternalError = 51
	// ExitCodeAPIInternalError is the exit code returned when the API server reported an internal error.
	ExitCodeAPIInternalError = 152

	// ExitCodeCantParseAuthResponse is the exit code returned when the auth server returned something we were unable to parse.
	ExitCodeCantParseAuthResponse = 52
	// ExitCodeCantParseAPIResponse is the exit code returned when the API server returned something we were unable to parse.
	ExitCodeCantParseAPIResponse = 152

	// ExitCodeInvalidCredentials is the exit code returned when the auth server says your credentials contain invalid characters.
	ExitCodeInvalidCredentials = 53
	// ExitCodeBadCredentials is the exit code returned when the auth server says your credentials don't match a user in its database.
	ExitCodeBadCredentials = 54

	// ExitCodeActionNotPermitted is the exit code returned when the API server says you haven't got permission to do that.
	ExitCodeActionNotPermitted = 155

	// ExitCodeNotFound is the exit code returned when the API server says you do not have permission to see the object you are trying to view, or that it does not exist.
	ExitCodeNotFound = 156

	// ExitCodeBadRequest is the exit code returned when we send a bad request to API. (E.g. names being too short or having wrong characters in)
	ExitCodeBadRequest = 157

	// ExitCodeUnknownAuthError is the exit code returned when we get an unexpected error from the auth server.
	ExitCodeUnknownAuthError = 149
	// ExitCodeUnknownAPIError is the exit code returned when we get an unexpected error from the Bytemark API.
	ExitCodeUnknownAPIError = 249
)

// HelpForExitCodes prints readable information on what the various exit codes do.
func HelpForExitCodes() ExitCode {
	log.Logf(`bytemark exit code list:

Exit code ranges:
    All these ranges are inclusive (i.e. 0-99 means numbers from 0 to 99, including 0 and 99.)

      0- 49: local problems
     50-149: problem talking to auth.
    150-249: problem talking to Bytemark.
    250-255: interrupts & signals

    Exit codes between 50 and 249 with the same tens and units have the same meaning but for a different endpoint

  0 -  49 Exit codes:

    0
	Nothing went wrong and I feel great!
    1
    	Problem with the client itself
    3
	Couldn't read a file from config directory
    4
	Couldn't write a file to config directory
    5
	The user caused the program to exit (usually by saying "no" to Yes/no prompts)
    6
	The user requested a non-empty group be deleted
    7
	The program was called with malformed arguments
    8
	Attempting to execute a subprocess failed

 50 - 249 Exit codes:

     50 / 150
        Unable to establish a connection to auth/API endpoint

     51 / 151
        Auth endpoint reported an internal error

     52 / 152
        Unable to parse output from auth endpoint (probably implies a protocol mismatch - try updating bytemark)

     53
	Your credentials were rejected for containing invalid characters or fields.

     54
	Your credentials did not match any user on file - check you entered them correctly

    155
	Your user account doesn't have authorisation to perform that action

    156
        Something couldn't be found by the API server. This could be due to the following reasons:
            * It doesn't exist
	    * Your user account doesn't have authorisation to see it
	    * Protocol mismatch between the Bytemark endpoint and our client (i.e. client out of date).

    149 / 249

        An unknown error fell out of the auth / API library.

250 - 255 Exit codes:

    255
	Trapped an interrupt signal, so exited.
`)
	return ExitCodeUsageDisplayed
}

// ProcessError processes the given error, outputs a message, and returns the relevant ExitCode for the given error.
func ProcessError(err error, message ...string) ExitCode {
	if err == nil {
		return ExitCodeSuccess
	}

	trace := make([]byte, 4096)
	runtime.Stack(trace, false)

	log.Debug(log.LvlOutline, "ProcessError called. Dumping arguments and stacktrace", os.Args, string(trace))
	if len(message) > 0 {
		log.Error(message)
	}
	errorMessage := "Unknown error"
	exitCode := ExitCode(ExitCodeUnknownError)
	if err != nil {
		switch e := err.(type) {
		case *auth3.Error:
			// TODO(telyn): I feel like this entire chunk should be in gitlab.bytemark.co.uk/auth/client
			switch e.Err.(type) {
			case *url.Error:
				urlErr, _ := e.Err.(*url.Error)
				if urlErr.Err != nil {
					if opError, ok := urlErr.Err.(*net.OpError); ok {
						errorMessage = fmt.Sprintf("Couldn't connect to the auth server: %v", opError.Err)
					} else {
						errorMessage = fmt.Sprintf("Couldn't connect to the auth server: %T %v\r\nPlease file a bug report quoting this message.", urlErr.Err, urlErr.Err)
					}
				} else {
					errorMessage = fmt.Sprintf("Couldn't connect to the auth server: %v", urlErr)
				}
				exitCode = ExitCodeCantConnectAuth
			default:
				errorMessage = fmt.Sprintf("Couldn't create auth session: %v", e.Err)
				exitCode = ExitCodeUnknownAuthError
			}
		case *url.Error:
			if e.Err != nil {
				if opError, ok := e.Err.(*net.OpError); ok {
					errorMessage = fmt.Sprintf("Couldn't connect to the Bytemark API: %v", opError.Err)
				} else {
					errorMessage = fmt.Sprintf("Couldn't connect to the Bytemark API: %T %v\r\nPlease file a bug report quoting this message.", e.Err, e.Err)
				}
			} else {
				errorMessage = fmt.Sprintf("Couldn't connect to the Bytemark API: %v", e)
			}
		case *exec.Error:
			if e.Name == "xdg-open" || e.Name == "x-www-browser" {
				errorMessage = "Unable to find a browser to start. You may wish to install xdg-open (part of the xdg-utils package on Debian systems)"
			} else if e.Name == "open" {
				errorMessage = "Unable to find a browser to start. Ensure that the 'open' tool is in your PATH (usually lives in /usr/bin)."

			} else if e.Name == "ssh" {
				errorMessage = fmt.Sprintf("Unable to find an SSH client, please check you have one installed.")
			} else {
				errorMessage = fmt.Sprintf("Unable to find %s in your PATH.", e.Name)
			}
			exitCode = ExitCodeSubprocessFailed
		case SubprocessFailedError:
			if e.Err == nil {
				return ExitCodeSuccess
			}
			errorMessage = err.Error()
			exitCode = ExitCodeSubprocessFailed
		case lib.NoDefaultAccountError:
			errorMessage = err.Error()
			exitCode = ExitCodeNoDefaultAccount
		case lib.NilAuthError:
			errorMessage = "Authorization wasn't set up in the client - please file a bug report containing the name of the command you tried to run."
			exitCode = ExitCodeClientBug
		case lib.ForbiddenError:
			errorMessage = err.Error()
			exitCode = ExitCodeActionNotPermitted
		case lib.BadRequestError:
			errorMessage = err.Error()
			exitCode = ExitCodeBadRequest
		case lib.ServiceUnavailableError:
			errorMessage = err.Error()
			exitCode = ExitCodeCantConnectAPI
		case lib.InternalServerError:
			errorMessage = err.Error()
			exitCode = ExitCodeAPIInternalError
		case lib.NotFoundError:
			errorMessage = err.Error()
			exitCode = ExitCodeNotFound
		case UserRequestedExit:
			errorMessage = ""
			exitCode = ExitCodeUserExit
		case *syscall.Errno:
			errorMessage = fmt.Sprintf("A command we tried to execute failed. The operating system gave us the error code %d", e)
			exitCode = ExitCodeUnknownError
		case lib.AmbiguousKeyError:
			exitCode = ExitCodeBadInput
			errorMessage = err.Error()
		case UsageDisplayedError:
			exitCode = ExitCodeBadInput
			errorMessage = err.Error()

		default:
			if fmt.Sprintf("%T", err) == "*errors.errorString" {
				errorMessage = err.Error()
				exitCode = ExitCodeBadInput // just going with BadInput because most errorStrings come from auth or validation functions.
			}
			msg := err.Error()
			if strings.Contains(msg, "Badly-formed parameters") {
				exitCode = ExitCodeInvalidCredentials
				errorMessage = "The supplied credentials contained invalid characters - please try again"
			} else if strings.Contains(msg, "Bad login credentials") {
				exitCode = ExitCodeBadCredentials
				errorMessage = "A user account with those credentials could not be found. Check your details and try again"

			}
		}

		if _, ok := err.(lib.APIError); ok && exitCode == ExitCodeUnknownError {
			errorMessage = fmt.Sprintf("Unknown error from API client library. %s", err.Error())
			exitCode = ExitCodeUnknownAPIError
		}
	} else {
		exitCode = 0
	}

	if exitCode == ExitCodeUnknownError {
		log.Errorf("Unknown error of type %T: %s.\r\nPlease send a bug report containing %s to support@bytemark.co.uk.\r\n", err, err, log.LogFile.Name())
	} else if len(message) == 0 { // the message (passed as argument) is shadowed by errorMessage (made in this function)
		log.Log(errorMessage)

	}
	return exitCode
}
<commit_msg>Support WontDeleteGroupWithVMsError in ProcessError<commit_after>package util

import (
	"fmt"
	"net"
	"net/url"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"syscall"

	"github.com/BytemarkHosting/bytemark-client/lib"
	"github.com/BytemarkHosting/bytemark-client/util/log"
	auth3 "gitlab.bytemark.co.uk/auth/client"
)

// UserRequestedExit is returned when the user said 'No' to a 'yes/no' prompt.
type UserRequestedExit struct{}

func (e UserRequestedExit) Error() string {
	return "User requested exit"
}

// ExitCode is a named type for the E_* constants which are used as exit codes.
type ExitCode int

const (
	// ExitCodeUsageDisplayed is returned when some usage info / help page was displayed. Unsure whether it should == E_SUCCESS or not
	ExitCodeUsageDisplayed ExitCode = 0
	// ExitCodeSuccess is used to say everything went well
	ExitCodeSuccess = 0
	// ExitCodeTrappedInterrupt is the exit code returned when an unexpected interrupt like SIGUSR1 was trapped
	ExitCodeTrappedInterrupt = -1
	// ExitCodeClientBug is the exit code returned when bytemark-client knows it's faulty
	ExitCodeClientBug = 1
	// ExitCodeCantReadConfig is the exit code returned when we couldn't read a config variable from the disk for some reason
	ExitCodeCantReadConfig = 3
	// ExitCodeCantWriteConfig is the exit code returned when we couldn't write a config variable to the disk for some reason
	ExitCodeCantWriteConfig = 4
	// ExitCodeUserExit is the exit code returned when the user's action caused the program to terminate (usually by saying no to a prompt)
	ExitCodeUserExit = 5
	// ExitCodeWontDeletePopulated is the exit code returned when the user's requested that a group be deleted when it still had servers in
	ExitCodeWontDeletePopulated = 6
	// ExitCodeBadInput is the exit code returned when the user entered a malformed command, name, or flag.
	ExitCodeBadInput = 7
	// ExitCodeSubprocessFailed is the exit code returned when the client attempted to run a subprocess (e.g. ssh, a browser or a vpn client) but couldn't
	ExitCodeSubprocessFailed = 8

	// ExitCodeNoDefaultAccount is the exit code returned when the client couldn't determine a default account. In this situation, the user should manually specify the account to use with the --account flag or using `bytemark config set account`
	ExitCodeNoDefaultAccount = 9

	// ExitCodeUnknownError is the exit code returned when we got an error we couldn't deal with.
	ExitCodeUnknownError = 49

	// ExitCodeCantConnectAuth is the exit code returned when we were unable to establish an HTTP connection to the auth endpoint.
	ExitCodeCantConnectAuth = 50
	// ExitCodeCantConnectAPI is the exit code returned when we were unable to establish an HTTP connection to the API endpoint.
	ExitCodeCantConnectAPI = 150

	// ExitCodeAuthInternalError is the exit code returned when the auth server reported an internal error.
	ExitCodeAuthInternalError = 51
	// ExitCodeAPIInternalError is the exit code returned when the API server reported an internal error.
	ExitCodeAPIInternalError = 152

	// ExitCodeCantParseAuthResponse is the exit code returned when the auth server returned something we were unable to parse.
	ExitCodeCantParseAuthResponse = 52
	// ExitCodeCantParseAPIResponse is the exit code returned when the API server returned something we were unable to parse.
	ExitCodeCantParseAPIResponse = 152

	// ExitCodeInvalidCredentials is the exit code returned when the auth server says your credentials contain invalid characters.
	ExitCodeInvalidCredentials = 53
	// ExitCodeBadCredentials is the exit code returned when the auth server says your credentials don't match a user in its database.
	ExitCodeBadCredentials = 54

	// ExitCodeActionNotPermitted is the exit code returned when the API server says you haven't got permission to do that.
	ExitCodeActionNotPermitted = 155

	// ExitCodeNotFound is the exit code returned when the API server says you do not have permission to see the object you are trying to view, or that it does not exist.
	ExitCodeNotFound = 156

	// ExitCodeBadRequest is the exit code returned when we send a bad request to API. (E.g. names being too short or having wrong characters in)
	ExitCodeBadRequest = 157

	// ExitCodeUnknownAuthError is the exit code returned when we get an unexpected error from the auth server.
	ExitCodeUnknownAuthError = 149
	// ExitCodeUnknownAPIError is the exit code returned when we get an unexpected error from the Bytemark API.
	ExitCodeUnknownAPIError = 249
)

// HelpForExitCodes prints readable information on what the various exit codes do.
func HelpForExitCodes() ExitCode {
	log.Logf(`bytemark exit code list:

Exit code ranges:
    All these ranges are inclusive (i.e. 0-99 means numbers from 0 to 99, including 0 and 99.)

      0- 49: local problems
     50-149: problem talking to auth.
    150-249: problem talking to Bytemark.
    250-255: interrupts & signals

    Exit codes between 50 and 249 with the same tens and units have the same meaning but for a different endpoint

  0 -  49 Exit codes:

    0
	Nothing went wrong and I feel great!
    1
    	Problem with the client itself
    3
	Couldn't read a file from config directory
    4
	Couldn't write a file to config directory
    5
	The user caused the program to exit (usually by saying "no" to Yes/no prompts)
    6
	The user requested a non-empty group be deleted
    7
	The program was called with malformed arguments
    8
	Attempting to execute a subprocess failed

 50 - 249 Exit codes:

     50 / 150
        Unable to establish a connection to auth/API endpoint

     51 / 151
        Auth endpoint reported an internal error

     52 / 152
        Unable to parse output from auth endpoint (probably implies a protocol mismatch - try updating bytemark)

     53
	Your credentials were rejected for containing invalid characters or fields.

     54
	Your credentials did not match any user on file - check you entered them correctly

    155
	Your user account doesn't have authorisation to perform that action

    156
        Something couldn't be found by the API server. This could be due to the following reasons:
            * It doesn't exist
	    * Your user account doesn't have authorisation to see it
	    * Protocol mismatch between the Bytemark endpoint and our client (i.e. client out of date).

    149 / 249

        An unknown error fell out of the auth / API library.

250 - 255 Exit codes:

    255
	Trapped an interrupt signal, so exited.
`)
	return ExitCodeUsageDisplayed
}

// ProcessError processes the given error, outputs a message, and returns the relevant ExitCode for the given error.
func ProcessError(err error, message ...string) ExitCode {
	if err == nil {
		return ExitCodeSuccess
	}

	trace := make([]byte, 4096)
	runtime.Stack(trace, false)

	log.Debug(log.LvlOutline, "ProcessError called. Dumping arguments and stacktrace", os.Args, string(trace))
	if len(message) > 0 {
		log.Error(message)
	}
	errorMessage := "Unknown error"
	exitCode := ExitCode(ExitCodeUnknownError)
	if err != nil {
		switch e := err.(type) {
		case *auth3.Error:
			// TODO(telyn): I feel like this entire chunk should be in gitlab.bytemark.co.uk/auth/client
			switch e.Err.(type) {
			case *url.Error:
				urlErr, _ := e.Err.(*url.Error)
				if urlErr.Err != nil {
					if opError, ok := urlErr.Err.(*net.OpError); ok {
						errorMessage = fmt.Sprintf("Couldn't connect to the auth server: %v", opError.Err)
					} else {
						errorMessage = fmt.Sprintf("Couldn't connect to the auth server: %T %v\r\nPlease file a bug report quoting this message.", urlErr.Err, urlErr.Err)
					}
				} else {
					errorMessage = fmt.Sprintf("Couldn't connect to the auth server: %v", urlErr)
				}
				exitCode = ExitCodeCantConnectAuth
			default:
				errorMessage = fmt.Sprintf("Couldn't create auth session: %v", e.Err)
				exitCode = ExitCodeUnknownAuthError
			}
		case *url.Error:
			if e.Err != nil {
				if opError, ok := e.Err.(*net.OpError); ok {
					errorMessage = fmt.Sprintf("Couldn't connect to the Bytemark API: %v", opError.Err)
				} else {
					errorMessage = fmt.Sprintf("Couldn't connect to the Bytemark API: %T %v\r\nPlease file a bug report quoting this message.", e.Err, e.Err)
				}
			} else {
				errorMessage = fmt.Sprintf("Couldn't connect to the Bytemark API: %v", e)
			}
		case *exec.Error:
			if e.Name == "xdg-open" || e.Name == "x-www-browser" {
				errorMessage = "Unable to find a browser to start. You may wish to install xdg-open (part of the xdg-utils package on Debian systems)"
			} else if e.Name == "open" {
				errorMessage = "Unable to find a browser to start. Ensure that the 'open' tool is in your PATH (usually lives in /usr/bin)."

			} else if e.Name == "ssh" {
				errorMessage = fmt.Sprintf("Unable to find an SSH client, please check you have one installed.")
			} else {
				errorMessage = fmt.Sprintf("Unable to find %s in your PATH.", e.Name)
			}
			exitCode = ExitCodeSubprocessFailed
		case SubprocessFailedError:
			if e.Err == nil {
				return ExitCodeSuccess
			}
			errorMessage = err.Error()
			exitCode = ExitCodeSubprocessFailed
		case lib.NoDefaultAccountError:
			errorMessage = err.Error()
			exitCode = ExitCodeNoDefaultAccount
		case lib.NilAuthError:
			errorMessage = "Authorization wasn't set up in the client - please file a bug report containing the name of the command you tried to run."
			exitCode = ExitCodeClientBug
		case lib.ForbiddenError:
			errorMessage = err.Error()
			exitCode = ExitCodeActionNotPermitted
		case lib.BadRequestError:
			errorMessage = err.Error()
			exitCode = ExitCodeBadRequest
		case lib.ServiceUnavailableError:
			errorMessage = err.Error()
			exitCode = ExitCodeCantConnectAPI
		case lib.InternalServerError:
			errorMessage = err.Error()
			exitCode = ExitCodeAPIInternalError
		case lib.NotFoundError:
			errorMessage = err.Error()
			exitCode = ExitCodeNotFound
		case util.WontDeleteGroupWithVMsError:
			errorMessage = err.Error()
			exitCode = ExitCodeWontDeletePopulated
		case UserRequestedExit:
			errorMessage = ""
			exitCode = ExitCodeUserExit
		case *syscall.Errno:
			errorMessage = fmt.Sprintf("A command we tried to execute failed. The operating system gave us the error code %d", e)
			exitCode = ExitCodeUnknownError
		case lib.AmbiguousKeyError:
			exitCode = ExitCodeBadInput
			errorMessage = err.Error()
		case UsageDisplayedError:
			exitCode = ExitCodeBadInput
			errorMessage = err.Error()

		default:
			if fmt.Sprintf("%T", err) == "*errors.errorString" {
				errorMessage = err.Error()
				exitCode = ExitCodeBadInput // just going with BadInput because most errorStrings come from auth or validation functions.
			}
			msg := err.Error()
			if strings.Contains(msg, "Badly-formed parameters") {
				exitCode = ExitCodeInvalidCredentials
				errorMessage = "The supplied credentials contained invalid characters - please try again"
			} else if strings.Contains(msg, "Bad login credentials") {
				exitCode = ExitCodeBadCredentials
				errorMessage = "A user account with those credentials could not be found. Check your details and try again"

			}
		}

		if _, ok := err.(lib.APIError); ok && exitCode == ExitCodeUnknownError {
			errorMessage = fmt.Sprintf("Unknown error from API client library. %s", err.Error())
			exitCode = ExitCodeUnknownAPIError
		}
	} else {
		exitCode = 0
	}

	if exitCode == ExitCodeUnknownError {
		log.Errorf("Unknown error of type %T: %s.\r\nPlease send a bug report containing %s to support@bytemark.co.uk.\r\n", err, err, log.LogFile.Name())
	} else if len(message) == 0 { // the message (passed as argument) is shadowed by errorMessage (made in this function)
		log.Log(errorMessage)

	}
	return exitCode
}
<|endoftext|>


<commit_before>package cmd

import (
	"fmt"

	bosherr "github.com/cloudfoundry/bosh-agent/errors"
	boshlog "github.com/cloudfoundry/bosh-agent/logger"
	boshsys "github.com/cloudfoundry/bosh-agent/system"
	biblobstore "github.com/cloudfoundry/bosh-init/blobstore"
	bicloud "github.com/cloudfoundry/bosh-init/cloud"
	biconfig "github.com/cloudfoundry/bosh-init/config"
	bicpirel "github.com/cloudfoundry/bosh-init/cpi/release"
	bidepl "github.com/cloudfoundry/bosh-init/deployment"
	bihttpagent "github.com/cloudfoundry/bosh-init/deployment/agentclient/http"
	biinstall "github.com/cloudfoundry/bosh-init/installation"
	biinstallmanifest "github.com/cloudfoundry/bosh-init/installation/manifest"
	bitarball "github.com/cloudfoundry/bosh-init/installation/tarball"
	birel "github.com/cloudfoundry/bosh-init/release"
	birelmanifest "github.com/cloudfoundry/bosh-init/release/manifest"
	birelsetmanifest "github.com/cloudfoundry/bosh-init/release/set/manifest"
	biui "github.com/cloudfoundry/bosh-init/ui"
)

func NewDeploymentDeleter(
	ui biui.UI,
	logTag string,
	logger boshlog.Logger,
	fs boshsys.FileSystem,
	deploymentStateService biconfig.DeploymentStateService,
	releaseManager birel.Manager,
	installerFactory biinstall.InstallerFactory,
	cloudFactory bicloud.Factory,
	agentClientFactory bihttpagent.AgentClientFactory,
	blobstoreFactory biblobstore.Factory,
	deploymentManagerFactory bidepl.ManagerFactory,
	releaseSetParser birelsetmanifest.Parser,
	releaseSetValidator birelsetmanifest.Validator,
	releaseExtractor birel.Extractor,
	installationParser biinstallmanifest.Parser,
	installationValidator biinstallmanifest.Validator,
	deploymentManifestPath string,
	tarballProvider bitarball.Provider,

) DeploymentDeleter {
	return DeploymentDeleter{
		ui:     ui,
		logTag: logTag,
		logger: logger,
		fs:     fs,
		deploymentStateService:   deploymentStateService,
		releaseManager:           releaseManager,
		installerFactory:         installerFactory,
		cloudFactory:             cloudFactory,
		agentClientFactory:       agentClientFactory,
		blobstoreFactory:         blobstoreFactory,
		deploymentManagerFactory: deploymentManagerFactory,
		releaseSetParser:         releaseSetParser,
		releaseSetValidator:      releaseSetValidator,
		releaseExtractor:         releaseExtractor,
		installationParser:       installationParser,
		installationValidator:    installationValidator,
		deploymentManifestPath:   deploymentManifestPath,
		tarballProvider:          tarballProvider,
	}
}

type DeploymentDeleter struct {
	ui                       biui.UI
	logTag                   string
	logger                   boshlog.Logger
	fs                       boshsys.FileSystem
	deploymentStateService   biconfig.DeploymentStateService
	releaseManager           birel.Manager
	installerFactory         biinstall.InstallerFactory
	cloudFactory             bicloud.Factory
	agentClientFactory       bihttpagent.AgentClientFactory
	blobstoreFactory         biblobstore.Factory
	deploymentManagerFactory bidepl.ManagerFactory
	releaseSetParser         birelsetmanifest.Parser
	releaseSetValidator      birelsetmanifest.Validator
	releaseExtractor         birel.Extractor
	installationParser       biinstallmanifest.Parser
	installationValidator    biinstallmanifest.Validator
	deploymentManifestPath   string
	tarballProvider          bitarball.Provider
}

func (c *DeploymentDeleter) DeleteDeployment(stage biui.Stage) (err error) {
	c.ui.PrintLinef("Deployment state: '%s'", c.deploymentStateService.Path())

	if !c.deploymentStateService.Exists() {
		c.ui.PrintLinef("No deployment state file found.")
		return nil
	}

	deploymentState, err := c.deploymentStateService.Load()
	if err != nil {
		return bosherr.WrapError(err, "Loading deployment state")
	}

	defer func() {
		err := c.releaseManager.DeleteAll()
		if err != nil {
			c.logger.Warn(c.logTag, "Deleting all extracted releases: %s", err.Error())
		}
	}()

	var installationManifest biinstallmanifest.Manifest
	err = stage.PerformComplex("validating", func(stage biui.Stage) error {
		var err error
		var cpiReleaseRef birelmanifest.ReleaseRef

		installationManifest, cpiReleaseRef, err = c.parseDeploymentManifest(stage, c.deploymentManifestPath)
		if err != nil {
			return err
		}

		cpiJobName := installationManifest.Template.Name
		return c.extractAndValidateCpiRelease(stage, cpiJobName, cpiReleaseRef)
	})
	if err != nil {
		return err
	}

	installer, err := c.installerFactory.NewInstaller()
	if err != nil {
		return bosherr.WrapError(err, "Creating CPI Installer")
	}

	var installation biinstall.Installation
	err = stage.PerformComplex("installing CPI", func(installStage biui.Stage) error {
		installation, err = installer.Install(installationManifest, installStage)
		return err
	})
	if err != nil {
		return err
	}

	return installation.WithRunningRegistry(c.logger, stage, func() error {
		c.logger.Debug(c.logTag, "Creating cloud client...")
		cloud, err := c.cloudFactory.NewCloud(installation, deploymentState.DirectorID)
		if err != nil {
			return bosherr.WrapError(err, "Creating CPI client from CPI installation")
		}

		c.logger.Debug(c.logTag, "Creating agent client...")
		agentClient := c.agentClientFactory.NewAgentClient(deploymentState.DirectorID, installationManifest.Mbus)

		c.logger.Debug(c.logTag, "Creating blobstore client...")
		blobstore, err := c.blobstoreFactory.Create(installationManifest.Mbus)
		if err != nil {
			return bosherr.WrapError(err, "Creating blobstore client")
		}

		c.logger.Debug(c.logTag, "Creating deployment manager...")
		deploymentManager := c.deploymentManagerFactory.NewManager(cloud, agentClient, blobstore)

		c.logger.Debug(c.logTag, "Finding current deployment...")
		deployment, found, err := deploymentManager.FindCurrent()
		if err != nil {
			return bosherr.WrapError(err, "Finding current deployment")
		}

		err = stage.PerformComplex("deleting deployment", func(deleteStage biui.Stage) error {
			if !found {
				//TODO: skip? would require adding skip support to PerformComplex
				c.logger.Debug(c.logTag, "No current deployment found...")
				return nil
			}

			return deployment.Delete(deleteStage)
		})
		if err != nil {
			return bosherr.WrapError(err, "Deleting deployment")
		}

		return deploymentManager.Cleanup(stage)
	})
}

func (c *DeploymentDeleter) extractAndValidateCpiRelease(stage biui.Stage, cpiJobName string, cpiReleaseRef birelmanifest.ReleaseRef) error {
	releasePath, err := c.tarballProvider.Get(bitarball.Source(cpiReleaseRef), stage)
	if err != nil {
		return err
	}

	err = stage.Perform(fmt.Sprintf("Validating release '%s'", cpiReleaseRef.Name), func() error {
		cpiRelease, err := c.releaseExtractor.Extract(releasePath)
		if err != nil {
			return bosherr.WrapErrorf(err, "Extracting release '%s'", releasePath)
		}
		c.releaseManager.Add(cpiRelease)

		err = bicpirel.NewValidator().Validate(cpiRelease, cpiJobName)
		if err != nil {
			return bosherr.WrapErrorf(err, "Invalid CPI release '%s'", cpiRelease.Name())
		}

		return nil
	})
	return err
}

func (c *DeploymentDeleter) parseDeploymentManifest(validationStage biui.Stage, deploymentManifestPath string) (
	biinstallmanifest.Manifest,
	birelmanifest.ReleaseRef,
	error,
) {
	var cpiRelease birelmanifest.ReleaseRef
	var releaseSetManifest birelsetmanifest.Manifest
	var installationManifest biinstallmanifest.Manifest

	err := validationStage.Perform("Validating deployment manifest", func() error {
		var err error
		installationManifest, err = c.installationParser.Parse(deploymentManifestPath)
		if err != nil {
			return bosherr.WrapErrorf(err, "Parsing installation manifest '%s'", deploymentManifestPath)
		}

		releaseSetManifest, err = c.releaseSetParser.Parse(deploymentManifestPath)
		if err != nil {
			return bosherr.WrapErrorf(err, "Parsing release set manifest '%s'", deploymentManifestPath)
		}

		err = c.releaseSetValidator.Validate(releaseSetManifest)
		if err != nil {
			return bosherr.WrapError(err, "Validating release set manifest")
		}

		err = c.installationValidator.Validate(installationManifest, releaseSetManifest)
		if err != nil {
			return bosherr.WrapError(err, "Validating installation manifest")
		}
		cpiReleaseName := installationManifest.Template.Release

		var found bool
		cpiRelease, found = releaseSetManifest.FindByName(cpiReleaseName)
		if !found {
			return bosherr.Errorf("installation release '%s' must refer to a release in releases", cpiReleaseName)
		}

		return nil
	})
	return installationManifest, cpiRelease, err
}
<commit_msg>Break up 100 line method into smaller methods<commit_after>package cmd

import (
	"fmt"

	bosherr "github.com/cloudfoundry/bosh-agent/errors"
	boshlog "github.com/cloudfoundry/bosh-agent/logger"
	boshsys "github.com/cloudfoundry/bosh-agent/system"
	biblobstore "github.com/cloudfoundry/bosh-init/blobstore"
	bicloud "github.com/cloudfoundry/bosh-init/cloud"
	biconfig "github.com/cloudfoundry/bosh-init/config"
	bicpirel "github.com/cloudfoundry/bosh-init/cpi/release"
	bidepl "github.com/cloudfoundry/bosh-init/deployment"
	bihttpagent "github.com/cloudfoundry/bosh-init/deployment/agentclient/http"
	biinstall "github.com/cloudfoundry/bosh-init/installation"
	biinstallmanifest "github.com/cloudfoundry/bosh-init/installation/manifest"
	bitarball "github.com/cloudfoundry/bosh-init/installation/tarball"
	birel "github.com/cloudfoundry/bosh-init/release"
	birelmanifest "github.com/cloudfoundry/bosh-init/release/manifest"
	birelsetmanifest "github.com/cloudfoundry/bosh-init/release/set/manifest"
	biui "github.com/cloudfoundry/bosh-init/ui"
)

func NewDeploymentDeleter(
	ui biui.UI,
	logTag string,
	logger boshlog.Logger,
	fs boshsys.FileSystem,
	deploymentStateService biconfig.DeploymentStateService,
	releaseManager birel.Manager,
	installerFactory biinstall.InstallerFactory,
	cloudFactory bicloud.Factory,
	agentClientFactory bihttpagent.AgentClientFactory,
	blobstoreFactory biblobstore.Factory,
	deploymentManagerFactory bidepl.ManagerFactory,
	releaseSetParser birelsetmanifest.Parser,
	releaseSetValidator birelsetmanifest.Validator,
	releaseExtractor birel.Extractor,
	installationParser biinstallmanifest.Parser,
	installationValidator biinstallmanifest.Validator,
	deploymentManifestPath string,
	tarballProvider bitarball.Provider,

) DeploymentDeleter {
	return DeploymentDeleter{
		ui:     ui,
		logTag: logTag,
		logger: logger,
		fs:     fs,
		deploymentStateService:   deploymentStateService,
		releaseManager:           releaseManager,
		installerFactory:         installerFactory,
		cloudFactory:             cloudFactory,
		agentClientFactory:       agentClientFactory,
		blobstoreFactory:         blobstoreFactory,
		deploymentManagerFactory: deploymentManagerFactory,
		releaseSetParser:         releaseSetParser,
		releaseSetValidator:      releaseSetValidator,
		releaseExtractor:         releaseExtractor,
		installationParser:       installationParser,
		installationValidator:    installationValidator,
		deploymentManifestPath:   deploymentManifestPath,
		tarballProvider:          tarballProvider,
	}
}

type DeploymentDeleter struct {
	ui                       biui.UI
	logTag                   string
	logger                   boshlog.Logger
	fs                       boshsys.FileSystem
	deploymentStateService   biconfig.DeploymentStateService
	releaseManager           birel.Manager
	installerFactory         biinstall.InstallerFactory
	cloudFactory             bicloud.Factory
	agentClientFactory       bihttpagent.AgentClientFactory
	blobstoreFactory         biblobstore.Factory
	deploymentManagerFactory bidepl.ManagerFactory
	releaseSetParser         birelsetmanifest.Parser
	releaseSetValidator      birelsetmanifest.Validator
	releaseExtractor         birel.Extractor
	installationParser       biinstallmanifest.Parser
	installationValidator    biinstallmanifest.Validator
	deploymentManifestPath   string
	tarballProvider          bitarball.Provider
}

func (c *DeploymentDeleter) DeleteDeployment(stage biui.Stage) (err error) {
	c.ui.PrintLinef("Deployment state: '%s'", c.deploymentStateService.Path())

	if !c.deploymentStateService.Exists() {
		c.ui.PrintLinef("No deployment state file found.")
		return nil
	}

	deploymentState, err := c.deploymentStateService.Load()
	if err != nil {
		return bosherr.WrapError(err, "Loading deployment state")
	}

	defer func() {
		err := c.releaseManager.DeleteAll()
		if err != nil {
			c.logger.Warn(c.logTag, "Deleting all extracted releases: %s", err.Error())
		}
	}()

	installationManifest, installation, err := c.installCPI(stage)
	if err != nil {
		return err
	}

	return installation.WithRunningRegistry(c.logger, stage, func() error {
		return c.findAndDeleteDeployment(stage, installation, deploymentState.DirectorID, installationManifest.Mbus)
	})
}

func (c *DeploymentDeleter) findAndDeleteDeployment(stage biui.Stage, installation biinstall.Installation, directorID, installationMbus string) error {
	deploymentManager, err := c.deploymentManager(installation, directorID, installationMbus)
	if err != nil {
		return err
	}

	c.logger.Debug(c.logTag, "Finding current deployment...")
	deployment, found, err := deploymentManager.FindCurrent()
	if err != nil {
		return bosherr.WrapError(err, "Finding current deployment")
	}

	err = stage.PerformComplex("deleting deployment", func(deleteStage biui.Stage) error {
		if !found {
			//TODO: skip? would require adding skip support to PerformComplex
			c.logger.Debug(c.logTag, "No current deployment found...")
			return nil
		}

		return deployment.Delete(deleteStage)
	})
	if err != nil {
		return bosherr.WrapError(err, "Deleting deployment")
	}

	return deploymentManager.Cleanup(stage)
}

func (c *DeploymentDeleter) installCPI(stage biui.Stage) (biinstallmanifest.Manifest, biinstall.Installation, error) {
	installationManifest, err := c.installationManifest(stage)
	if err != nil {
		return installationManifest, nil, err
	}

	installer, err := c.installerFactory.NewInstaller()
	if err != nil {
		return installationManifest, nil, bosherr.WrapError(err, "Creating CPI Installer")
	}

	var installation biinstall.Installation
	err = stage.PerformComplex("installing CPI", func(installStage biui.Stage) error {
		installation, err = installer.Install(installationManifest, installStage)
		return err
	})
	return installationManifest, installation, err
}

func (c *DeploymentDeleter) installationManifest(stage biui.Stage) (biinstallmanifest.Manifest, error) {
	var installationManifest biinstallmanifest.Manifest
	err := stage.PerformComplex("validating", func(stage biui.Stage) error {
		var err error
		var cpiReleaseRef birelmanifest.ReleaseRef
		installationManifest, cpiReleaseRef, err = c.parseDeploymentManifest(stage, c.deploymentManifestPath)
		if err != nil {
			return err
		}
		return c.extractAndValidateCpiRelease(stage, installationManifest.Template.Name, cpiReleaseRef)
	})
	return installationManifest, err
}

func (c *DeploymentDeleter) deploymentManager(installation biinstall.Installation, directorID, installationMbus string) (bidepl.Manager, error) {
	c.logger.Debug(c.logTag, "Creating cloud client...")
	cloud, err := c.cloudFactory.NewCloud(installation, directorID)
	if err != nil {
		return nil, bosherr.WrapError(err, "Creating CPI client from CPI installation")
	}

	c.logger.Debug(c.logTag, "Creating agent client...")
	agentClient := c.agentClientFactory.NewAgentClient(directorID, installationMbus)

	c.logger.Debug(c.logTag, "Creating blobstore client...")
	blobstore, err := c.blobstoreFactory.Create(installationMbus)
	if err != nil {
		return nil, bosherr.WrapError(err, "Creating blobstore client")
	}

	c.logger.Debug(c.logTag, "Creating deployment manager...")
	return c.deploymentManagerFactory.NewManager(cloud, agentClient, blobstore), nil
}

func (c *DeploymentDeleter) extractAndValidateCpiRelease(stage biui.Stage, cpiJobName string, cpiReleaseRef birelmanifest.ReleaseRef) error {
	releasePath, err := c.tarballProvider.Get(bitarball.Source(cpiReleaseRef), stage)
	if err != nil {
		return err
	}

	err = stage.Perform(fmt.Sprintf("Validating release '%s'", cpiReleaseRef.Name), func() error {
		cpiRelease, err := c.releaseExtractor.Extract(releasePath)
		if err != nil {
			return bosherr.WrapErrorf(err, "Extracting release '%s'", releasePath)
		}
		c.releaseManager.Add(cpiRelease)

		err = bicpirel.NewValidator().Validate(cpiRelease, cpiJobName)
		if err != nil {
			return bosherr.WrapErrorf(err, "Invalid CPI release '%s'", cpiRelease.Name())
		}

		return nil
	})
	return err
}

func (c *DeploymentDeleter) parseDeploymentManifest(validationStage biui.Stage, deploymentManifestPath string) (
	biinstallmanifest.Manifest,
	birelmanifest.ReleaseRef,
	error,
) {
	var cpiRelease birelmanifest.ReleaseRef
	var releaseSetManifest birelsetmanifest.Manifest
	var installationManifest biinstallmanifest.Manifest

	err := validationStage.Perform("Validating deployment manifest", func() error {
		var err error
		installationManifest, err = c.installationParser.Parse(deploymentManifestPath)
		if err != nil {
			return bosherr.WrapErrorf(err, "Parsing installation manifest '%s'", deploymentManifestPath)
		}

		releaseSetManifest, err = c.releaseSetParser.Parse(deploymentManifestPath)
		if err != nil {
			return bosherr.WrapErrorf(err, "Parsing release set manifest '%s'", deploymentManifestPath)
		}

		err = c.releaseSetValidator.Validate(releaseSetManifest)
		if err != nil {
			return bosherr.WrapError(err, "Validating release set manifest")
		}

		err = c.installationValidator.Validate(installationManifest, releaseSetManifest)
		if err != nil {
			return bosherr.WrapError(err, "Validating installation manifest")
		}
		cpiReleaseName := installationManifest.Template.Release

		var found bool
		cpiRelease, found = releaseSetManifest.FindByName(cpiReleaseName)
		if !found {
			return bosherr.Errorf("installation release '%s' must refer to a release in releases", cpiReleaseName)
		}

		return nil
	})
	return installationManifest, cpiRelease, err
}
<|endoftext|>