forked from wso2/cellery
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
45ef3a1
commit 40c0168
Showing
9 changed files
with
420 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/* | ||
* Copyright (c) 2019 WSO2 Inc. (http:www.wso2.org) All Rights Reserved. | ||
* | ||
* WSO2 Inc. licenses this file to you 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 main | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/cellery-io/sdk/components/cli/pkg/util" | ||
|
||
"github.com/spf13/cobra" | ||
|
||
"github.com/cellery-io/sdk/components/cli/pkg/commands" | ||
"github.com/cellery-io/sdk/components/cli/pkg/constants" | ||
) | ||
|
||
func newRouteTrafficCommand() *cobra.Command { | ||
var source string | ||
var targetPercentage string | ||
var targetInstance string | ||
var percentage int | ||
var srcInstances []string | ||
cmd := &cobra.Command{ | ||
Use: "route-traffic [--source|-s=<list_of_source_cell_instances>] <dependency_instance_name> --percentage|-p <target_cell_instance>=<x>", | ||
Short: "route a percentage of the traffic to a cell instance", | ||
Example: "cellery route-traffic --source hr-client-inst1 hr-inst-1 --percentage hr-inst-2=20 \n" + | ||
"cellery route-traffic hr-inst-1 --percentage hr-inst-2=20", | ||
Args: func(cmd *cobra.Command, args []string) error { | ||
err := cobra.MinimumNArgs(1)(cmd, args) | ||
if err != nil { | ||
return err | ||
} | ||
// validate dependency cell instance name | ||
isCellInstValid, err := regexp.MatchString(fmt.Sprintf("^%s$", constants.CELLERY_ID_PATTERN), args[0]) | ||
if err != nil { | ||
util.ExitWithErrorMessage("Error in running route traffic command", err) | ||
} | ||
if !isCellInstValid { | ||
util.ExitWithErrorMessage("Error in running route traffic command", fmt.Errorf("expects a valid cell instance name, received %s", args[0])) | ||
} | ||
//validate source cell instances | ||
srcInstances = getSourceCellInstanceArr(source) | ||
for _, srcInstance := range srcInstances { | ||
isCellInstValid, err := regexp.MatchString(fmt.Sprintf("^%s$", constants.CELLERY_ID_PATTERN), srcInstance) | ||
if err != nil { | ||
util.ExitWithErrorMessage("Error in running route traffic command", err) | ||
} | ||
if !isCellInstValid { | ||
util.ExitWithErrorMessage("Error in running route traffic command", fmt.Errorf("expects a valid source cell instance name, received %s", srcInstance)) | ||
} | ||
} | ||
targetInstance, percentage, err = getTargetCelInstanceAndPercentage(targetPercentage) | ||
if err != nil { | ||
util.ExitWithErrorMessage("Error in running route traffic command", err) | ||
} | ||
return nil | ||
}, | ||
Run: func(cmd *cobra.Command, args []string) { | ||
err := commands.RunRouteTrafficCommand(srcInstances, args[0], targetInstance, percentage) | ||
if err != nil { | ||
util.ExitWithErrorMessage(fmt.Sprintf("Unable to route traffic to the target instance: %s, percentage: %s", targetInstance, percentage), err) | ||
} | ||
}, | ||
} | ||
cmd.Flags().StringVarP(&source, "source", "s", "", "comma separated source instance list") | ||
cmd.Flags().StringVarP(&targetPercentage, "percentage", "p", "", "target instance and percentage of traffic joined by a '=' mark") | ||
_ = cmd.MarkFlagRequired("percentage") | ||
return cmd | ||
} | ||
|
||
func getSourceCellInstanceArr(sourceCellInstances string) []string { | ||
var trimmedInstances []string | ||
if len(sourceCellInstances) == 0 { | ||
return trimmedInstances | ||
} | ||
instances := strings.Split(sourceCellInstances, ",") | ||
for _, instance := range instances { | ||
trimmedInstances = append(trimmedInstances, strings.TrimSpace(instance)) | ||
} | ||
return trimmedInstances | ||
} | ||
|
||
func getTargetCelInstanceAndPercentage(target string) (string, int, error) { | ||
parts := strings.Split(target, "=") | ||
if len(parts) != 2 { | ||
return "", -1, fmt.Errorf("target instance and percentage in incorrect format %s", target) | ||
} | ||
percentage, err := strconv.Atoi(parts[1]) | ||
if err != nil { | ||
return "", -1, err | ||
} | ||
if percentage > 100 { | ||
return "", -1, fmt.Errorf("invalid percentge provided for target instance: %d", percentage) | ||
} | ||
// validate target cell instance name | ||
isCellInstValid, err := regexp.MatchString(fmt.Sprintf("^%s$", constants.CELLERY_ID_PATTERN), parts[0]) | ||
if err != nil { | ||
return "", -1, err | ||
} | ||
if !isCellInstValid { | ||
return "", -1, fmt.Errorf("expects a valid cell instance name, received %s", parts[0]) | ||
} | ||
return parts[0], percentage, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
///* | ||
// * Copyright (c) 2019 WSO2 Inc. (http:www.wso2.org) All Rights Reserved. | ||
// * | ||
// * WSO2 Inc. licenses this file to you 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 commands | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"strings" | ||
|
||
"github.com/cellery-io/sdk/components/cli/pkg/util" | ||
|
||
"github.com/ghodss/yaml" | ||
|
||
"github.com/cellery-io/sdk/components/cli/pkg/kubectl" | ||
"github.com/cellery-io/sdk/components/cli/pkg/routing" | ||
) | ||
|
||
const instance = "instance" | ||
|
||
func RunRouteTrafficCommand(sourceInstances []string, dependencyInstance string, targetInstance string, percentage int) error { | ||
spinner := util.StartNewSpinner(fmt.Sprintf("Starting to route %d%% of traffic to instance %s", percentage, targetInstance)) | ||
// check if the target instance exists | ||
_, err := kubectl.GetCell(targetInstance) | ||
if err != nil { | ||
spinner.Stop(false) | ||
return err | ||
} | ||
// check the source instance and see if the dependency exists in the source | ||
dependingSourceInstances, err := getDependingSrcInstances(sourceInstances, dependencyInstance) | ||
// now we have the source instance list which actually depend on the given dependency instance. | ||
// get the virtual services corresponding to the given source instances and modify accordingly. | ||
if len(*dependingSourceInstances) == 0 { | ||
// no depending instances | ||
spinner.Stop(false) | ||
return fmt.Errorf("dependency cell instance %s not found among the source cell instances", dependencyInstance) | ||
} | ||
var modfiedVss []kubectl.VirtualService | ||
spinner.SetNewAction("Modifying routing rules") | ||
for _, depSrcInst := range *dependingSourceInstances { | ||
vs, err := kubectl.GetVirtualService(routing.GetCellVsName(depSrcInst)) | ||
if err != nil { | ||
spinner.Stop(false) | ||
return err | ||
} | ||
// modify the vs to include new route information. | ||
modfiedVss = append(modfiedVss, *getModifiedVs(vs, dependencyInstance, targetInstance, percentage)) | ||
} | ||
vsFile := fmt.Sprintf("./%s-vs.yaml", dependencyInstance) | ||
err = writeVirtualSvcsToFile(vsFile, &modfiedVss) | ||
if err != nil { | ||
spinner.Stop(false) | ||
return err | ||
} | ||
defer func() { | ||
_ = os.Remove(vsFile) | ||
}() | ||
// perform kubectl apply | ||
err = kubectl.ApplyFile(vsFile) | ||
if err != nil { | ||
spinner.Stop(false) | ||
return err | ||
} | ||
|
||
spinner.Stop(true) | ||
util.PrintSuccessMessage(fmt.Sprintf("Successfully routed %d%% of traffic to instance %s", percentage, targetInstance)) | ||
return nil | ||
} | ||
|
||
func getDependingSrcInstances(sourceInstances []string, dependencyInstance string) (*[]string, error) { | ||
var dependingSourceInstances []string | ||
if len(sourceInstances) > 0 { | ||
for _, srcInst := range sourceInstances { | ||
cellInst, err := kubectl.GetCell(srcInst) | ||
if err != nil { | ||
return nil, err | ||
} | ||
dependencies, err := extractDependencies(cellInst.CellMetaData.Annotations.Dependencies) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for _, dependency := range dependencies { | ||
if dependency[instance] == dependencyInstance { | ||
dependingSourceInstances = append(dependingSourceInstances, srcInst) | ||
} | ||
} | ||
} | ||
} else { | ||
// need to get all cell instances, then check if there are instances which depend on the `dependencyInstance` | ||
cellInstances, err := kubectl.GetCells() | ||
if err != nil { | ||
return nil, err | ||
} | ||
for _, cellInst := range cellInstances.Items { | ||
dependencies, err := extractDependencies(cellInst.CellMetaData.Annotations.Dependencies) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for _, dependency := range dependencies { | ||
if dependency[instance] == dependencyInstance { | ||
dependingSourceInstances = append(dependingSourceInstances, cellInst.CellMetaData.Name) | ||
} | ||
} | ||
} | ||
} | ||
return &dependingSourceInstances, nil | ||
} | ||
|
||
func extractDependencies(depJson string) ([]map[string]string, error) { | ||
var dependencies []map[string]string | ||
if depJson == "" { | ||
// no dependencies | ||
return dependencies, nil | ||
} | ||
err := json.Unmarshal([]byte(depJson), &dependencies) | ||
if err != nil { | ||
return dependencies, err | ||
} | ||
return dependencies, nil | ||
} | ||
|
||
func getModifiedVs(vs kubectl.VirtualService, dependencyInst string, targetInst string, percentageForTarget int) *kubectl.VirtualService { | ||
var routesCollection []kubectl.Route | ||
for i, httpRule := range vs.VsSpec.HTTP { | ||
for _, match := range httpRule.Match { | ||
if strings.HasPrefix(match.Authority.Regex, fmt.Sprintf("^(%s)", dependencyInst)) { | ||
routesCollection = *buildRoutes(dependencyInst, targetInst, percentageForTarget) | ||
httpRule.Route = routesCollection | ||
} | ||
} | ||
vs.VsSpec.HTTP[i] = httpRule | ||
} | ||
return &vs | ||
} | ||
|
||
func buildRoutes(dependencyInst string, targetInst string, percentageForTarget int) *[]kubectl.Route { | ||
var routes []kubectl.Route | ||
if percentageForTarget == 100 { | ||
// full traffic switch to target, need only one route | ||
routes = append(routes, kubectl.Route{ | ||
Destination: kubectl.Destination{ | ||
Host: routing.GetCellGatewayHost(targetInst), | ||
}, | ||
Weight: 100, | ||
}) | ||
} else { | ||
// modify the existing Route's weight | ||
existingRoute := kubectl.Route{ | ||
Destination: kubectl.Destination{ | ||
Host: routing.GetCellGatewayHost(dependencyInst), | ||
}, | ||
Weight: (100 - percentageForTarget), | ||
} | ||
// add the new route | ||
newRoute := kubectl.Route{ | ||
Destination: kubectl.Destination{ | ||
Host: routing.GetCellGatewayHost(targetInst), | ||
}, | ||
Weight: percentageForTarget, | ||
} | ||
routes = append(routes, existingRoute) | ||
routes = append(routes, newRoute) | ||
} | ||
return &routes | ||
} | ||
|
||
func writeVirtualSvcsToFile(policiesFile string, vss *[]kubectl.VirtualService) error { | ||
f, err := os.OpenFile(policiesFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) | ||
if err != nil { | ||
return err | ||
} | ||
defer func() { | ||
_ = f.Close() | ||
}() | ||
for _, vs := range *vss { | ||
yamlContent, err := yaml.Marshal(vs) | ||
if err != nil { | ||
return err | ||
} | ||
if _, err := f.Write(yamlContent); err != nil { | ||
return err | ||
} | ||
if _, err := f.Write([]byte("---\n")); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.