From 40c0168a7fa093396c57f96bb057cd1a23c9c5f1 Mon Sep 17 00:00:00 2001 From: Izza Date: Wed, 26 Jun 2019 20:48:01 +0530 Subject: [PATCH] route traffic - initial changes --- components/cli/cmd/cellery/cellery.go | 1 + components/cli/cmd/cellery/route_traffic.go | 122 +++++++++++ components/cli/pkg/commands/route_traffic.go | 204 ++++++++++++++++++ components/cli/pkg/kubectl/get.go | 18 ++ components/cli/pkg/kubectl/structs.go | 40 ++++ .../cli/pkg/routing/advanced_routing.go | 29 +++ test-cases/employee-portal/cellery/hr/hr.bal | 4 +- test-cases/pet-care-store/pet-fe/pet-fe.bal | 2 +- .../cellery/reviews/reviews.bal | 6 +- 9 files changed, 420 insertions(+), 6 deletions(-) create mode 100644 components/cli/cmd/cellery/route_traffic.go create mode 100644 components/cli/pkg/commands/route_traffic.go create mode 100644 components/cli/pkg/routing/advanced_routing.go diff --git a/components/cli/cmd/cellery/cellery.go b/components/cli/cmd/cellery/cellery.go index 9b0740bd..5c3d24dc 100644 --- a/components/cli/cmd/cellery/cellery.go +++ b/components/cli/cmd/cellery/cellery.go @@ -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 diff --git a/components/cli/cmd/cellery/route_traffic.go b/components/cli/cmd/cellery/route_traffic.go new file mode 100644 index 00000000..46a39dec --- /dev/null +++ b/components/cli/cmd/cellery/route_traffic.go @@ -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=] --percentage|-p =", + 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 +} diff --git a/components/cli/pkg/commands/route_traffic.go b/components/cli/pkg/commands/route_traffic.go new file mode 100644 index 00000000..0353ab68 --- /dev/null +++ b/components/cli/pkg/commands/route_traffic.go @@ -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 +} diff --git a/components/cli/pkg/kubectl/get.go b/components/cli/pkg/kubectl/get.go index 3faf24bf..729575cd 100644 --- a/components/cli/pkg/kubectl/get.go +++ b/components/cli/pkg/kubectl/get.go @@ -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 +} diff --git a/components/cli/pkg/kubectl/structs.go b/components/cli/pkg/kubectl/structs.go index 25260253..65f37d77 100644 --- a/components/cli/pkg/kubectl/structs.go +++ b/components/cli/pkg/kubectl/structs.go @@ -133,6 +133,7 @@ type CellAnnotations struct { Organization string `json:"mesh.cellery.io/cell-image-org"` Name string `json:"mesh.cellery.io/cell-image-name"` Version string `json:"mesh.cellery.io/cell-image-version"` + Dependencies string `json:"mesh.cellery.io/cell-dependencies"` } type CellStatus struct { @@ -186,3 +187,42 @@ type ServiceSpec struct { type ServicePort struct { Port int `json:"port"` } + +type VirtualService struct { + Kind string `json:"kind"` + APIVersion string `json:"apiVersion"` + VsMetaData VsMetaData `json:"metadata"` + VsSpec VsSpec `json:"spec"` +} + +type VsMetaData struct { + Name string `json:"name"` +} + +type VsSpec struct { + Hosts []string `json:"hosts"` + HTTP []HTTP `json:"http,omitempty"` +} + +type HTTP struct { + Match []Match `json:"match"` + Route []Route `json:"route"` +} + +type Match struct { + Authority Authority `json:"authority"` + SourceLabels map[string]string `json:"sourceLabels"` +} + +type Authority struct { + Regex string `json:"regex"` +} + +type Route struct { + Destination Destination `json:"destination"` + Weight int `json:"weight,omitempty"` +} + +type Destination struct { + Host string `json:"host"` +} diff --git a/components/cli/pkg/routing/advanced_routing.go b/components/cli/pkg/routing/advanced_routing.go new file mode 100644 index 00000000..db52a371 --- /dev/null +++ b/components/cli/pkg/routing/advanced_routing.go @@ -0,0 +1,29 @@ +/* + * 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 routing + +import "fmt" + +func GetCellGatewayHost(instance string) string { + return fmt.Sprintf("%s--gateway-service", instance) +} + +func GetCellVsName(instance string) string { + return fmt.Sprintf("%s--vs", instance) +} diff --git a/test-cases/employee-portal/cellery/hr/hr.bal b/test-cases/employee-portal/cellery/hr/hr.bal index cf15281a..0ddb214c 100644 --- a/test-cases/employee-portal/cellery/hr/hr.bal +++ b/test-cases/employee-portal/cellery/hr/hr.bal @@ -29,8 +29,8 @@ public function build(cellery:ImageName iName) returns error? { }, dependencies: { cells: { - employeeCellDep: "myorg/employee:1.0.0", // fully qualified dependency image name as a string - stockCellDep: { org: "myorg", name: "stock", ver: "1.0.0" } // dependency as a struct + employeeCellDep: "izza/employee:0.0.3", // fully qualified dependency image name as a string + stockCellDep: { org: "izza", name: "stock", ver: "0.0.3" } // dependency as a struct } } }; diff --git a/test-cases/pet-care-store/pet-fe/pet-fe.bal b/test-cases/pet-care-store/pet-fe/pet-fe.bal index 2440641a..56ab4174 100644 --- a/test-cases/pet-care-store/pet-fe/pet-fe.bal +++ b/test-cases/pet-care-store/pet-fe/pet-fe.bal @@ -56,7 +56,7 @@ public function build(cellery:ImageName iName) returns error? { }, dependencies: { cells: { - petstorebackend: { org: "myorg", name: "petbe", ver: "1.0.0" } + petstorebackend: { org: "izza", name: "petbe", ver: "0.0.3" } } } }; diff --git a/test-cases/product-review/cellery/reviews/reviews.bal b/test-cases/product-review/cellery/reviews/reviews.bal index 6ab11500..bbf591b9 100644 --- a/test-cases/product-review/cellery/reviews/reviews.bal +++ b/test-cases/product-review/cellery/reviews/reviews.bal @@ -58,7 +58,7 @@ public function build(cellery:ImageName iName) returns error? { } ] }, - expose: "global" + expose: "local" } }, envVars: { @@ -79,8 +79,8 @@ public function build(cellery:ImageName iName) returns error? { }, dependencies: { cells: { - customerProduct: { org: "myorg", name: "products", ver: "1.0.0" }, - database: { org: "myorg", name: "database", ver: "1.0.0" } + customerProduct: { org: "izza", name: "customer", ver: "1.0.0" }, + database: { org: "izza", name: "database", ver: "1.0.0" } } } };