Skip to content

Commit

Permalink
route traffic - initial changes
Browse files Browse the repository at this point in the history
  • Loading branch information
isurulucky authored and Mirage20 committed Jun 28, 2019
1 parent 45ef3a1 commit 40c0168
Show file tree
Hide file tree
Showing 9 changed files with 420 additions and 6 deletions.
1 change: 1 addition & 0 deletions components/cli/cmd/cellery/cellery.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func newCliCommand() *cobra.Command {
newExportPolicyCommand(),
newApplyPolicyCommand(),
newUpdateCellComponentsCommand(),
newRouteTrafficCommand(),
)
cmd.PersistentFlags().BoolVarP(&verboseMode, "verbose", "v", false, "Run on verbose mode")
return cmd
Expand Down
122 changes: 122 additions & 0 deletions components/cli/cmd/cellery/route_traffic.go
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
}
204 changes: 204 additions & 0 deletions components/cli/pkg/commands/route_traffic.go
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
}
18 changes: 18 additions & 0 deletions components/cli/pkg/kubectl/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,21 @@ func GetGateways(cellName string) (Gateway, error) {
err = json.Unmarshal([]byte(out), &jsonOutput)
return jsonOutput, err
}

func GetVirtualService(vs string) (VirtualService, error) {
cmd := exec.Command(constants.KUBECTL,
"get",
"virtualservice",
vs,
"-o",
"json",
)
displayVerboseOutput(cmd)
jsonOutput := VirtualService{}
out, err := getCommandOutput(cmd)
if err != nil {
return jsonOutput, err
}
err = json.Unmarshal([]byte(out), &jsonOutput)
return jsonOutput, err
}
Loading

0 comments on commit 40c0168

Please sign in to comment.