<commit_before>/*
Copyright 2019 The Kubernetes Authors.

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

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

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

package mockautoscaling

import (
	"fmt"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/request"
	"github.com/aws/aws-sdk-go/service/autoscaling"
	"k8s.io/klog/v2"
)

func (m *MockAutoscaling) AttachInstances(input *autoscaling.AttachInstancesInput) (*autoscaling.AttachInstancesOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock AttachInstances %v", input)

	g := m.Groups[aws.StringValue(input.AutoScalingGroupName)]
	if g == nil {
		return nil, fmt.Errorf("AutoScaling Group not found")
	}

	for _, instanceID := range input.InstanceIds {
		g.Instances = append(g.Instances, &autoscaling.Instance{InstanceId: instanceID})
	}

	return &autoscaling.AttachInstancesOutput{}, nil
}

func (m *MockAutoscaling) CreateAutoScalingGroup(input *autoscaling.CreateAutoScalingGroupInput) (*autoscaling.CreateAutoScalingGroupOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock CreateAutoScalingGroup %v", input)

	createdTime := time.Now().UTC()

	g := &autoscaling.Group{
		AutoScalingGroupName: input.AutoScalingGroupName,
		AvailabilityZones:    input.AvailabilityZones,
		CreatedTime:          &createdTime,
		DefaultCooldown:      input.DefaultCooldown,
		DesiredCapacity:      input.DesiredCapacity,
		// EnabledMetrics:          input.EnabledMetrics,
		HealthCheckGracePeriod:           input.HealthCheckGracePeriod,
		HealthCheckType:                  input.HealthCheckType,
		Instances:                        []*autoscaling.Instance{},
		LaunchConfigurationName:          input.LaunchConfigurationName,
		LaunchTemplate:                   input.LaunchTemplate,
		LoadBalancerNames:                input.LoadBalancerNames,
		MaxSize:                          input.MaxSize,
		MinSize:                          input.MinSize,
		NewInstancesProtectedFromScaleIn: input.NewInstancesProtectedFromScaleIn,
		PlacementGroup:                   input.PlacementGroup,
		// Status:                           input.Status,
		SuspendedProcesses:  make([]*autoscaling.SuspendedProcess, 0),
		TargetGroupARNs:     input.TargetGroupARNs,
		TerminationPolicies: input.TerminationPolicies,
		VPCZoneIdentifier:   input.VPCZoneIdentifier,
		MaxInstanceLifetime: input.MaxInstanceLifetime,
	}

	if input.LaunchTemplate != nil {
		g.LaunchTemplate.LaunchTemplateName = input.AutoScalingGroupName
		if g.LaunchTemplate.LaunchTemplateId == nil {
			return nil, fmt.Errorf("AutoScalingGroup has LaunchTemplate without ID")
		}
	}

	for _, tag := range input.Tags {
		g.Tags = append(g.Tags, &autoscaling.TagDescription{
			Key:               tag.Key,
			PropagateAtLaunch: tag.PropagateAtLaunch,
			ResourceId:        tag.ResourceId,
			ResourceType:      tag.ResourceType,
			Value:             tag.Value,
		})
	}

	if m.Groups == nil {
		m.Groups = make(map[string]*autoscaling.Group)
	}
	m.Groups[*g.AutoScalingGroupName] = g

	return &autoscaling.CreateAutoScalingGroupOutput{}, nil
}

func (m *MockAutoscaling) UpdateAutoScalingGroup(request *autoscaling.UpdateAutoScalingGroupInput) (*autoscaling.UpdateAutoScalingGroupOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	klog.V(2).Infof("Mock UpdateAutoScalingGroup %v", request)

	if _, ok := m.Groups[*request.AutoScalingGroupName]; !ok {
		return nil, fmt.Errorf("Autoscaling group not found: %v", *request.AutoScalingGroupName)
	}
	group := m.Groups[*request.AutoScalingGroupName]

	if request.AvailabilityZones != nil {
		group.AvailabilityZones = request.AvailabilityZones
	}
	if request.CapacityRebalance != nil {
		group.CapacityRebalance = request.CapacityRebalance
	}
	if request.DesiredCapacity != nil {
		group.DesiredCapacity = request.DesiredCapacity
	}
	if request.HealthCheckGracePeriod != nil {
		group.HealthCheckGracePeriod = request.HealthCheckGracePeriod
	}
	if request.HealthCheckType != nil {
		group.HealthCheckType = request.HealthCheckType
	}
	if request.LaunchConfigurationName != nil {
		group.LaunchConfigurationName = request.LaunchConfigurationName
	}
	if request.LaunchTemplate != nil {
		group.LaunchTemplate = request.LaunchTemplate
	}
	if request.MaxInstanceLifetime != nil {
		group.MaxInstanceLifetime = request.MaxInstanceLifetime
	}
	if request.MaxSize != nil {
		group.MaxSize = request.MaxSize
	}
	if request.MinSize != nil {
		group.MinSize = request.MinSize
	}
	if request.MixedInstancesPolicy != nil {
		group.MixedInstancesPolicy = request.MixedInstancesPolicy
	}
	if request.NewInstancesProtectedFromScaleIn != nil {
		group.NewInstancesProtectedFromScaleIn = request.NewInstancesProtectedFromScaleIn
	}
	if request.PlacementGroup != nil {
		group.PlacementGroup = request.PlacementGroup
	}
	if request.ServiceLinkedRoleARN != nil {
		group.ServiceLinkedRoleARN = request.ServiceLinkedRoleARN
	}
	if request.TerminationPolicies != nil {
		group.TerminationPolicies = request.TerminationPolicies
	}
	if request.VPCZoneIdentifier != nil {
		group.VPCZoneIdentifier = request.VPCZoneIdentifier
	}
	return &autoscaling.UpdateAutoScalingGroupOutput{}, nil
}

func (m *MockAutoscaling) EnableMetricsCollection(request *autoscaling.EnableMetricsCollectionInput) (*autoscaling.EnableMetricsCollectionOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock EnableMetricsCollection: %v", request)

	g := m.Groups[*request.AutoScalingGroupName]
	if g == nil {
		return nil, fmt.Errorf("AutoScalingGroup not found")
	}

	metrics := make(map[string]*autoscaling.EnabledMetric)
	for _, m := range g.EnabledMetrics {
		metrics[*m.Metric] = m
	}
	for _, m := range request.Metrics {
		metrics[*m] = &autoscaling.EnabledMetric{
			Metric:      m,
			Granularity: request.Granularity,
		}
	}

	g.EnabledMetrics = nil
	for _, m := range metrics {
		g.EnabledMetrics = append(g.EnabledMetrics, m)
	}

	response := &autoscaling.EnableMetricsCollectionOutput{}

	return response, nil
}

func (m *MockAutoscaling) SuspendProcesses(input *autoscaling.ScalingProcessQuery) (*autoscaling.SuspendProcessesOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock SuspendProcesses: %v", input)

	g := m.Groups[*input.AutoScalingGroupName]
	if g == nil {
		return nil, fmt.Errorf("AutoScalingGroup not found")
	}

	for _, p := range input.ScalingProcesses {
		found := false
		for _, asgProc := range g.SuspendedProcesses {
			if aws.StringValue(asgProc.ProcessName) == aws.StringValue(p) {
				found = true
			}
		}
		if !found {
			g.SuspendedProcesses = append(g.SuspendedProcesses, &autoscaling.SuspendedProcess{
				ProcessName: p,
			})
		}
	}

	return &autoscaling.SuspendProcessesOutput{}, nil
}

func (m *MockAutoscaling) DescribeAutoScalingGroups(input *autoscaling.DescribeAutoScalingGroupsInput) (*autoscaling.DescribeAutoScalingGroupsOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock DescribeAutoScalingGroups: %v", input)

	groups := []*autoscaling.Group{}
	for _, group := range m.Groups {
		match := false

		if len(input.AutoScalingGroupNames) > 0 {
			for _, inputGroupName := range input.AutoScalingGroupNames {
				if aws.StringValue(group.AutoScalingGroupName) == aws.StringValue(inputGroupName) {
					match = true
				}
			}
		} else {
			match = true
		}

		if match {
			groups = append(groups, group)
		}
	}

	return &autoscaling.DescribeAutoScalingGroupsOutput{
		AutoScalingGroups: groups,
	}, nil
}

func (m *MockAutoscaling) TerminateInstanceInAutoScalingGroup(input *autoscaling.TerminateInstanceInAutoScalingGroupInput) (*autoscaling.TerminateInstanceInAutoScalingGroupOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	for _, group := range m.Groups {
		for i := range group.Instances {
			if aws.StringValue(group.Instances[i].InstanceId) == aws.StringValue(input.InstanceId) {
				group.Instances = append(group.Instances[:i], group.Instances[i+1:]...)
				return &autoscaling.TerminateInstanceInAutoScalingGroupOutput{
					Activity: nil, // TODO
				}, nil
			}
		}
		wp := m.WarmPoolInstances[*group.AutoScalingGroupName]
		for i := range wp {
			if aws.StringValue(wp[i].InstanceId) == aws.StringValue(input.InstanceId) {
				m.WarmPoolInstances[*group.AutoScalingGroupName] = append(wp[:i], wp[i+1:]...)
				return &autoscaling.TerminateInstanceInAutoScalingGroupOutput{
					Activity: nil, // TODO
				}, nil
			}
		}
	}

	return nil, fmt.Errorf("Instance not found")
}

func (m *MockAutoscaling) DescribeAutoScalingGroupsWithContext(aws.Context, *autoscaling.DescribeAutoScalingGroupsInput, ...request.Option) (*autoscaling.DescribeAutoScalingGroupsOutput, error) {
	klog.Fatalf("Not implemented")
	return nil, nil
}

func (m *MockAutoscaling) DescribeAutoScalingGroupsRequest(*autoscaling.DescribeAutoScalingGroupsInput) (*request.Request, *autoscaling.DescribeAutoScalingGroupsOutput) {
	klog.Fatalf("Not implemented")
	return nil, nil
}

func (m *MockAutoscaling) DescribeAutoScalingGroupsPages(request *autoscaling.DescribeAutoScalingGroupsInput, callback func(*autoscaling.DescribeAutoScalingGroupsOutput, bool) bool) error {
	if request.MaxRecords != nil {
		klog.Fatalf("MaxRecords not implemented")
	}
	if request.NextToken != nil {
		klog.Fatalf("NextToken not implemented")
	}

	// For the mock, we just send everything in one page
	page, err := m.DescribeAutoScalingGroups(request)
	if err != nil {
		return err
	}

	callback(page, false)

	return nil
}

func (m *MockAutoscaling) DescribeAutoScalingGroupsPagesWithContext(aws.Context, *autoscaling.DescribeAutoScalingGroupsInput, func(*autoscaling.DescribeAutoScalingGroupsOutput, bool) bool, ...request.Option) error {
	klog.Fatalf("Not implemented")
	return nil
}

func (m *MockAutoscaling) DeleteAutoScalingGroup(request *autoscaling.DeleteAutoScalingGroupInput) (*autoscaling.DeleteAutoScalingGroupOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock DeleteAutoScalingGroup: %v", request)

	id := aws.StringValue(request.AutoScalingGroupName)
	o := m.Groups[id]
	if o == nil {
		return nil, fmt.Errorf("AutoScalingGroup %q not found", id)
	}
	delete(m.Groups, id)

	return &autoscaling.DeleteAutoScalingGroupOutput{}, nil
}

func (m *MockAutoscaling) DeleteAutoScalingGroupWithContext(aws.Context, *autoscaling.DeleteAutoScalingGroupInput, ...request.Option) (*autoscaling.DeleteAutoScalingGroupOutput, error) {
	klog.Fatalf("Not implemented")
	return nil, nil
}

func (m *MockAutoscaling) DeleteAutoScalingGroupRequest(*autoscaling.DeleteAutoScalingGroupInput) (*request.Request, *autoscaling.DeleteAutoScalingGroupOutput) {
	klog.Fatalf("Not implemented")
	return nil, nil
}

func (m *MockAutoscaling) PutLifecycleHook(input *autoscaling.PutLifecycleHookInput) (*autoscaling.PutLifecycleHookOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	hook := &autoscaling.LifecycleHook{
		AutoScalingGroupName:  input.AutoScalingGroupName,
		DefaultResult:         input.DefaultResult,
		GlobalTimeout:         input.HeartbeatTimeout,
		HeartbeatTimeout:      input.HeartbeatTimeout,
		LifecycleHookName:     input.LifecycleHookName,
		LifecycleTransition:   input.LifecycleTransition,
		NotificationMetadata:  input.NotificationMetadata,
		NotificationTargetARN: input.NotificationTargetARN,
		RoleARN:               input.RoleARN,
	}

	if m.LifecycleHooks == nil {
		m.LifecycleHooks = make(map[string]*autoscaling.LifecycleHook)
	}
	m.LifecycleHooks[*hook.AutoScalingGroupName] = hook

	return &autoscaling.PutLifecycleHookOutput{}, nil
}

func (m *MockAutoscaling) DescribeLifecycleHooks(input *autoscaling.DescribeLifecycleHooksInput) (*autoscaling.DescribeLifecycleHooksOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	name := *input.AutoScalingGroupName
	response := &autoscaling.DescribeLifecycleHooksOutput{}

	hook := m.LifecycleHooks[name]
	if hook == nil {
		return response, nil
	}
	response.LifecycleHooks = []*autoscaling.LifecycleHook{hook}
	return response, nil
}
<commit_msg>Support multiple lifecycle hooks for the same ASG in our mocks<commit_after>/*
Copyright 2019 The Kubernetes Authors.

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

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

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

package mockautoscaling

import (
	"fmt"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/request"
	"github.com/aws/aws-sdk-go/service/autoscaling"
	"k8s.io/klog/v2"
)

func (m *MockAutoscaling) AttachInstances(input *autoscaling.AttachInstancesInput) (*autoscaling.AttachInstancesOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock AttachInstances %v", input)

	g := m.Groups[aws.StringValue(input.AutoScalingGroupName)]
	if g == nil {
		return nil, fmt.Errorf("AutoScaling Group not found")
	}

	for _, instanceID := range input.InstanceIds {
		g.Instances = append(g.Instances, &autoscaling.Instance{InstanceId: instanceID})
	}

	return &autoscaling.AttachInstancesOutput{}, nil
}

func (m *MockAutoscaling) CreateAutoScalingGroup(input *autoscaling.CreateAutoScalingGroupInput) (*autoscaling.CreateAutoScalingGroupOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock CreateAutoScalingGroup %v", input)

	createdTime := time.Now().UTC()

	g := &autoscaling.Group{
		AutoScalingGroupName: input.AutoScalingGroupName,
		AvailabilityZones:    input.AvailabilityZones,
		CreatedTime:          &createdTime,
		DefaultCooldown:      input.DefaultCooldown,
		DesiredCapacity:      input.DesiredCapacity,
		// EnabledMetrics:          input.EnabledMetrics,
		HealthCheckGracePeriod:           input.HealthCheckGracePeriod,
		HealthCheckType:                  input.HealthCheckType,
		Instances:                        []*autoscaling.Instance{},
		LaunchConfigurationName:          input.LaunchConfigurationName,
		LaunchTemplate:                   input.LaunchTemplate,
		LoadBalancerNames:                input.LoadBalancerNames,
		MaxSize:                          input.MaxSize,
		MinSize:                          input.MinSize,
		NewInstancesProtectedFromScaleIn: input.NewInstancesProtectedFromScaleIn,
		PlacementGroup:                   input.PlacementGroup,
		// Status:                           input.Status,
		SuspendedProcesses:  make([]*autoscaling.SuspendedProcess, 0),
		TargetGroupARNs:     input.TargetGroupARNs,
		TerminationPolicies: input.TerminationPolicies,
		VPCZoneIdentifier:   input.VPCZoneIdentifier,
		MaxInstanceLifetime: input.MaxInstanceLifetime,
	}

	if input.LaunchTemplate != nil {
		g.LaunchTemplate.LaunchTemplateName = input.AutoScalingGroupName
		if g.LaunchTemplate.LaunchTemplateId == nil {
			return nil, fmt.Errorf("AutoScalingGroup has LaunchTemplate without ID")
		}
	}

	for _, tag := range input.Tags {
		g.Tags = append(g.Tags, &autoscaling.TagDescription{
			Key:               tag.Key,
			PropagateAtLaunch: tag.PropagateAtLaunch,
			ResourceId:        tag.ResourceId,
			ResourceType:      tag.ResourceType,
			Value:             tag.Value,
		})
	}

	if m.Groups == nil {
		m.Groups = make(map[string]*autoscaling.Group)
	}
	m.Groups[*g.AutoScalingGroupName] = g

	return &autoscaling.CreateAutoScalingGroupOutput{}, nil
}

func (m *MockAutoscaling) UpdateAutoScalingGroup(request *autoscaling.UpdateAutoScalingGroupInput) (*autoscaling.UpdateAutoScalingGroupOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	klog.V(2).Infof("Mock UpdateAutoScalingGroup %v", request)

	if _, ok := m.Groups[*request.AutoScalingGroupName]; !ok {
		return nil, fmt.Errorf("Autoscaling group not found: %v", *request.AutoScalingGroupName)
	}
	group := m.Groups[*request.AutoScalingGroupName]

	if request.AvailabilityZones != nil {
		group.AvailabilityZones = request.AvailabilityZones
	}
	if request.CapacityRebalance != nil {
		group.CapacityRebalance = request.CapacityRebalance
	}
	if request.DesiredCapacity != nil {
		group.DesiredCapacity = request.DesiredCapacity
	}
	if request.HealthCheckGracePeriod != nil {
		group.HealthCheckGracePeriod = request.HealthCheckGracePeriod
	}
	if request.HealthCheckType != nil {
		group.HealthCheckType = request.HealthCheckType
	}
	if request.LaunchConfigurationName != nil {
		group.LaunchConfigurationName = request.LaunchConfigurationName
	}
	if request.LaunchTemplate != nil {
		group.LaunchTemplate = request.LaunchTemplate
	}
	if request.MaxInstanceLifetime != nil {
		group.MaxInstanceLifetime = request.MaxInstanceLifetime
	}
	if request.MaxSize != nil {
		group.MaxSize = request.MaxSize
	}
	if request.MinSize != nil {
		group.MinSize = request.MinSize
	}
	if request.MixedInstancesPolicy != nil {
		group.MixedInstancesPolicy = request.MixedInstancesPolicy
	}
	if request.NewInstancesProtectedFromScaleIn != nil {
		group.NewInstancesProtectedFromScaleIn = request.NewInstancesProtectedFromScaleIn
	}
	if request.PlacementGroup != nil {
		group.PlacementGroup = request.PlacementGroup
	}
	if request.ServiceLinkedRoleARN != nil {
		group.ServiceLinkedRoleARN = request.ServiceLinkedRoleARN
	}
	if request.TerminationPolicies != nil {
		group.TerminationPolicies = request.TerminationPolicies
	}
	if request.VPCZoneIdentifier != nil {
		group.VPCZoneIdentifier = request.VPCZoneIdentifier
	}
	return &autoscaling.UpdateAutoScalingGroupOutput{}, nil
}

func (m *MockAutoscaling) EnableMetricsCollection(request *autoscaling.EnableMetricsCollectionInput) (*autoscaling.EnableMetricsCollectionOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock EnableMetricsCollection: %v", request)

	g := m.Groups[*request.AutoScalingGroupName]
	if g == nil {
		return nil, fmt.Errorf("AutoScalingGroup not found")
	}

	metrics := make(map[string]*autoscaling.EnabledMetric)
	for _, m := range g.EnabledMetrics {
		metrics[*m.Metric] = m
	}
	for _, m := range request.Metrics {
		metrics[*m] = &autoscaling.EnabledMetric{
			Metric:      m,
			Granularity: request.Granularity,
		}
	}

	g.EnabledMetrics = nil
	for _, m := range metrics {
		g.EnabledMetrics = append(g.EnabledMetrics, m)
	}

	response := &autoscaling.EnableMetricsCollectionOutput{}

	return response, nil
}

func (m *MockAutoscaling) SuspendProcesses(input *autoscaling.ScalingProcessQuery) (*autoscaling.SuspendProcessesOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock SuspendProcesses: %v", input)

	g := m.Groups[*input.AutoScalingGroupName]
	if g == nil {
		return nil, fmt.Errorf("AutoScalingGroup not found")
	}

	for _, p := range input.ScalingProcesses {
		found := false
		for _, asgProc := range g.SuspendedProcesses {
			if aws.StringValue(asgProc.ProcessName) == aws.StringValue(p) {
				found = true
			}
		}
		if !found {
			g.SuspendedProcesses = append(g.SuspendedProcesses, &autoscaling.SuspendedProcess{
				ProcessName: p,
			})
		}
	}

	return &autoscaling.SuspendProcessesOutput{}, nil
}

func (m *MockAutoscaling) DescribeAutoScalingGroups(input *autoscaling.DescribeAutoScalingGroupsInput) (*autoscaling.DescribeAutoScalingGroupsOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock DescribeAutoScalingGroups: %v", input)

	groups := []*autoscaling.Group{}
	for _, group := range m.Groups {
		match := false

		if len(input.AutoScalingGroupNames) > 0 {
			for _, inputGroupName := range input.AutoScalingGroupNames {
				if aws.StringValue(group.AutoScalingGroupName) == aws.StringValue(inputGroupName) {
					match = true
				}
			}
		} else {
			match = true
		}

		if match {
			groups = append(groups, group)
		}
	}

	return &autoscaling.DescribeAutoScalingGroupsOutput{
		AutoScalingGroups: groups,
	}, nil
}

func (m *MockAutoscaling) TerminateInstanceInAutoScalingGroup(input *autoscaling.TerminateInstanceInAutoScalingGroupInput) (*autoscaling.TerminateInstanceInAutoScalingGroupOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	for _, group := range m.Groups {
		for i := range group.Instances {
			if aws.StringValue(group.Instances[i].InstanceId) == aws.StringValue(input.InstanceId) {
				group.Instances = append(group.Instances[:i], group.Instances[i+1:]...)
				return &autoscaling.TerminateInstanceInAutoScalingGroupOutput{
					Activity: nil, // TODO
				}, nil
			}
		}
		wp := m.WarmPoolInstances[*group.AutoScalingGroupName]
		for i := range wp {
			if aws.StringValue(wp[i].InstanceId) == aws.StringValue(input.InstanceId) {
				m.WarmPoolInstances[*group.AutoScalingGroupName] = append(wp[:i], wp[i+1:]...)
				return &autoscaling.TerminateInstanceInAutoScalingGroupOutput{
					Activity: nil, // TODO
				}, nil
			}
		}
	}

	return nil, fmt.Errorf("Instance not found")
}

func (m *MockAutoscaling) DescribeAutoScalingGroupsWithContext(aws.Context, *autoscaling.DescribeAutoScalingGroupsInput, ...request.Option) (*autoscaling.DescribeAutoScalingGroupsOutput, error) {
	klog.Fatalf("Not implemented")
	return nil, nil
}

func (m *MockAutoscaling) DescribeAutoScalingGroupsRequest(*autoscaling.DescribeAutoScalingGroupsInput) (*request.Request, *autoscaling.DescribeAutoScalingGroupsOutput) {
	klog.Fatalf("Not implemented")
	return nil, nil
}

func (m *MockAutoscaling) DescribeAutoScalingGroupsPages(request *autoscaling.DescribeAutoScalingGroupsInput, callback func(*autoscaling.DescribeAutoScalingGroupsOutput, bool) bool) error {
	if request.MaxRecords != nil {
		klog.Fatalf("MaxRecords not implemented")
	}
	if request.NextToken != nil {
		klog.Fatalf("NextToken not implemented")
	}

	// For the mock, we just send everything in one page
	page, err := m.DescribeAutoScalingGroups(request)
	if err != nil {
		return err
	}

	callback(page, false)

	return nil
}

func (m *MockAutoscaling) DescribeAutoScalingGroupsPagesWithContext(aws.Context, *autoscaling.DescribeAutoScalingGroupsInput, func(*autoscaling.DescribeAutoScalingGroupsOutput, bool) bool, ...request.Option) error {
	klog.Fatalf("Not implemented")
	return nil
}

func (m *MockAutoscaling) DeleteAutoScalingGroup(request *autoscaling.DeleteAutoScalingGroupInput) (*autoscaling.DeleteAutoScalingGroupOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	klog.V(2).Infof("Mock DeleteAutoScalingGroup: %v", request)

	id := aws.StringValue(request.AutoScalingGroupName)
	o := m.Groups[id]
	if o == nil {
		return nil, fmt.Errorf("AutoScalingGroup %q not found", id)
	}
	delete(m.Groups, id)

	return &autoscaling.DeleteAutoScalingGroupOutput{}, nil
}

func (m *MockAutoscaling) DeleteAutoScalingGroupWithContext(aws.Context, *autoscaling.DeleteAutoScalingGroupInput, ...request.Option) (*autoscaling.DeleteAutoScalingGroupOutput, error) {
	klog.Fatalf("Not implemented")
	return nil, nil
}

func (m *MockAutoscaling) DeleteAutoScalingGroupRequest(*autoscaling.DeleteAutoScalingGroupInput) (*request.Request, *autoscaling.DeleteAutoScalingGroupOutput) {
	klog.Fatalf("Not implemented")
	return nil, nil
}

func (m *MockAutoscaling) PutLifecycleHook(input *autoscaling.PutLifecycleHookInput) (*autoscaling.PutLifecycleHookOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	hook := &autoscaling.LifecycleHook{
		AutoScalingGroupName:  input.AutoScalingGroupName,
		DefaultResult:         input.DefaultResult,
		GlobalTimeout:         input.HeartbeatTimeout,
		HeartbeatTimeout:      input.HeartbeatTimeout,
		LifecycleHookName:     input.LifecycleHookName,
		LifecycleTransition:   input.LifecycleTransition,
		NotificationMetadata:  input.NotificationMetadata,
		NotificationTargetARN: input.NotificationTargetARN,
		RoleARN:               input.RoleARN,
	}

	if m.LifecycleHooks == nil {
		m.LifecycleHooks = make(map[string]*autoscaling.LifecycleHook)
	}
	name := *input.AutoScalingGroupName + "::" + *input.LifecycleHookName
	m.LifecycleHooks[name] = hook

	return &autoscaling.PutLifecycleHookOutput{}, nil
}

func (m *MockAutoscaling) DescribeLifecycleHooks(input *autoscaling.DescribeLifecycleHooksInput) (*autoscaling.DescribeLifecycleHooksOutput, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	response := &autoscaling.DescribeLifecycleHooksOutput{}
	for _, lifecycleHookName := range input.LifecycleHookNames {
		name := *input.AutoScalingGroupName + "::" + *lifecycleHookName

		hook := m.LifecycleHooks[name]
		if hook != nil {
			response.LifecycleHooks = append(response.LifecycleHooks, hook)
		}
	}
	return response, nil
}
<|endoftext|>


<commit_before>/*
Copyright 2018 The Knative Authors

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

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

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

package v1alpha1

import (
	"context"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	logtesting "github.com/knative/pkg/logging/testing"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"

	"github.com/knative/serving/pkg/apis/config"
	"github.com/knative/serving/pkg/apis/networking"
)

func TestClusterIngressDefaulting(t *testing.T) {
	tests := []struct {
		name string
		in   *ClusterIngress
		want *ClusterIngress
		wc   func(context.Context) context.Context
	}{{
		name: "empty",
		in:   &ClusterIngress{},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "has-visibility",
		in: &ClusterIngress{
			Spec: IngressSpec{
				Visibility: IngressVisibilityClusterLocal,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Visibility: IngressVisibilityClusterLocal,
			},
		},
	}, {
		name: "tls-defaulting",
		in: &ClusterIngress{
			Spec: IngressSpec{
				TLS: []IngressTLS{{
					SecretNamespace: "secret-space",
					SecretName:      "secret-name",
				}},
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				TLS: []IngressTLS{{
					SecretNamespace: "secret-space",
					SecretName:      "secret-name",
					// Default secret keys are filled in.
					ServerCertificate: "tls.crt",
					PrivateKey:        "tls.key",
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "tls-not-defaulting",
		in: &ClusterIngress{
			Spec: IngressSpec{
				TLS: []IngressTLS{{
					SecretNamespace:   "secret-space",
					SecretName:        "secret-name",
					ServerCertificate: "custom.tls.cert",
					PrivateKey:        "custom.tls.key",
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				TLS: []IngressTLS{{
					SecretNamespace: "secret-space",
					SecretName:      "secret-name",
					// Default secret keys are kept intact.
					ServerCertificate: "custom.tls.cert",
					PrivateKey:        "custom.tls.key",
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "split-timeout-retry-defaulting",
		in: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
							}},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is filled in.
								Percent: 100,
							}},
							// Timeout and Retries are filled in.
							Timeout: &metav1.Duration{Duration: defaultMaxRevisionTimeout},
							Retries: &HTTPRetry{
								PerTryTimeout: &metav1.Duration{Duration: defaultMaxRevisionTimeout},
								Attempts:      networking.DefaultRetryCount,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "split-timeout-retry-not-defaulting",
		in: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								Percent: 30,
							}, {
								IngressBackend: IngressBackend{
									ServiceName:      "revision-001",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								Percent: 70,
							}},
							Timeout: &metav1.Duration{Duration: 10 * time.Second},
							Retries: &HTTPRetry{
								PerTryTimeout: &metav1.Duration{Duration: 10 * time.Second},
								Attempts:      2,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is kept intact.
								Percent: 30,
							}, {
								IngressBackend: IngressBackend{
									ServiceName:      "revision-001",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is kept intact.
								Percent: 70,
							}},
							// Timeout and Retries are kept intact.
							Timeout: &metav1.Duration{Duration: 10 * time.Second},
							Retries: &HTTPRetry{
								PerTryTimeout: &metav1.Duration{Duration: 10 * time.Second},
								Attempts:      2,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "perTryTimeout-in-retry-defaulting",
		in: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								Percent: 30,
							}, {
								IngressBackend: IngressBackend{
									ServiceName:      "revision-001",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								Percent: 70,
							}},
							Timeout: &metav1.Duration{Duration: 10 * time.Second},
							Retries: &HTTPRetry{
								Attempts: 2,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is kept intact.
								Percent: 30,
							}, {
								IngressBackend: IngressBackend{
									ServiceName:      "revision-001",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is kept intact.
								Percent: 70,
							}},
							// Timeout and Retries are kept intact.
							Timeout: &metav1.Duration{Duration: 10 * time.Second},
							Retries: &HTTPRetry{
								// PerTryTimeout is filled in.
								PerTryTimeout: &metav1.Duration{Duration: defaultMaxRevisionTimeout},
								Attempts:      2,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "custom max-revision-timeout-seconds",
		in: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
							}},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is filled in.
								Percent: 100,
							}},
							// Timeout and Retries are filled in.
							Timeout: &metav1.Duration{Duration: time.Second * 2000},
							Retries: &HTTPRetry{
								PerTryTimeout: &metav1.Duration{Duration: time.Second * 2000},
								Attempts:      networking.DefaultRetryCount,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		wc: func(ctx context.Context) context.Context {
			s := config.NewStore(logtesting.TestLogger(t))
			s.OnConfigChanged(&corev1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{Name: config.DefaultsConfigName},
				Data:       map[string]string{"max-revision-timeout-seconds": "2000"},
			})
			return s.ToContext(ctx)
		},
	}}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got := test.in
			ctx := context.Background()
			if test.wc != nil {
				ctx = test.wc(ctx)
			}
			got.SetDefaults(ctx)
			if diff := cmp.Diff(test.want, got); diff != "" {
				t.Errorf("SetDefaults (-want, +got) = %v", diff)
			}
		})
	}

}
<commit_msg>Fix config tests to run more than once (#4498)<commit_after>/*
Copyright 2018 The Knative Authors

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

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

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

package v1alpha1

import (
	"context"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	logtesting "github.com/knative/pkg/logging/testing"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"

	"github.com/knative/serving/pkg/apis/config"
	"github.com/knative/serving/pkg/apis/networking"
)

func TestClusterIngressDefaulting(t *testing.T) {
	defer logtesting.ClearAll()
	tests := []struct {
		name string
		in   *ClusterIngress
		want *ClusterIngress
		wc   func(context.Context) context.Context
	}{{
		name: "empty",
		in:   &ClusterIngress{},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "has-visibility",
		in: &ClusterIngress{
			Spec: IngressSpec{
				Visibility: IngressVisibilityClusterLocal,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Visibility: IngressVisibilityClusterLocal,
			},
		},
	}, {
		name: "tls-defaulting",
		in: &ClusterIngress{
			Spec: IngressSpec{
				TLS: []IngressTLS{{
					SecretNamespace: "secret-space",
					SecretName:      "secret-name",
				}},
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				TLS: []IngressTLS{{
					SecretNamespace: "secret-space",
					SecretName:      "secret-name",
					// Default secret keys are filled in.
					ServerCertificate: "tls.crt",
					PrivateKey:        "tls.key",
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "tls-not-defaulting",
		in: &ClusterIngress{
			Spec: IngressSpec{
				TLS: []IngressTLS{{
					SecretNamespace:   "secret-space",
					SecretName:        "secret-name",
					ServerCertificate: "custom.tls.cert",
					PrivateKey:        "custom.tls.key",
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				TLS: []IngressTLS{{
					SecretNamespace: "secret-space",
					SecretName:      "secret-name",
					// Default secret keys are kept intact.
					ServerCertificate: "custom.tls.cert",
					PrivateKey:        "custom.tls.key",
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "split-timeout-retry-defaulting",
		in: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
							}},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is filled in.
								Percent: 100,
							}},
							// Timeout and Retries are filled in.
							Timeout: &metav1.Duration{Duration: defaultMaxRevisionTimeout},
							Retries: &HTTPRetry{
								PerTryTimeout: &metav1.Duration{Duration: defaultMaxRevisionTimeout},
								Attempts:      networking.DefaultRetryCount,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "split-timeout-retry-not-defaulting",
		in: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								Percent: 30,
							}, {
								IngressBackend: IngressBackend{
									ServiceName:      "revision-001",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								Percent: 70,
							}},
							Timeout: &metav1.Duration{Duration: 10 * time.Second},
							Retries: &HTTPRetry{
								PerTryTimeout: &metav1.Duration{Duration: 10 * time.Second},
								Attempts:      2,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is kept intact.
								Percent: 30,
							}, {
								IngressBackend: IngressBackend{
									ServiceName:      "revision-001",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is kept intact.
								Percent: 70,
							}},
							// Timeout and Retries are kept intact.
							Timeout: &metav1.Duration{Duration: 10 * time.Second},
							Retries: &HTTPRetry{
								PerTryTimeout: &metav1.Duration{Duration: 10 * time.Second},
								Attempts:      2,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "perTryTimeout-in-retry-defaulting",
		in: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								Percent: 30,
							}, {
								IngressBackend: IngressBackend{
									ServiceName:      "revision-001",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								Percent: 70,
							}},
							Timeout: &metav1.Duration{Duration: 10 * time.Second},
							Retries: &HTTPRetry{
								Attempts: 2,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is kept intact.
								Percent: 30,
							}, {
								IngressBackend: IngressBackend{
									ServiceName:      "revision-001",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is kept intact.
								Percent: 70,
							}},
							// Timeout and Retries are kept intact.
							Timeout: &metav1.Duration{Duration: 10 * time.Second},
							Retries: &HTTPRetry{
								// PerTryTimeout is filled in.
								PerTryTimeout: &metav1.Duration{Duration: defaultMaxRevisionTimeout},
								Attempts:      2,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
	}, {
		name: "custom max-revision-timeout-seconds",
		in: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
							}},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		want: &ClusterIngress{
			Spec: IngressSpec{
				Rules: []IngressRule{{
					HTTP: &HTTPIngressRuleValue{
						Paths: []HTTPIngressPath{{
							Splits: []IngressBackendSplit{{
								IngressBackend: IngressBackend{
									ServiceName:      "revision-000",
									ServiceNamespace: "default",
									ServicePort:      intstr.FromInt(8080),
								},
								// Percent is filled in.
								Percent: 100,
							}},
							// Timeout and Retries are filled in.
							Timeout: &metav1.Duration{Duration: time.Second * 2000},
							Retries: &HTTPRetry{
								PerTryTimeout: &metav1.Duration{Duration: time.Second * 2000},
								Attempts:      networking.DefaultRetryCount,
							},
						}},
					},
				}},
				Visibility: IngressVisibilityExternalIP,
			},
		},
		wc: func(ctx context.Context) context.Context {
			s := config.NewStore(logtesting.TestLogger(t))
			s.OnConfigChanged(&corev1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{Name: config.DefaultsConfigName},
				Data:       map[string]string{"max-revision-timeout-seconds": "2000"},
			})
			return s.ToContext(ctx)
		},
	}}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got := test.in
			ctx := context.Background()
			if test.wc != nil {
				ctx = test.wc(ctx)
			}
			got.SetDefaults(ctx)
			if diff := cmp.Diff(test.want, got); diff != "" {
				t.Errorf("SetDefaults (-want, +got) = %v", diff)
			}
		})
	}

}
<|endoftext|>


<commit_before>package auth

import (
	"reflect"

	"github.com/pkg/errors"
	"github.com/rancher/rancher/pkg/namespace"
	"github.com/rancher/types/apis/management.cattle.io/v3"
	rbacv1 "github.com/rancher/types/apis/rbac.authorization.k8s.io/v1"
	"github.com/rancher/types/config"
	"github.com/sirupsen/logrus"

	"k8s.io/api/rbac/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
)

var (
	globalRoleBindingLabel = map[string]string{"authz.management.cattle.io/globalrolebinding": "true"}
	crbNameAnnotation      = "authz.management.cattle.io/crb-name"
	crbNamePrefix          = "cattle-globalrolebinding-"
	grbController          = "mgmt-auth-grb-controller"
)

const (
	globalCatalogRole                  = "global-catalog"
	globalCatalogRoleBinding           = "global-catalog-binding"
	templateResourceRule               = "templates"
	templateVersionResourceRule        = "templateversions"
	catalogTemplateResourceRule        = "catalogtemplates"
	catalogTemplateVersionResourceRule = "catalogtemplateversions"
)

func newGlobalRoleBindingLifecycle(management *config.ManagementContext) *globalRoleBindingLifecycle {
	return &globalRoleBindingLifecycle{
		crbLister:         management.RBAC.ClusterRoleBindings("").Controller().Lister(),
		crbClient:         management.RBAC.ClusterRoleBindings(""),
		grLister:          management.Management.GlobalRoles("").Controller().Lister(),
		roles:             management.RBAC.Roles(""),
		roleLister:        management.RBAC.Roles("").Controller().Lister(),
		roleBindings:      management.RBAC.RoleBindings(""),
		roleBindingLister: management.RBAC.RoleBindings("").Controller().Lister(),
	}
}

type globalRoleBindingLifecycle struct {
	crbLister         rbacv1.ClusterRoleBindingLister
	grLister          v3.GlobalRoleLister
	crbClient         rbacv1.ClusterRoleBindingInterface
	roles             rbacv1.RoleInterface
	roleLister        rbacv1.RoleLister
	roleBindings      rbacv1.RoleBindingInterface
	roleBindingLister rbacv1.RoleBindingLister
}

func (grb *globalRoleBindingLifecycle) Create(obj *v3.GlobalRoleBinding) (runtime.Object, error) {
	err := grb.reconcileGlobalRoleBinding(obj)
	return obj, err
}

func (grb *globalRoleBindingLifecycle) Updated(obj *v3.GlobalRoleBinding) (runtime.Object, error) {
	err := grb.reconcileGlobalRoleBinding(obj)
	return nil, err
}

func (grb *globalRoleBindingLifecycle) Remove(obj *v3.GlobalRoleBinding) (runtime.Object, error) {
	// Don't need to delete the created ClusterRole because owner reference will take care of that
	return nil, nil
}

func (grb *globalRoleBindingLifecycle) reconcileGlobalRoleBinding(globalRoleBinding *v3.GlobalRoleBinding) error {
	var catalogTemplateRule, catalogTemplateVersionRule *v1.PolicyRule
	crbName, ok := globalRoleBinding.Annotations[crbNameAnnotation]
	if !ok {
		crbName = crbNamePrefix + globalRoleBinding.Name
	}
	subject := v1.Subject{
		Kind:     "User",
		Name:     globalRoleBinding.UserName,
		APIGroup: rbacv1.GroupName,
	}
	crb, _ := grb.crbLister.Get("", crbName)
	if crb != nil {
		subjects := []v1.Subject{subject}
		updateSubject := !reflect.DeepEqual(subjects, crb.Subjects)

		updateRoleRef := false
		var roleRef v1.RoleRef
		gr, _ := grb.grLister.Get("", globalRoleBinding.GlobalRoleName)
		if gr != nil {
			crNameFromGR := getCRName(gr)
			if crNameFromGR != crb.RoleRef.Name {
				updateRoleRef = true
				roleRef = v1.RoleRef{
					Name: crNameFromGR,
					Kind: clusterRoleKind,
				}
			}
		}
		if updateSubject || updateRoleRef {
			crb = crb.DeepCopy()
			if updateRoleRef {
				crb.RoleRef = roleRef
			}
			crb.Subjects = subjects
			logrus.Infof("[%v] Updating clusterRoleBinding %v for globalRoleBinding %v user %v", grbController, crb.Name, globalRoleBinding.Name, globalRoleBinding.UserName)
			if _, err := grb.crbClient.Update(crb); err != nil {
				return errors.Wrapf(err, "couldn't update ClusterRoleBinding %v", crb.Name)
			}
		}
		return nil
	}

	logrus.Infof("Creating new GlobalRoleBinding for GlobalRoleBinding %v", globalRoleBinding.Name)
	gr, _ := grb.grLister.Get("", globalRoleBinding.GlobalRoleName)
	var crName string
	if gr != nil {
		crName = getCRName(gr)
	} else {
		crName = generateCRName(globalRoleBinding.GlobalRoleName)
	}
	logrus.Infof("[%v] Creating clusterRoleBinding for globalRoleBinding %v for user %v with role %v", grbController, globalRoleBinding.Name, globalRoleBinding.UserName, crName)
	_, err := grb.crbClient.Create(&v1.ClusterRoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name: crbName,
			OwnerReferences: []metav1.OwnerReference{
				{
					APIVersion: globalRoleBinding.TypeMeta.APIVersion,
					Kind:       globalRoleBinding.TypeMeta.Kind,
					Name:       globalRoleBinding.Name,
					UID:        globalRoleBinding.UID,
				},
			},
			Labels: globalRoleBindingLabel,
		},
		Subjects: []v1.Subject{subject},
		RoleRef: v1.RoleRef{
			Name: crName,
			Kind: clusterRoleKind,
		},
	})
	if err != nil {
		return err
	}
	// Add an annotation to the globalrole indicating the name we used for future updates
	if globalRoleBinding.Annotations == nil {
		globalRoleBinding.Annotations = map[string]string{}
	}
	globalRoleBinding.Annotations[crbNameAnnotation] = crbName

	// Check if the current globalRole has rules for templates and templateversions
	gr, _ = grb.grLister.Get("", globalRoleBinding.GlobalRoleName)
	if gr != nil {
		for _, rule := range gr.Rules {
			for _, resource := range rule.Resources {
				if resource == templateResourceRule {
					catalogTemplateRule = &v1.PolicyRule{
						APIGroups: rule.APIGroups,
						Resources: []string{catalogTemplateResourceRule},
						Verbs:     rule.Verbs,
					}
				} else if resource == templateVersionResourceRule {
					catalogTemplateVersionRule = &v1.PolicyRule{
						APIGroups: rule.APIGroups,
						Resources: []string{catalogTemplateVersionResourceRule},
						Verbs:     rule.Verbs,
					}
				}
			}
		}
	}
	// If rules for "templates and "templateversions" exists, create a role for the granting access to
	// catalogtemplates and catalogtemplateversions in the global namespace
	var rules []v1.PolicyRule
	if catalogTemplateRule != nil {
		rules = append(rules, *catalogTemplateRule)
	}
	if catalogTemplateVersionRule != nil {
		rules = append(rules, *catalogTemplateVersionRule)
	}
	if len(rules) > 0 {
		_, err = grb.roles.GetNamespaced(namespace.GlobalNamespace, globalCatalogRole, metav1.GetOptions{})
		if err != nil {
			if apierrors.IsNotFound(err) {
				role := &v1.Role{
					ObjectMeta: metav1.ObjectMeta{
						Name:      globalCatalogRole,
						Namespace: namespace.GlobalNamespace,
					},
					Rules: rules,
				}
				_, err = grb.roles.Create(role)
				if err != nil && !apierrors.IsAlreadyExists(err) {
					return err
				}
			} else {
				return err
			}
		}
		// Create a rolebinding, referring the above role, and using globalrole user.Username as the subject
		// Check if rb exists first!
		grbName := globalRoleBinding.UserName + "-" + globalCatalogRoleBinding
		_, err = grb.roleBindings.GetNamespaced(namespace.GlobalNamespace, grbName, metav1.GetOptions{})
		if err != nil {
			if apierrors.IsNotFound(err) {
				rb := &v1.RoleBinding{
					ObjectMeta: metav1.ObjectMeta{
						Name:      grbName,
						Namespace: namespace.GlobalNamespace,
					},
					Subjects: []v1.Subject{subject},
					RoleRef: v1.RoleRef{
						Kind: "Role",
						Name: globalCatalogRole,
					},
				}
				_, err = grb.roleBindings.Create(rb)
				if err != nil && !apierrors.IsAlreadyExists(err) {
					return err
				}
			} else {
				return err
			}
		}
	}
	return nil
}
<commit_msg>Create template/version rules for users created pre-upgrade<commit_after>package auth

import (
	"reflect"

	"github.com/pkg/errors"
	"github.com/rancher/norman/types/slice"
	"github.com/rancher/rancher/pkg/namespace"
	"github.com/rancher/types/apis/management.cattle.io/v3"
	rbacv1 "github.com/rancher/types/apis/rbac.authorization.k8s.io/v1"
	"github.com/rancher/types/config"
	"github.com/sirupsen/logrus"

	"k8s.io/api/rbac/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
)

var (
	globalRoleBindingLabel = map[string]string{"authz.management.cattle.io/globalrolebinding": "true"}
	crbNameAnnotation      = "authz.management.cattle.io/crb-name"
	crbNamePrefix          = "cattle-globalrolebinding-"
	grbController          = "mgmt-auth-grb-controller"
)

const (
	globalCatalogRole                  = "global-catalog"
	globalCatalogRoleBinding           = "global-catalog-binding"
	templateResourceRule               = "templates"
	templateVersionResourceRule        = "templateversions"
	catalogTemplateResourceRule        = "catalogtemplates"
	catalogTemplateVersionResourceRule = "catalogtemplateversions"
)

func newGlobalRoleBindingLifecycle(management *config.ManagementContext) *globalRoleBindingLifecycle {
	return &globalRoleBindingLifecycle{
		crbLister:         management.RBAC.ClusterRoleBindings("").Controller().Lister(),
		crbClient:         management.RBAC.ClusterRoleBindings(""),
		grLister:          management.Management.GlobalRoles("").Controller().Lister(),
		roles:             management.RBAC.Roles(""),
		roleLister:        management.RBAC.Roles("").Controller().Lister(),
		roleBindings:      management.RBAC.RoleBindings(""),
		roleBindingLister: management.RBAC.RoleBindings("").Controller().Lister(),
	}
}

type globalRoleBindingLifecycle struct {
	crbLister         rbacv1.ClusterRoleBindingLister
	grLister          v3.GlobalRoleLister
	crbClient         rbacv1.ClusterRoleBindingInterface
	roles             rbacv1.RoleInterface
	roleLister        rbacv1.RoleLister
	roleBindings      rbacv1.RoleBindingInterface
	roleBindingLister rbacv1.RoleBindingLister
}

func (grb *globalRoleBindingLifecycle) Create(obj *v3.GlobalRoleBinding) (runtime.Object, error) {
	err := grb.reconcileGlobalRoleBinding(obj)
	return obj, err
}

func (grb *globalRoleBindingLifecycle) Updated(obj *v3.GlobalRoleBinding) (runtime.Object, error) {
	err := grb.reconcileGlobalRoleBinding(obj)
	return nil, err
}

func (grb *globalRoleBindingLifecycle) Remove(obj *v3.GlobalRoleBinding) (runtime.Object, error) {
	// Don't need to delete the created ClusterRole because owner reference will take care of that
	return nil, nil
}

func (grb *globalRoleBindingLifecycle) reconcileGlobalRoleBinding(globalRoleBinding *v3.GlobalRoleBinding) error {
	crbName, ok := globalRoleBinding.Annotations[crbNameAnnotation]
	if !ok {
		crbName = crbNamePrefix + globalRoleBinding.Name
	}
	subject := v1.Subject{
		Kind:     "User",
		Name:     globalRoleBinding.UserName,
		APIGroup: rbacv1.GroupName,
	}
	crb, _ := grb.crbLister.Get("", crbName)
	if crb != nil {
		subjects := []v1.Subject{subject}
		updateSubject := !reflect.DeepEqual(subjects, crb.Subjects)

		updateRoleRef := false
		var roleRef v1.RoleRef
		gr, _ := grb.grLister.Get("", globalRoleBinding.GlobalRoleName)
		if gr != nil {
			crNameFromGR := getCRName(gr)
			if crNameFromGR != crb.RoleRef.Name {
				updateRoleRef = true
				roleRef = v1.RoleRef{
					Name: crNameFromGR,
					Kind: clusterRoleKind,
				}
			}
		}
		if updateSubject || updateRoleRef {
			crb = crb.DeepCopy()
			if updateRoleRef {
				crb.RoleRef = roleRef
			}
			crb.Subjects = subjects
			logrus.Infof("[%v] Updating clusterRoleBinding %v for globalRoleBinding %v user %v", grbController, crb.Name, globalRoleBinding.Name, globalRoleBinding.UserName)
			if _, err := grb.crbClient.Update(crb); err != nil {
				return errors.Wrapf(err, "couldn't update ClusterRoleBinding %v", crb.Name)
			}
		}
		return grb.addRulesForTemplateAndTemplateVersions(globalRoleBinding, subject)
	}

	logrus.Infof("Creating new GlobalRoleBinding for GlobalRoleBinding %v", globalRoleBinding.Name)
	gr, _ := grb.grLister.Get("", globalRoleBinding.GlobalRoleName)
	var crName string
	if gr != nil {
		crName = getCRName(gr)
	} else {
		crName = generateCRName(globalRoleBinding.GlobalRoleName)
	}
	logrus.Infof("[%v] Creating clusterRoleBinding for globalRoleBinding %v for user %v with role %v", grbController, globalRoleBinding.Name, globalRoleBinding.UserName, crName)
	_, err := grb.crbClient.Create(&v1.ClusterRoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name: crbName,
			OwnerReferences: []metav1.OwnerReference{
				{
					APIVersion: globalRoleBinding.TypeMeta.APIVersion,
					Kind:       globalRoleBinding.TypeMeta.Kind,
					Name:       globalRoleBinding.Name,
					UID:        globalRoleBinding.UID,
				},
			},
			Labels: globalRoleBindingLabel,
		},
		Subjects: []v1.Subject{subject},
		RoleRef: v1.RoleRef{
			Name: crName,
			Kind: clusterRoleKind,
		},
	})
	if err != nil {
		return err
	}
	// Add an annotation to the globalrole indicating the name we used for future updates
	if globalRoleBinding.Annotations == nil {
		globalRoleBinding.Annotations = map[string]string{}
	}
	globalRoleBinding.Annotations[crbNameAnnotation] = crbName

	return grb.addRulesForTemplateAndTemplateVersions(globalRoleBinding, subject)
}

func (grb *globalRoleBindingLifecycle) addRulesForTemplateAndTemplateVersions(globalRoleBinding *v3.GlobalRoleBinding, subject v1.Subject) error {
	var catalogTemplateRule, catalogTemplateVersionRule *v1.PolicyRule
	// Check if the current globalRole has rules for templates and templateversions
	gr, err := grb.grLister.Get("", globalRoleBinding.GlobalRoleName)
	if err != nil && !apierrors.IsNotFound(err) {
		return err
	}
	if gr != nil {
		for _, rule := range gr.Rules {
			for _, resource := range rule.Resources {
				if resource == templateResourceRule {
					if catalogTemplateRule == nil {
						catalogTemplateRule = &v1.PolicyRule{
							APIGroups: rule.APIGroups,
							Resources: []string{catalogTemplateResourceRule},
							Verbs:     rule.Verbs,
						}
					} else {
						for _, v := range rule.Verbs {
							if !slice.ContainsString(catalogTemplateRule.Verbs, v) {
								catalogTemplateRule.Verbs = append(catalogTemplateRule.Verbs, v)
							}
						}
					}
				} else if resource == templateVersionResourceRule {
					if catalogTemplateVersionRule == nil {
						catalogTemplateVersionRule = &v1.PolicyRule{
							APIGroups: rule.APIGroups,
							Resources: []string{catalogTemplateVersionResourceRule},
							Verbs:     rule.Verbs,
						}
					} else {
						for _, v := range rule.Verbs {
							if !slice.ContainsString(catalogTemplateVersionRule.Verbs, v) {
								catalogTemplateVersionRule.Verbs = append(catalogTemplateVersionRule.Verbs, v)
							}
						}
					}
				}
			}
		}
	}
	// If rules for "templates and "templateversions" exists, create a role for the granting access to
	// catalogtemplates and catalogtemplateversions in the global namespace
	var rules []v1.PolicyRule
	if catalogTemplateRule != nil {
		rules = append(rules, *catalogTemplateRule)
	}
	if catalogTemplateVersionRule != nil {
		rules = append(rules, *catalogTemplateVersionRule)
	}
	if len(rules) > 0 {
		_, err := grb.roles.GetNamespaced(namespace.GlobalNamespace, globalCatalogRole, metav1.GetOptions{})
		if err != nil {
			if apierrors.IsNotFound(err) {
				role := &v1.Role{
					ObjectMeta: metav1.ObjectMeta{
						Name:      globalCatalogRole,
						Namespace: namespace.GlobalNamespace,
					},
					Rules: rules,
				}
				_, err = grb.roles.Create(role)
				if err != nil && !apierrors.IsAlreadyExists(err) {
					return err
				}
			} else {
				return err
			}
		}
		// Create a rolebinding, referring the above role, and using globalrole user.Username as the subject
		// Check if rb exists first!
		grbName := globalRoleBinding.UserName + "-" + globalCatalogRoleBinding
		_, err = grb.roleBindings.GetNamespaced(namespace.GlobalNamespace, grbName, metav1.GetOptions{})
		if err != nil {
			if apierrors.IsNotFound(err) {
				rb := &v1.RoleBinding{
					ObjectMeta: metav1.ObjectMeta{
						Name:      grbName,
						Namespace: namespace.GlobalNamespace,
					},
					Subjects: []v1.Subject{subject},
					RoleRef: v1.RoleRef{
						Kind: "Role",
						Name: globalCatalogRole,
					},
				}
				_, err = grb.roleBindings.Create(rb)
				if err != nil && !apierrors.IsAlreadyExists(err) {
					return err
				}
			} else {
				return err
			}
		}
	}
	return nil
}
<|endoftext|>


<commit_before>package workflowtrigger

import (
	"fmt"
	"strconv"
	"sync"
	"time"

	"github.com/caicloud/cyclone/pkg/apis/cyclone/v1alpha1"
	"github.com/caicloud/cyclone/pkg/k8s/clientset"
	"github.com/caicloud/cyclone/pkg/workflow/common"
	"github.com/pkg/errors"
	"github.com/robfig/cron"
	log "github.com/sirupsen/logrus"
	errors2 "k8s.io/kubernetes/staging/src/k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/storage/names"
)

const (
	KeyTemplate = "%s/%s"

	// time zone offset to UTC, in minutes
	// -480 for Asia/Shanghai(+8)
	ParamTimeZoneOffset = "timeZoneOffset"
	ParamSchedule       = "schedule"
)

type CronTrigger struct {
	Cron                *cron.Cron
	IsRunning           bool
	SuccCount           int
	FailCount           int
	Namespace           string
	WorkflowTriggerName string
	WorkflowRun         *v1alpha1.WorkflowRun
	Manage              *CronTriggerManager
}

type CronTriggerManager struct {
	Client         clientset.Interface
	CronTriggerMap map[string]*CronTrigger
	mutex          sync.Mutex
}

func NewTriggerManager(client clientset.Interface) *CronTriggerManager {
	return &CronTriggerManager{
		Client:         client,
		CronTriggerMap: make(map[string]*CronTrigger),
		mutex:          sync.Mutex{},
	}
}

func (trigger *CronTrigger) getKeyFromTrigger() string {
	return fmt.Sprintf(KeyTemplate, trigger.Namespace, trigger.WorkflowTriggerName)
}

func (m *CronTriggerManager) AddTrigger(trigger *CronTrigger) {
	wftKey := trigger.getKeyFromTrigger()
	m.mutex.Lock()
	defer m.mutex.Unlock()
	if wft, ok := m.CronTriggerMap[wftKey]; ok {
		// this situation may not happen for k8s will do same name check
		log.Warnf("failed to add cronTrigger, already exists: %+v\n", wft)
	} else {
		m.CronTriggerMap[wftKey] = trigger
	}
}

func (m *CronTriggerManager) DeleteTrigger(wftKey string) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	if wft, ok := m.CronTriggerMap[wftKey]; ok {
		wft.Cron.Stop()
		wft.IsRunning = false
		delete(m.CronTriggerMap, wftKey)
	} else {
		log.Warnf("failed to delete cronTrigger(%s), not exist\n", wftKey)
	}
}

func ToWorkflowTrigger(obj interface{}) (*v1alpha1.WorkflowTrigger, error) {

	wft, ok := obj.(*v1alpha1.WorkflowTrigger)
	if !ok {
		return nil, errors.New(fmt.Sprintf("I want type: WorkflowTrigger, but it is: %T", obj))
	} else {
		return wft, nil
	}
}

func (c *CronTrigger) Run() {

	if c.WorkflowRun.Labels == nil {
		c.WorkflowRun.Labels = make(map[string]string)
	}
	c.WorkflowRun.Labels[common.WorkflowRunLabelName] = c.WorkflowRun.Spec.WorkflowRef.Name

	for {
		c.WorkflowRun.Name = names.SimpleNameGenerator.GenerateName(c.WorkflowTriggerName + "-")
		_, err := c.Manage.Client.CycloneV1alpha1().WorkflowRuns(c.Namespace).Create(c.WorkflowRun)
		if err != nil {
			if errors2.IsAlreadyExists(err) {
				continue
			} else {
				c.FailCount++
				log.Warnf("can not create WorkflowRun: %s", err)
				break
			}
		} else {
			c.SuccCount++
			break
		}
	}
}

func getParamValue(items []v1alpha1.ParameterItem, key string) (string, bool) {
	for _, item := range items {
		if item.Name == key {
			return item.Value, true
		}
	}
	return "", false
}

func (m *CronTriggerManager) CreateCron(wft *v1alpha1.WorkflowTrigger) {

	schedule, has := getParamValue(wft.Spec.Parameters, ParamSchedule)
	if !has {
		log.WithField("wft", wft.Name).Warn("Parameter 'schedule' not set in WorkflowTrigger spec")
		return
	}

	timezone, has := getParamValue(wft.Spec.Parameters, ParamTimeZoneOffset)
	if !has {
		timezone = "0"
	}

	minuteOffUTC, err := strconv.Atoi(timezone)
	if err != nil {
		log.Warnf("can not parse timezone(%s) to int", timezone)
		minuteOffUTC = 0
	}

	ct := &CronTrigger{
		Namespace:           wft.Namespace,
		WorkflowTriggerName: wft.Name,
	}

	wfr := &v1alpha1.WorkflowRun{
		Spec: wft.Spec.WorkflowRunSpec,
	}

	ct.WorkflowRun = wfr

	c := cron.NewWithLocation(time.FixedZone("userZone", -1*60*minuteOffUTC))
	err = c.AddJob(schedule, ct)
	if err != nil {
		log.Errorf("can not create Cron job: %s", err)
		return
	}

	ct.Cron = c
	ct.Manage = m
	m.AddTrigger(ct)

	if !wft.Spec.Disabled {
		ct.Cron.Start()
		ct.IsRunning = true
	}
}

func (m *CronTriggerManager) UpdateCron(wft *v1alpha1.WorkflowTrigger) {
	m.DeleteCron(wft)
	m.CreateCron(wft)
}

func (m *CronTriggerManager) DeleteCron(wft *v1alpha1.WorkflowTrigger) {
	wftKey := getKeyFromWorkflowTrigger(wft)
	m.DeleteTrigger(wftKey)
}

func getKeyFromWorkflowTrigger(wft *v1alpha1.WorkflowTrigger) string {
	return fmt.Sprintf(KeyTemplate, wft.Namespace, wft.Name)
}
<commit_msg>fix(trigger): fix import error (#702)<commit_after>package workflowtrigger

import (
	"fmt"
	"strconv"
	"sync"
	"time"

	"github.com/caicloud/cyclone/pkg/apis/cyclone/v1alpha1"
	"github.com/caicloud/cyclone/pkg/k8s/clientset"
	"github.com/caicloud/cyclone/pkg/workflow/common"
	"github.com/pkg/errors"
	"github.com/robfig/cron"
	log "github.com/sirupsen/logrus"
	errors2 "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/util/rand"
)

const (
	KeyTemplate = "%s/%s"

	// time zone offset to UTC, in minutes
	// -480 for Asia/Shanghai(+8)
	ParamTimeZoneOffset = "timeZoneOffset"
	ParamSchedule       = "schedule"
)

type CronTrigger struct {
	Cron                *cron.Cron
	IsRunning           bool
	SuccCount           int
	FailCount           int
	Namespace           string
	WorkflowTriggerName string
	WorkflowRun         *v1alpha1.WorkflowRun
	Manage              *CronTriggerManager
}

type CronTriggerManager struct {
	Client         clientset.Interface
	CronTriggerMap map[string]*CronTrigger
	mutex          sync.Mutex
}

func NewTriggerManager(client clientset.Interface) *CronTriggerManager {
	return &CronTriggerManager{
		Client:         client,
		CronTriggerMap: make(map[string]*CronTrigger),
		mutex:          sync.Mutex{},
	}
}

func (trigger *CronTrigger) getKeyFromTrigger() string {
	return fmt.Sprintf(KeyTemplate, trigger.Namespace, trigger.WorkflowTriggerName)
}

func (m *CronTriggerManager) AddTrigger(trigger *CronTrigger) {
	wftKey := trigger.getKeyFromTrigger()
	m.mutex.Lock()
	defer m.mutex.Unlock()
	if wft, ok := m.CronTriggerMap[wftKey]; ok {
		// this situation may not happen for k8s will do same name check
		log.Warnf("failed to add cronTrigger, already exists: %+v\n", wft)
	} else {
		m.CronTriggerMap[wftKey] = trigger
	}
}

func (m *CronTriggerManager) DeleteTrigger(wftKey string) {
	m.mutex.Lock()
	defer m.mutex.Unlock()
	if wft, ok := m.CronTriggerMap[wftKey]; ok {
		wft.Cron.Stop()
		wft.IsRunning = false
		delete(m.CronTriggerMap, wftKey)
	} else {
		log.Warnf("failed to delete cronTrigger(%s), not exist\n", wftKey)
	}
}

func ToWorkflowTrigger(obj interface{}) (*v1alpha1.WorkflowTrigger, error) {

	wft, ok := obj.(*v1alpha1.WorkflowTrigger)
	if !ok {
		return nil, errors.New(fmt.Sprintf("I want type: WorkflowTrigger, but it is: %T", obj))
	} else {
		return wft, nil
	}
}

func (c *CronTrigger) Run() {

	if c.WorkflowRun.Labels == nil {
		c.WorkflowRun.Labels = make(map[string]string)
	}
	c.WorkflowRun.Labels[common.WorkflowRunLabelName] = c.WorkflowRun.Spec.WorkflowRef.Name

	for {
		c.WorkflowRun.Name = fmt.Sprintf("%s-%s", c.WorkflowTriggerName, rand.String(5))
		_, err := c.Manage.Client.CycloneV1alpha1().WorkflowRuns(c.Namespace).Create(c.WorkflowRun)
		if err != nil {
			if errors2.IsAlreadyExists(err) {
				continue
			} else {
				c.FailCount++
				log.Warnf("can not create WorkflowRun: %s", err)
				break
			}
		} else {
			c.SuccCount++
			break
		}
	}
}

func getParamValue(items []v1alpha1.ParameterItem, key string) (string, bool) {
	for _, item := range items {
		if item.Name == key {
			return item.Value, true
		}
	}
	return "", false
}

func (m *CronTriggerManager) CreateCron(wft *v1alpha1.WorkflowTrigger) {

	schedule, has := getParamValue(wft.Spec.Parameters, ParamSchedule)
	if !has {
		log.WithField("wft", wft.Name).Warn("Parameter 'schedule' not set in WorkflowTrigger spec")
		return
	}

	timezone, has := getParamValue(wft.Spec.Parameters, ParamTimeZoneOffset)
	if !has {
		timezone = "0"
	}

	minuteOffUTC, err := strconv.Atoi(timezone)
	if err != nil {
		log.Warnf("can not parse timezone(%s) to int", timezone)
		minuteOffUTC = 0
	}

	ct := &CronTrigger{
		Namespace:           wft.Namespace,
		WorkflowTriggerName: wft.Name,
	}

	wfr := &v1alpha1.WorkflowRun{
		Spec: wft.Spec.WorkflowRunSpec,
	}

	ct.WorkflowRun = wfr

	c := cron.NewWithLocation(time.FixedZone("userZone", -1*60*minuteOffUTC))
	err = c.AddJob(schedule, ct)
	if err != nil {
		log.Errorf("can not create Cron job: %s", err)
		return
	}

	ct.Cron = c
	ct.Manage = m
	m.AddTrigger(ct)

	if !wft.Spec.Disabled {
		ct.Cron.Start()
		ct.IsRunning = true
	}
}

func (m *CronTriggerManager) UpdateCron(wft *v1alpha1.WorkflowTrigger) {
	m.DeleteCron(wft)
	m.CreateCron(wft)
}

func (m *CronTriggerManager) DeleteCron(wft *v1alpha1.WorkflowTrigger) {
	wftKey := getKeyFromWorkflowTrigger(wft)
	m.DeleteTrigger(wftKey)
}

func getKeyFromWorkflowTrigger(wft *v1alpha1.WorkflowTrigger) string {
	return fmt.Sprintf(KeyTemplate, wft.Namespace, wft.Name)
}
<|endoftext|>


<commit_before>package command

import (
	"github.com/codegangsta/cli"
	"log"

	. "github.com/foostan/fileconsul/fileconsul"
)

var StatusFlags = []cli.Flag{
	cli.StringFlag{
		Name:  "addr",
		Value: "localhost:8500",
		Usage: "consul HTTP API address with port",
	},
	cli.StringFlag{
		Name:  "dc",
		Value: "dc1",
		Usage: "consul datacenter, uses local if blank",
	},
	cli.StringFlag{
		Name:  "prefix",
		Value: "fileconsul",
		Usage: "reading file status from Consul's K/V store with the given prefix",
	},
	cli.StringFlag{
		Name:  "basepath",
		Value: ".",
		Usage: "base directory path of target files",
	},
}

func StatusCommand(c *cli.Context) {
	addr := c.String("addr")
	dc := c.String("dc")
	prefix := c.String("prefix")
	basepath := c.String("basepath")

	client, err := NewClient(&ClientConfig{
		ConsulAddr: addr,
		ConsulDC:   dc,
	})
	if err != nil {
		log.Fatal(err)
	}

	lfList, err := ReadLFList(basepath)
	if err != nil {
		log.Fatal(err)
	}

	rfList, err := client.ReadRFList(prefix)
	if err != nil {
		log.Fatal(err)
	}

	rfDiff := rfList.Diff(lfList.ToRFList())

	for _, remotefile := range rfDiff.Eq {
		println("remote/local:\t" + remotefile.Path)
	}

	for _, remotefile := range rfDiff.Add {
		println("remote:\t" + remotefile.Path)
	}

	for _, remotefile := range rfDiff.Del {
		println("local:\t" + remotefile.Path)
	}

	for _, remotefile := range rfDiff.New {
		println("remote/local:(modified)\t" + remotefile.Path)
	}
}
<commit_msg>Show status message like git<commit_after>package command

import (
	"github.com/codegangsta/cli"
	"log"
	"fmt"
	"path/filepath"

	. "github.com/foostan/fileconsul/fileconsul"
)

var StatusFlags = []cli.Flag{
	cli.StringFlag{
		Name:  "addr",
		Value: "localhost:8500",
		Usage: "consul HTTP API address with port",
	},
	cli.StringFlag{
		Name:  "dc",
		Value: "dc1",
		Usage: "consul datacenter, uses local if blank",
	},
	cli.StringFlag{
		Name:  "prefix",
		Value: "fileconsul",
		Usage: "reading file status from Consul's K/V store with the given prefix",
	},
	cli.StringFlag{
		Name:  "basepath",
		Value: ".",
		Usage: "base directory path of target files",
	},
}

func StatusCommand(c *cli.Context) {
	addr := c.String("addr")
	dc := c.String("dc")
	prefix := c.String("prefix")
	basepath := c.String("basepath")

	client, err := NewClient(&ClientConfig{
		ConsulAddr: addr,
		ConsulDC:   dc,
	})
	if err != nil {
		log.Fatal(err)
	}

	lfList, err := ReadLFList(basepath)
	if err != nil {
		log.Fatal(err)
	}

	rfList, err := client.ReadRFList(prefix)
	if err != nil {
		log.Fatal(err)
	}

	lfrfList := lfList.ToRFList()
	rfDiff := lfrfList.Diff(rfList)

	if !lfrfList.Equal(rfList) {
		fmt.Println("Changes to be pushed:\n  (use \"fileconsul pull [command options]\" to reset all files)")
	}

	for _, remotefile := range rfDiff.Add {
		println("\tnew file:\t" + filepath.Join(basepath, remotefile.Path))
	}

	for _, remotefile := range rfDiff.Del {
		println("\tdeleted:\t" + filepath.Join(basepath, remotefile.Path))
	}

	for _, remotefile := range rfDiff.New {
		println("\tmodified:\t" + filepath.Join(basepath, remotefile.Path))
	}
}
<|endoftext|>


<commit_before>package commands

import (
	"errors"
	"io/ioutil"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/aisk/logp"
	"github.com/leancloud/go-upload"
	"github.com/leancloud/lean-cli/api"
	"github.com/leancloud/lean-cli/api/regions"
	"github.com/leancloud/lean-cli/apps"
	"github.com/leancloud/lean-cli/runtimes"
	"github.com/leancloud/lean-cli/utils"
	"github.com/leancloud/lean-cli/version"
	"github.com/urfave/cli"
)

func deployAction(c *cli.Context) error {
	version.PrintCurrentVersion()
	isDeployFromGit := c.Bool("g")
	isDeployFromJavaWar := c.Bool("war")
	ignoreFilePath := c.String("leanignore")
	noDepsCache := c.Bool("no-cache")
	message := c.String("message")
	keepFile := c.Bool("keep-deploy-file")
	revision := c.String("revision")
	prodString := c.String("prod")

	var prod int

	appID, err := apps.GetCurrentAppID(".")
	if err != nil {
		return err
	}

	groupName, err := apps.GetCurrentGroup(".")
	if err != nil {
		return err
	}

	logp.Info("Retrieving app info ...")

	region, err := apps.GetAppRegion(appID)
	if err != nil {
		return err
	}

	appInfo, err := api.GetAppInfo(appID)
	if err != nil {
		return err
	}

	if prodString == "" {
		groupInfo, err := api.GetGroup(appID, groupName)
		if err != nil {
			return err
		}

		if groupInfo.Staging.Deployable {
			prod = 0
		} else {
			prod = 1
		}
	} else {
		prod, err = strconv.Atoi(prodString)
		if err != nil {
			return err
		}
	}

	if prod == 1 {
		logp.Infof("Preparing to deploy %s(%s) to region: %s group: %s production\r\n", appInfo.AppName, appID, region, groupName)
	} else {
		logp.Infof("Preparing to deploy %s(%s) to region: %s group: %s staging\r\n", appInfo.AppName, appID, region, groupName)
	}

	opts := &api.DeployOptions{
		NoDepsCache: noDepsCache,
		Options:     c.String("options"),
	}

	if isDeployFromGit {
		err = deployFromGit(appID, groupName, prod, revision, opts)
		if err != nil {
			return err
		}
	} else {
		opts.Message = getCommentMessage(message)

		err = deployFromLocal(appID, groupName, prod, isDeployFromJavaWar, ignoreFilePath, keepFile, opts)
		if err != nil {
			return err
		}
	}
	return nil
}

func uploadProject(appID string, region regions.Region, repoPath string, ignoreFilePath string) (*upload.File, error) {
	fileDir, err := ioutil.TempDir("", "leanengine")
	if err != nil {
		return nil, err
	}

	archiveFile := filepath.Join(fileDir, "leanengine.zip")

	runtime, err := runtimes.DetectRuntime(repoPath)
	if err == runtimes.ErrRuntimeNotFound {
		logp.Warn("Failed to recognize project type. Please inspect the directory structure if the deployment failed.")
	} else if err != nil {
		return nil, err
	}

	err = runtime.ArchiveUploadFiles(archiveFile, ignoreFilePath)
	if err != nil {
		return nil, err
	}

	file, err := api.UploadToRepoStorage(region, archiveFile)
	if err != nil {
		return nil, err
	}

	return file, nil
}

func uploadWar(appID string, region regions.Region, repoPath string) (*upload.File, error) {
	var warPath string
	files, err := ioutil.ReadDir(filepath.Join(repoPath, "target"))
	if err != nil {
		return nil, err
	}
	for _, file := range files {
		if strings.HasSuffix(file.Name(), ".war") && !file.IsDir() {
			warPath = filepath.Join(repoPath, "target", file.Name())
		}
	}
	if warPath == "" {
		return nil, errors.New("Cannot find .war file in ./target")
	}

	logp.Info("Found .war file:", warPath)

	fileDir, err := ioutil.TempDir("", "leanengine")
	if err != nil {
		return nil, err
	}
	archivePath := filepath.Join(fileDir, "ROOT.war.zip")

	file := []struct{ Name, Path string }{{
		Name: "ROOT.war",
		Path: warPath,
	}}
	if err = utils.ArchiveFiles(archivePath, file); err != nil {
		return nil, err
	}

	return api.UploadToRepoStorage(region, archivePath)
}

func deployFromLocal(appID string, group string, prod int, isDeployFromJavaWar bool, ignoreFilePath string, keepFile bool, opts *api.DeployOptions) error {
	region, err := apps.GetAppRegion(appID)
	if err != nil {
		return err
	}

	var file *upload.File
	if isDeployFromJavaWar {
		file, err = uploadWar(appID, region, ".")
	} else {
		file, err = uploadProject(appID, region, ".", ignoreFilePath)
		if err != nil {
			return err
		}
	}

	if !keepFile {
		defer func() {
			logp.Info("Deleting temporary files")
			err := api.DeleteFromRepoStorage(region, file.ObjectID)
			if err != nil {
				logp.Error(err)
			}
		}()
	}

	eventTok, err := api.DeployAppFromFile(appID, group, prod, file.URL, opts)
	if err != nil {
		return err
	}
	ok, err := api.PollEvents(appID, eventTok)
	if err != nil {
		return err
	}
	if !ok {
		return cli.NewExitError("Deployment failed", 1)
	}
	return nil
}

func deployFromGit(appID string, group string, prod int, revision string, opts *api.DeployOptions) error {
	eventTok, err := api.DeployAppFromGit(appID, group, prod, revision, opts)
	if err != nil {
		return err
	}
	ok, err := api.PollEvents(appID, eventTok)
	if err != nil {
		return err
	}
	if !ok {
		return cli.NewExitError("Deployment failed", 1)
	}
	return nil
}

func getCommentMessage(message string) string {
	if message == "" {
		_, err := exec.LookPath("git")

		if err == nil {
			messageBuf, err := exec.Command("git", "log", "-1", "--no-color", "--pretty=%B").CombinedOutput()
			messageStr := string(messageBuf)

			if err != nil && strings.Contains(messageStr, "Not a git repository") {
				// Ignore
			} else if err != nil {
				logp.Error(err)
			} else {
				message = "WIP on: " + strings.TrimSpace(messageStr)
			}
		}
	}

	if message == "" {
		message = "Creating from the CLI"
	}

	return message
}
<commit_msg>:twisted_rightwards_arrows: Merge pull request #446 from leancloud/comments-error<commit_after>package commands

import (
	"errors"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/aisk/logp"
	"github.com/leancloud/go-upload"
	"github.com/leancloud/lean-cli/api"
	"github.com/leancloud/lean-cli/api/regions"
	"github.com/leancloud/lean-cli/apps"
	"github.com/leancloud/lean-cli/runtimes"
	"github.com/leancloud/lean-cli/utils"
	"github.com/leancloud/lean-cli/version"
	"github.com/urfave/cli"
)

func deployAction(c *cli.Context) error {
	version.PrintCurrentVersion()
	isDeployFromGit := c.Bool("g")
	isDeployFromJavaWar := c.Bool("war")
	ignoreFilePath := c.String("leanignore")
	noDepsCache := c.Bool("no-cache")
	message := c.String("message")
	keepFile := c.Bool("keep-deploy-file")
	revision := c.String("revision")
	prodString := c.String("prod")

	var prod int

	appID, err := apps.GetCurrentAppID(".")
	if err != nil {
		return err
	}

	groupName, err := apps.GetCurrentGroup(".")
	if err != nil {
		return err
	}

	logp.Info("Retrieving app info ...")

	region, err := apps.GetAppRegion(appID)
	if err != nil {
		return err
	}

	appInfo, err := api.GetAppInfo(appID)
	if err != nil {
		return err
	}

	if prodString == "" {
		groupInfo, err := api.GetGroup(appID, groupName)
		if err != nil {
			return err
		}

		if groupInfo.Staging.Deployable {
			prod = 0
		} else {
			prod = 1
		}
	} else {
		prod, err = strconv.Atoi(prodString)
		if err != nil {
			return err
		}
	}

	if prod == 1 {
		logp.Infof("Preparing to deploy %s(%s) to region: %s group: %s production\r\n", appInfo.AppName, appID, region, groupName)
	} else {
		logp.Infof("Preparing to deploy %s(%s) to region: %s group: %s staging\r\n", appInfo.AppName, appID, region, groupName)
	}

	opts := &api.DeployOptions{
		NoDepsCache: noDepsCache,
		Options:     c.String("options"),
	}

	if isDeployFromGit {
		err = deployFromGit(appID, groupName, prod, revision, opts)
		if err != nil {
			return err
		}
	} else {
		opts.Message = getCommentMessage(message)

		err = deployFromLocal(appID, groupName, prod, isDeployFromJavaWar, ignoreFilePath, keepFile, opts)
		if err != nil {
			return err
		}
	}
	return nil
}

func uploadProject(appID string, region regions.Region, repoPath string, ignoreFilePath string) (*upload.File, error) {
	fileDir, err := ioutil.TempDir("", "leanengine")
	if err != nil {
		return nil, err
	}

	archiveFile := filepath.Join(fileDir, "leanengine.zip")

	runtime, err := runtimes.DetectRuntime(repoPath)
	if err == runtimes.ErrRuntimeNotFound {
		logp.Warn("Failed to recognize project type. Please inspect the directory structure if the deployment failed.")
	} else if err != nil {
		return nil, err
	}

	err = runtime.ArchiveUploadFiles(archiveFile, ignoreFilePath)
	if err != nil {
		return nil, err
	}

	file, err := api.UploadToRepoStorage(region, archiveFile)
	if err != nil {
		return nil, err
	}

	return file, nil
}

func uploadWar(appID string, region regions.Region, repoPath string) (*upload.File, error) {
	var warPath string
	files, err := ioutil.ReadDir(filepath.Join(repoPath, "target"))
	if err != nil {
		return nil, err
	}
	for _, file := range files {
		if strings.HasSuffix(file.Name(), ".war") && !file.IsDir() {
			warPath = filepath.Join(repoPath, "target", file.Name())
		}
	}
	if warPath == "" {
		return nil, errors.New("Cannot find .war file in ./target")
	}

	logp.Info("Found .war file:", warPath)

	fileDir, err := ioutil.TempDir("", "leanengine")
	if err != nil {
		return nil, err
	}
	archivePath := filepath.Join(fileDir, "ROOT.war.zip")

	file := []struct{ Name, Path string }{{
		Name: "ROOT.war",
		Path: warPath,
	}}
	if err = utils.ArchiveFiles(archivePath, file); err != nil {
		return nil, err
	}

	return api.UploadToRepoStorage(region, archivePath)
}

func deployFromLocal(appID string, group string, prod int, isDeployFromJavaWar bool, ignoreFilePath string, keepFile bool, opts *api.DeployOptions) error {
	region, err := apps.GetAppRegion(appID)
	if err != nil {
		return err
	}

	var file *upload.File
	if isDeployFromJavaWar {
		file, err = uploadWar(appID, region, ".")
	} else {
		file, err = uploadProject(appID, region, ".", ignoreFilePath)
		if err != nil {
			return err
		}
	}

	if !keepFile {
		defer func() {
			logp.Info("Deleting temporary files")
			err := api.DeleteFromRepoStorage(region, file.ObjectID)
			if err != nil {
				logp.Error(err)
			}
		}()
	}

	eventTok, err := api.DeployAppFromFile(appID, group, prod, file.URL, opts)
	if err != nil {
		return err
	}
	ok, err := api.PollEvents(appID, eventTok)
	if err != nil {
		return err
	}
	if !ok {
		return cli.NewExitError("Deployment failed", 1)
	}
	return nil
}

func deployFromGit(appID string, group string, prod int, revision string, opts *api.DeployOptions) error {
	eventTok, err := api.DeployAppFromGit(appID, group, prod, revision, opts)
	if err != nil {
		return err
	}
	ok, err := api.PollEvents(appID, eventTok)
	if err != nil {
		return err
	}
	if !ok {
		return cli.NewExitError("Deployment failed", 1)
	}
	return nil
}

func getCommentMessage(message string) string {
	if message == "" {
		_, err := exec.LookPath("git")

		if err == nil {
			if _, err := os.Stat("./.git"); !os.IsNotExist(err) {
				messageBuf, err := exec.Command("git", "log", "-1", "--no-color", "--pretty=%B").CombinedOutput()
				messageStr := string(messageBuf)

				if err != nil {
					logp.Error("failed to load git message: ", err)
				} else {
					message = "WIP on: " + strings.TrimSpace(messageStr)
				}
			}
		}
	}

	if message == "" {
		message = "Creating from the CLI"
	}

	return message
}
<|endoftext|>


<commit_before>// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package all

import (
	"os"
	"reflect"

	jujucmd "github.com/juju/cmd"
	"github.com/juju/errors"
	"gopkg.in/juju/names.v2"

	"github.com/juju/juju/api/base"
	"github.com/juju/juju/apiserver/charmrevisionupdater"
	"github.com/juju/juju/apiserver/common"
	"github.com/juju/juju/apiserver/common/apihttp"
	"github.com/juju/juju/cmd/juju/charmcmd"
	"github.com/juju/juju/cmd/juju/commands"
	"github.com/juju/juju/cmd/modelcmd"
	"github.com/juju/juju/resource"
	"github.com/juju/juju/resource/api"
	"github.com/juju/juju/resource/api/client"
	privateapi "github.com/juju/juju/resource/api/private"
	internalclient "github.com/juju/juju/resource/api/private/client"
	internalserver "github.com/juju/juju/resource/api/private/server"
	"github.com/juju/juju/resource/api/server"
	"github.com/juju/juju/resource/cmd"
	"github.com/juju/juju/resource/context"
	contextcmd "github.com/juju/juju/resource/context/cmd"
	"github.com/juju/juju/resource/resourceadapters"
	corestate "github.com/juju/juju/state"
	unitercontext "github.com/juju/juju/worker/uniter/runner/context"
	"github.com/juju/juju/worker/uniter/runner/jujuc"
)

// resources exposes the registration methods needed
// for the top-level component machinery.
type resources struct{}

// RegisterForServer is the top-level registration method
// for the component in a jujud context.
func (r resources) registerForServer() error {
	r.registerState()
	r.registerAgentWorkers()
	r.registerPublicFacade()
	r.registerHookContext()
	return nil
}

// RegisterForClient is the top-level registration method
// for the component in a "juju" command context.
func (r resources) registerForClient() error {
	r.registerPublicCommands()

	// needed for help-tool
	r.registerHookContextCommands()
	return nil
}

// registerPublicFacade adds the resources public API facade
// to the API server.
func (r resources) registerPublicFacade() {
	if !markRegistered(resource.ComponentName, "public-facade") {
		return
	}

	// NOTE: facade is also defined in api/facadeversions.go.
	common.RegisterStandardFacade(
		resource.FacadeName,
		server.Version,
		resourceadapters.NewPublicFacade,
	)

	common.RegisterAPIModelEndpoint(api.HTTPEndpointPattern, apihttp.HandlerSpec{
		Constraints: apihttp.HandlerConstraints{
			AuthKinds:           []string{names.UserTagKind, names.MachineTagKind},
			StrictValidation:    true,
			ControllerModelOnly: false,
		},
		NewHandler: resourceadapters.NewApplicationHandler,
	})
}

// resourcesAPIClient adds a Close() method to the resources public API client.
type resourcesAPIClient struct {
	*client.Client
	closeConnFunc func() error
}

// Close implements io.Closer.
func (client resourcesAPIClient) Close() error {
	return client.closeConnFunc()
}

// registerAgentWorkers adds the resources workers to the agents.
func (r resources) registerAgentWorkers() {
	if !markRegistered(resource.ComponentName, "agent-workers") {
		return
	}

	charmrevisionupdater.RegisterLatestCharmHandler("resources", resourceadapters.NewLatestCharmHandler)
}

// registerState registers the state functionality for resources.
func (resources) registerState() {
	if !markRegistered(resource.ComponentName, "state") {
		return
	}

	corestate.SetResourcesComponent(resourceadapters.NewResourceState)
	corestate.SetResourcesPersistence(resourceadapters.NewResourcePersistence)
	corestate.RegisterCleanupHandler(corestate.CleanupKindResourceBlob, resourceadapters.CleanUpBlob)
}

// registerPublicCommands adds the resources-related commands
// to the "juju" supercommand.
func (r resources) registerPublicCommands() {
	if !markRegistered(resource.ComponentName, "public-commands") {
		return
	}

	charmcmd.RegisterSubCommand(cmd.NewListCharmResourcesCommand())

	commands.RegisterEnvCommand(func() modelcmd.ModelCommand {
		return cmd.NewUploadCommand(cmd.UploadDeps{
			NewClient: func(c *cmd.UploadCommand) (cmd.UploadClient, error) {
				apiRoot, err := c.NewAPIRoot()
				if err != nil {
					return nil, errors.Trace(err)
				}
				return resourceadapters.NewAPIClient(apiRoot)
			},
			OpenResource: func(s string) (cmd.ReadSeekCloser, error) {
				return os.Open(s)
			},
		})

	})

	commands.RegisterEnvCommand(func() modelcmd.ModelCommand {
		return cmd.NewShowServiceCommand(cmd.ShowServiceDeps{
			NewClient: func(c *cmd.ShowServiceCommand) (cmd.ShowServiceClient, error) {
				apiRoot, err := c.NewAPIRoot()
				if err != nil {
					return nil, errors.Trace(err)
				}
				return resourceadapters.NewAPIClient(apiRoot)
			},
		})
	})
}

// TODO(katco): This seems to be common across components. Pop up a
// level and genericize?
func (r resources) registerHookContext() {
	if markRegistered(resource.ComponentName, "hook-context") == false {
		return
	}

	unitercontext.RegisterComponentFunc(
		resource.ComponentName,
		func(config unitercontext.ComponentConfig) (jujuc.ContextComponent, error) {
			unitID := names.NewUnitTag(config.UnitName).String()
			hctxClient, err := r.newUnitFacadeClient(unitID, config.APICaller)
			if err != nil {
				return nil, errors.Trace(err)
			}
			// TODO(ericsnow) Pass the unit's tag through to the component?
			return context.NewContextAPI(hctxClient, config.DataDir), nil
		},
	)

	r.registerHookContextCommands()
	r.registerHookContextFacade()
	r.registerUnitDownloadEndpoint()
}

func (r resources) registerHookContextCommands() {
	if markRegistered(resource.ComponentName, "hook-context-commands") == false {
		return
	}

	jujuc.RegisterCommand(
		contextcmd.GetCmdName,
		func(ctx jujuc.Context) (jujucmd.Command, error) {
			compCtx, err := ctx.Component(resource.ComponentName)
			if err != nil {
				return nil, errors.Trace(err)
			}
			cmd, err := contextcmd.NewGetCmd(compCtx)
			if err != nil {
				return nil, errors.Trace(err)
			}
			return cmd, nil
		},
	)
}

func (r resources) registerHookContextFacade() {
	common.RegisterHookContextFacade(
		context.HookContextFacade,
		internalserver.FacadeVersion,
		r.newHookContextFacade,
		reflect.TypeOf(&internalserver.UnitFacade{}),
	)

}

func (r resources) registerUnitDownloadEndpoint() {
	common.RegisterAPIModelEndpoint(privateapi.HTTPEndpointPattern, apihttp.HandlerSpec{
		Constraints: apihttp.HandlerConstraints{
			// Machines are allowed too so that unit resources can be
			// retrieved for model migrations.
			AuthKinds:           []string{names.UnitTagKind, names.MachineTagKind},
			StrictValidation:    true,
			ControllerModelOnly: false,
		},
		NewHandler: resourceadapters.NewDownloadHandler,
	})
}

// resourcesUnitDatastore is a shim to elide serviceName from
// ListResources.
type resourcesUnitDataStore struct {
	resources corestate.Resources
	unit      *corestate.Unit
}

// ListResources implements resource/api/private/server.UnitDataStore.
func (ds *resourcesUnitDataStore) ListResources() (resource.ServiceResources, error) {
	return ds.resources.ListResources(ds.unit.ApplicationName())
}

func (r resources) newHookContextFacade(st *corestate.State, unit *corestate.Unit) (interface{}, error) {
	res, err := st.Resources()
	if err != nil {
		return nil, errors.Trace(err)
	}
	return internalserver.NewUnitFacade(&resourcesUnitDataStore{res, unit}), nil
}

func (r resources) newUnitFacadeClient(unitName string, caller base.APICaller) (context.APIClient, error) {

	facadeCaller := base.NewFacadeCallerForVersion(caller, context.HookContextFacade, internalserver.FacadeVersion)
	httpClient, err := caller.HTTPClient()
	if err != nil {
		return nil, errors.Trace(err)
	}
	unitHTTPClient := internalclient.NewUnitHTTPClient(httpClient, unitName)

	return internalclient.NewUnitFacadeClient(facadeCaller, unitHTTPClient), nil
}
<commit_msg>Remove machine access to unit resource endpoint<commit_after>// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package all

import (
	"os"
	"reflect"

	jujucmd "github.com/juju/cmd"
	"github.com/juju/errors"
	"gopkg.in/juju/names.v2"

	"github.com/juju/juju/api/base"
	"github.com/juju/juju/apiserver/charmrevisionupdater"
	"github.com/juju/juju/apiserver/common"
	"github.com/juju/juju/apiserver/common/apihttp"
	"github.com/juju/juju/cmd/juju/charmcmd"
	"github.com/juju/juju/cmd/juju/commands"
	"github.com/juju/juju/cmd/modelcmd"
	"github.com/juju/juju/resource"
	"github.com/juju/juju/resource/api"
	"github.com/juju/juju/resource/api/client"
	privateapi "github.com/juju/juju/resource/api/private"
	internalclient "github.com/juju/juju/resource/api/private/client"
	internalserver "github.com/juju/juju/resource/api/private/server"
	"github.com/juju/juju/resource/api/server"
	"github.com/juju/juju/resource/cmd"
	"github.com/juju/juju/resource/context"
	contextcmd "github.com/juju/juju/resource/context/cmd"
	"github.com/juju/juju/resource/resourceadapters"
	corestate "github.com/juju/juju/state"
	unitercontext "github.com/juju/juju/worker/uniter/runner/context"
	"github.com/juju/juju/worker/uniter/runner/jujuc"
)

// resources exposes the registration methods needed
// for the top-level component machinery.
type resources struct{}

// RegisterForServer is the top-level registration method
// for the component in a jujud context.
func (r resources) registerForServer() error {
	r.registerState()
	r.registerAgentWorkers()
	r.registerPublicFacade()
	r.registerHookContext()
	return nil
}

// RegisterForClient is the top-level registration method
// for the component in a "juju" command context.
func (r resources) registerForClient() error {
	r.registerPublicCommands()

	// needed for help-tool
	r.registerHookContextCommands()
	return nil
}

// registerPublicFacade adds the resources public API facade
// to the API server.
func (r resources) registerPublicFacade() {
	if !markRegistered(resource.ComponentName, "public-facade") {
		return
	}

	// NOTE: facade is also defined in api/facadeversions.go.
	common.RegisterStandardFacade(
		resource.FacadeName,
		server.Version,
		resourceadapters.NewPublicFacade,
	)

	common.RegisterAPIModelEndpoint(api.HTTPEndpointPattern, apihttp.HandlerSpec{
		Constraints: apihttp.HandlerConstraints{
			AuthKinds:           []string{names.UserTagKind, names.MachineTagKind},
			StrictValidation:    true,
			ControllerModelOnly: false,
		},
		NewHandler: resourceadapters.NewApplicationHandler,
	})
}

// resourcesAPIClient adds a Close() method to the resources public API client.
type resourcesAPIClient struct {
	*client.Client
	closeConnFunc func() error
}

// Close implements io.Closer.
func (client resourcesAPIClient) Close() error {
	return client.closeConnFunc()
}

// registerAgentWorkers adds the resources workers to the agents.
func (r resources) registerAgentWorkers() {
	if !markRegistered(resource.ComponentName, "agent-workers") {
		return
	}

	charmrevisionupdater.RegisterLatestCharmHandler("resources", resourceadapters.NewLatestCharmHandler)
}

// registerState registers the state functionality for resources.
func (resources) registerState() {
	if !markRegistered(resource.ComponentName, "state") {
		return
	}

	corestate.SetResourcesComponent(resourceadapters.NewResourceState)
	corestate.SetResourcesPersistence(resourceadapters.NewResourcePersistence)
	corestate.RegisterCleanupHandler(corestate.CleanupKindResourceBlob, resourceadapters.CleanUpBlob)
}

// registerPublicCommands adds the resources-related commands
// to the "juju" supercommand.
func (r resources) registerPublicCommands() {
	if !markRegistered(resource.ComponentName, "public-commands") {
		return
	}

	charmcmd.RegisterSubCommand(cmd.NewListCharmResourcesCommand())

	commands.RegisterEnvCommand(func() modelcmd.ModelCommand {
		return cmd.NewUploadCommand(cmd.UploadDeps{
			NewClient: func(c *cmd.UploadCommand) (cmd.UploadClient, error) {
				apiRoot, err := c.NewAPIRoot()
				if err != nil {
					return nil, errors.Trace(err)
				}
				return resourceadapters.NewAPIClient(apiRoot)
			},
			OpenResource: func(s string) (cmd.ReadSeekCloser, error) {
				return os.Open(s)
			},
		})

	})

	commands.RegisterEnvCommand(func() modelcmd.ModelCommand {
		return cmd.NewShowServiceCommand(cmd.ShowServiceDeps{
			NewClient: func(c *cmd.ShowServiceCommand) (cmd.ShowServiceClient, error) {
				apiRoot, err := c.NewAPIRoot()
				if err != nil {
					return nil, errors.Trace(err)
				}
				return resourceadapters.NewAPIClient(apiRoot)
			},
		})
	})
}

// TODO(katco): This seems to be common across components. Pop up a
// level and genericize?
func (r resources) registerHookContext() {
	if markRegistered(resource.ComponentName, "hook-context") == false {
		return
	}

	unitercontext.RegisterComponentFunc(
		resource.ComponentName,
		func(config unitercontext.ComponentConfig) (jujuc.ContextComponent, error) {
			unitID := names.NewUnitTag(config.UnitName).String()
			hctxClient, err := r.newUnitFacadeClient(unitID, config.APICaller)
			if err != nil {
				return nil, errors.Trace(err)
			}
			// TODO(ericsnow) Pass the unit's tag through to the component?
			return context.NewContextAPI(hctxClient, config.DataDir), nil
		},
	)

	r.registerHookContextCommands()
	r.registerHookContextFacade()
	r.registerUnitDownloadEndpoint()
}

func (r resources) registerHookContextCommands() {
	if markRegistered(resource.ComponentName, "hook-context-commands") == false {
		return
	}

	jujuc.RegisterCommand(
		contextcmd.GetCmdName,
		func(ctx jujuc.Context) (jujucmd.Command, error) {
			compCtx, err := ctx.Component(resource.ComponentName)
			if err != nil {
				return nil, errors.Trace(err)
			}
			cmd, err := contextcmd.NewGetCmd(compCtx)
			if err != nil {
				return nil, errors.Trace(err)
			}
			return cmd, nil
		},
	)
}

func (r resources) registerHookContextFacade() {
	common.RegisterHookContextFacade(
		context.HookContextFacade,
		internalserver.FacadeVersion,
		r.newHookContextFacade,
		reflect.TypeOf(&internalserver.UnitFacade{}),
	)

}

func (r resources) registerUnitDownloadEndpoint() {
	common.RegisterAPIModelEndpoint(privateapi.HTTPEndpointPattern, apihttp.HandlerSpec{
		Constraints: apihttp.HandlerConstraints{
			AuthKinds:           []string{names.UnitTagKind},
			StrictValidation:    true,
			ControllerModelOnly: false,
		},
		NewHandler: resourceadapters.NewDownloadHandler,
	})
}

// resourcesUnitDatastore is a shim to elide serviceName from
// ListResources.
type resourcesUnitDataStore struct {
	resources corestate.Resources
	unit      *corestate.Unit
}

// ListResources implements resource/api/private/server.UnitDataStore.
func (ds *resourcesUnitDataStore) ListResources() (resource.ServiceResources, error) {
	return ds.resources.ListResources(ds.unit.ApplicationName())
}

func (r resources) newHookContextFacade(st *corestate.State, unit *corestate.Unit) (interface{}, error) {
	res, err := st.Resources()
	if err != nil {
		return nil, errors.Trace(err)
	}
	return internalserver.NewUnitFacade(&resourcesUnitDataStore{res, unit}), nil
}

func (r resources) newUnitFacadeClient(unitName string, caller base.APICaller) (context.APIClient, error) {

	facadeCaller := base.NewFacadeCallerForVersion(caller, context.HookContextFacade, internalserver.FacadeVersion)
	httpClient, err := caller.HTTPClient()
	if err != nil {
		return nil, errors.Trace(err)
	}
	unitHTTPClient := internalclient.NewUnitHTTPClient(httpClient, unitName)

	return internalclient.NewUnitFacadeClient(facadeCaller, unitHTTPClient), nil
}
<|endoftext|>