eligible/eligible.go (176 lines of code) (raw):
// Copyright 2017 Netflix, Inc.
//
// 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 eligible contains methods that determine which instances are eligible for Chaos Monkey termination
package eligible
import (
"github.com/Netflix/chaosmonkey/v2"
"github.com/Netflix/chaosmonkey/v2/deploy"
"github.com/Netflix/chaosmonkey/v2/grp"
"github.com/SmartThingsOSS/frigga-go"
"github.com/pkg/errors"
"strings"
)
// TODO: make these a configuration parameter
var neverEligibleSuffixes = []string{"-canary", "-baseline", "-citrus", "-citrusproxy"}
type (
cluster struct {
appName deploy.AppName
accountName deploy.AccountName
cloudProvider deploy.CloudProvider
regionName deploy.RegionName
clusterName deploy.ClusterName
}
instance struct {
appName deploy.AppName
accountName deploy.AccountName
regionName deploy.RegionName
stackName deploy.StackName
clusterName deploy.ClusterName
asgName deploy.ASGName
id deploy.InstanceID
cloudProvider deploy.CloudProvider
}
)
func (i instance) AppName() string {
return string(i.appName)
}
func (i instance) AccountName() string {
return string(i.accountName)
}
func (i instance) RegionName() string {
return string(i.regionName)
}
func (i instance) StackName() string {
return string(i.stackName)
}
func (i instance) ClusterName() string {
return string(i.clusterName)
}
func (i instance) ASGName() string {
return string(i.asgName)
}
func (i instance) Name() string {
return string(i.clusterName)
}
func (i instance) ID() string {
return string(i.id)
}
func (i instance) CloudProvider() string {
return string(i.cloudProvider)
}
func isException(exs []chaosmonkey.Exception, account deploy.AccountName, names *frigga.Names, region deploy.RegionName) bool {
for _, ex := range exs {
if ex.Matches(string(account), names.Stack, names.Detail, string(region)) {
return true
}
}
return false
}
func isNeverEligible(cluster deploy.ClusterName) bool {
for _, suffix := range neverEligibleSuffixes {
if strings.HasSuffix(string(cluster), suffix) {
return true
}
}
return false
}
func clusters(group grp.InstanceGroup, cloudProvider deploy.CloudProvider, exs []chaosmonkey.Exception, dep deploy.Deployment) ([]cluster, error) {
account := deploy.AccountName(group.Account())
clusterNames, err := dep.GetClusterNames(group.App(), account)
if err != nil {
return nil, err
}
result := make([]cluster, 0)
for _, clusterName := range clusterNames {
names, err := frigga.Parse(string(clusterName))
if err != nil {
return nil, err
}
deployedRegions, err := dep.GetRegionNames(names.App, account, clusterName)
if err != nil {
return nil, err
}
for _, region := range regions(group, deployedRegions) {
if isException(exs, account, names, region) {
continue
}
if isNeverEligible(clusterName) {
continue
}
if grp.Contains(group, string(account), string(region), string(clusterName)) {
result = append(result, cluster{
appName: deploy.AppName(names.App),
accountName: account,
cloudProvider: cloudProvider,
regionName: region,
clusterName: clusterName,
})
}
}
}
return result, nil
}
// regions returns list of candidate regions for termination given app config and where cluster is deployed
func regions(group grp.InstanceGroup, deployedRegions []deploy.RegionName) []deploy.RegionName {
region, ok := group.Region()
if ok {
return regionsWhenTermScopedtoSingleRegion(region, deployedRegions)
}
return deployedRegions
}
// regionsWhenTermScopedtoSingleRegion returns a list containing either the region or empty, depending on whether the region is one of the deployed ones
func regionsWhenTermScopedtoSingleRegion(region string, deployedRegions []deploy.RegionName) []deploy.RegionName {
if contains(region, deployedRegions) {
return []deploy.RegionName{deploy.RegionName(region)}
}
return nil
}
func contains(region string, regions []deploy.RegionName) bool {
for _, r := range regions {
if region == string(r) {
return true
}
}
return false
}
const whiteListErrorMessage = "whitelist is not supported"
// isWhiteList returns true if an error is related to a whitelist
func isWhitelist(err error) bool {
return err.Error() == whiteListErrorMessage
}
// Instances returns instances eligible for termination
func Instances(group grp.InstanceGroup, exs []chaosmonkey.Exception, dep deploy.Deployment) ([]chaosmonkey.Instance, error) {
cloudProvider, err := dep.CloudProvider(group.Account())
if err != nil {
return nil, errors.Wrap(err, "retrieve cloud provider failed")
}
cls, err := clusters(group, deploy.CloudProvider(cloudProvider), exs, dep)
if err != nil {
return nil, err
}
result := make([]chaosmonkey.Instance, 0)
for _, cl := range cls {
instances, err := getInstances(cl, dep)
if err != nil {
return nil, err
}
result = append(result, instances...)
}
return result, nil
}
func getInstances(cl cluster, dep deploy.Deployment) ([]chaosmonkey.Instance, error) {
result := make([]chaosmonkey.Instance, 0)
asgName, ids, err := dep.GetInstanceIDs(string(cl.appName), cl.accountName, string(cl.cloudProvider), cl.regionName, cl.clusterName)
if err != nil {
return nil, err
}
for _, id := range ids {
names, err := frigga.Parse(string(asgName))
if err != nil {
return nil, errors.Wrap(err, "failed to parse")
}
result = append(result,
instance{appName: cl.appName,
accountName: cl.accountName,
regionName: cl.regionName,
stackName: deploy.StackName(names.Stack),
clusterName: cl.clusterName,
asgName: deploy.ASGName(asgName),
id: id,
cloudProvider: cl.cloudProvider,
})
}
return result, nil
}