Using go-rancher to update LB service rules

I’m writing a go program to add/update service rules to a rancher lb (LbConfig, PortRules etc.).

Just needing some help in terms of the right approach for this i.e. how to merge in the new portRules and which resources to update.

I’m able to get the correct lb into a *rancher.LoadBalancerService object and then would like to manipulate. Example code would be ideal, but tips on how to merge in the new lbconfig and portrules would certainly help and how to then apply it - do we need to Update the LbConfig then do an Upgrade on the lb?

Generally a simple thing to do is open up the developer tools in the browser and go to the Network tab and then perform the action you’re interested in and see what API calls the UI made. Then do the equivalent in your code.

portRules are directly editable on a balancer so you can just change them and PUT to the balancer (Update). Upgrade causes new containers to be created and the old ones to be deleted, e.g. when changing the image.

Thanks @vincent. Ok that makes sense. I do see what is being changed in the dev tools, no problem identifying there.

Didn’t quite realise that I would just update the LB by directly editing, then doing an update. Will give it a go and report back.

Is that Update or ActionUpdate? I think that is part of my confusion.

They probably both work but Update, which does a HTTP PUT. Action* are (as the name suggests) for executing actions (like restart, start, stop on a service).

For dumb historical framework reasons there is also an action corresponding to the create, update and remove HTTP methods (POST, PUT and DELETE) on everything. And then it shows up in the generated client because it is an action the API says is there. (these are gone in 2.0)

I guess a go newbie problem here:

	// create port rule client
	portRule := &rancher.PortRule{
		Protocol: "https",
		Hostname: "flaccid-is-learning",
		Path: "",
		SourcePort: 443,
		TargetPort: 80,
		ServiceId: "1s258",
	}
	log.Info("new port rule ", portRule)

	log.Info("current port rules ", servicesRouter.LbConfig.PortRules)

	// add the new port rule
	servicesRouter.LbConfig.PortRules = append(servicesRouter.LbConfig.PortRules, *portRule)

	log.Info("new port rules ", servicesRouter.LbConfig.PortRules)

	// Update the services router
	log.Info("updating the services router")
	update, err := rancherClient.LoadBalancerService.ActionUpdate(servicesRouter)
	if err != nil {
		panic(err)
	} else {
		log.Info("update complete: ", update)
	}
INFO[0000] new port rules [{{ portRule map[] map[]}    3 https  1s1128 443 8043} {{ portRule map[] map[]}  c41337ee.dev.foo.io  4 https  1s1136 443 80} {{  map[] map[]}  flaccid-is-learning  0 https  1s258 443 80}]

Is not being appended in as a portRule. Is append() the wrong technique or just wrong syntax?

I’m seeing the same behaviour as @flaccid here. As neatly formatting code seems challenging in the forum, here’s a link to a Gist that contains the bare-minimum code to reproduce this.

I’ve tried using rancherClient.LoadBalancerService.Update() instead of ActionUpdate() but that didn’t work as it said it is not updatable. Digging through the code I see it looks at the Resource Methods for “loadBalancerService” to determine if it’s updatable or not. Interestingly, if I look at /v2-beta/schemas (I believe this is where go-rancher parses the schema JSON from), it lists “GET” as the only resource method for loadBalancerService. If I look at /v2-beta/projects/1a7/schemas, it lists “GET”, “PUT” and “DELETE” as the resource methods for loadBalancerService. Any feedback on the discrepancy?

As for ActionUpdate(), @vincent you mentioned both should probably work but as far as I can see, it puts the load balancer into updating but the port rule change never makes it.

Appreciate any pointers.

Well, I figured out one thing. The UI actually calls an Upgrade: http://myMasters:8080/env/1a5/balancers/run?serviceId=1s43&stackId=1st35&upgrade=true

The code in the gist here: Working Upgrade Gist

Also, I switched to v3, then again I am testing this on my 2.0 instance, but just switching to v2 will still work.

1 Like

Appreciate this. True, I do see in the UI its a post to the LB with and upgrade key and inServiceUpgradeStrategy. The URL in the address bar has &upgrade=true.

Will give it a try and report back (using v2 on rancher 1.6.10).

Ok I tested this out, but still not working. The new rule just doesn’t get added and I get no error back :frowning:

Here is the full code (you can ignore a lot of as code above doing the upgrade will be used once I can get the upgrade to work):

package main

import (
	"fmt"
	"os"
  "time"

  "reflect"

  rancher "github.com/rancher/go-rancher/v2"
	log "github.com/Sirupsen/logrus"
	"github.com/urfave/cli"
)

type Client struct {
	client *rancher.RancherClient
}

var (
	VERSION = "v0.0.0-dev"
)

var withoutPagination *rancher.ListOpts

func createClient(rancherURL, accessKey, secretKey string) (*rancher.RancherClient) {
	client, err := rancher.NewRancherClient(&rancher.ClientOpts{
		Url:       rancherURL,
		AccessKey: accessKey,
		SecretKey: secretKey,
    Timeout:   time.Second * 5,
	})

	if err != nil {
		log.Errorf("Failed to create a client for rancher, error: %s", err)
		os.Exit(1)
	}

	return client
}

func listRancherLoadBalancerServices(client *rancher.RancherClient) []*rancher.LoadBalancerService {
	var servicesList []*rancher.LoadBalancerService

	services, err := client.LoadBalancerService.List(withoutPagination)

	if err != nil {
		log.Errorf("cannot get services: %+v", err)
	}

	for k := range services.Data {
		servicesList = append(servicesList, &services.Data[k])
	}

	return servicesList
}

func beforeApp(c *cli.Context) error {
	if c.GlobalBool("debug") {
		log.SetLevel(log.DebugLevel)
	}
	return nil
}

func main() {
	app := cli.NewApp()
	app.Name = "rancher-svcd"
	app.Version = VERSION
	app.Usage = "rancher-svcd"
	app.Action = start
	app.Before = beforeApp
	app.Flags = []cli.Flag{
		cli.BoolFlag{
			Name: "debug,d",
		},
		cli.StringFlag{
			Name:  "metadata-url",
			Value: "http://169.254.169.250/2016-07-29",
			Usage: "Provide full URL of Metadata",
      EnvVar: "RANCHER_METADATA_URL",
		},
    cli.StringFlag{
			Name:  "router-service-tag",
			Value: "services_router",
			Usage: "Tag used to identify the load balancer serviced used for routing",
      EnvVar: "ROUTER_SERVICE_TAG",
		},
    cli.StringFlag{
			Name:  "lb-id",
			Usage: "Public rancher load balancer ID",
		},
    cli.StringFlag{
			Name:  "rancher-url",
			Value: "http://localhost:8080/",
			Usage: "Provide full URL of rancher server",
      EnvVar: "RANCHER_URL",
		},
    cli.StringFlag{
			Name:  "rancher-access-key",
			Usage: "Rancher Access Key",
      EnvVar: "RANCHER_ACCESS_KEY",
		},
    cli.StringFlag{
			Name:  "rancher-secret-key",
			Usage: "Rancher Secret Key",
      EnvVar: "RANCHER_SECRET_KEY",
		},
	}

	app.Run(os.Args)
}

func start(c *cli.Context) error {
	log.Info("starting up")

  // create the rancher client
  rancherClient := createClient(c.String("rancher-url"),
                                c.String("rancher-access-key"),
                                c.String("rancher-secret-key"))

	var servicesRouter *rancher.LoadBalancerService
	var targetLBs []*rancher.LoadBalancerService

	loadBalancerServices := listRancherLoadBalancerServices(rancherClient)

  // get the lb
  if len(c.String("lb-id")) > 0 {
    loadBalancerService, err := rancherClient.LoadBalancerService.ById(c.String("lb-id"))
    if err != nil {
      panic(err)
    }

    log.Debug(loadBalancerService)
    log.Debug(loadBalancerService.LaunchConfig.Labels)
  } else {
    for _, s := range loadBalancerServices {
			log.Debug("processing ", s.Uuid)
			log.Debug("data ", s)
			log.Debug("labels: ", s.LaunchConfig.Labels)

			for k, v := range s.LaunchConfig.Labels {
				// this lb is a service router
				if k == "services_router" && v == "true" {
					servicesRouter = s
					log.Debug(reflect.TypeOf(s))
					log.Debug(reflect.TypeOf(servicesRouter))
			  	log.Debug(s)
			  	break
			 	}

				// this lb is a service target
				if k == "platform_identifier" {
					log.Debug("found a target service ", s)
					targetLBs = append(targetLBs, s)
				}
			}
    }
  }

	// by now we should have the service router resource!
	//if servicesRouter ? {
	//	log.Errorf("no services router! found")
	//	os.Exit(2)
	//}
	log.Info("using services router, ", servicesRouter.Name)

	log.Info("found ", len(targetLBs), " target service(s)")
	log.Debug("target LBs", targetLBs)

	var requestHost string

	// for each target
	for i, t := range targetLBs {
		var platformId string
		var platformDnsDomain string

		log.Debug(i, t)

		// construct the request host
		for k, v := range t.LaunchConfig.Labels {
			log.Debug(k)
		  if k == "platform_identifier" {
				log.Debug(v)
				log.Debug(reflect.TypeOf(v))
				platformId = fmt.Sprint(v)
			}
			if k == "platform_dns_domain" {
				log.Debug(v)
				log.Debug(reflect.TypeOf(v))
				platformDnsDomain = fmt.Sprint(v)
			}
		}
		requestHost = fmt.Sprintf(platformId + "." + platformDnsDomain)

		log.Info("request host: ", requestHost)
	}

	log.Info("servicesRouter LbConfig", servicesRouter.LbConfig)
	log.Info("servicesRouter LinkedServices", servicesRouter.LinkedServices)

	// create port rule client
	portRule := rancher.PortRule{
		Protocol: "https",
		Hostname: "flaccid-is-learning",
		Path: "",
		Priority: 10,
		SourcePort: 443,
		TargetPort: 80,
		ServiceId: "1s1128",
	}
	log.Info("new port rule ", portRule)

	// The strategy allows you to upgrade
 	strategy := &rancher.ServiceUpgrade{
		InServiceStrategy: &rancher.InServiceUpgradeStrategy{
 		  BatchSize: 1,
 		  StartFirst: true,
 		  LaunchConfig: servicesRouter.LaunchConfig,
 	  },
  }

	// Here we can see the new rule added to the current list of rules
	log.Info("updating the services router")
  servicesRouter.LbConfig.PortRules = append(servicesRouter.LbConfig.PortRules, portRule)
	log.Infof("New service Rules: %+v", servicesRouter.LbConfig.PortRules)
	update, err := rancherClient.LoadBalancerService.ActionUpgrade(servicesRouter, strategy)
	if err != nil {
		panic(err)
	} else {
    // Notice here it isn't present in the returned object
		log.Infof("update complete: +%v", update.LbConfig.PortRules)
	}

  return nil
}

I’m on my phone and don’t have time to go compiling things and play with this now but 1.x vs 2.0 preview and upcoming preview2 are all very different.

There is no backwards-compatibility for the v2 API in 2.0 yet (but v3 is aliased to “/v2” and that makes many basic things work). There is no upgrade action on services in 2.0/v3 at all anymore (or if there is it’s cruft), and the confusing update action is gone (vs PUT).

In 2.0 all changes are PUT and the backend figures out if they can be done by updating the existing containers or if the rolling upgrade needs to be done to replace them with new ones.

I also can’t remember how well Balancers work in general in the current preview, and the whole model will be different for preview2 (ingress controllers).

So TL;DR stick to 1.x until 2.0 is baked.

For 1.x the UI does an update (PUT) when you click save which updates some things (including portRules). If there are changes which require an upgrade ( e.g. updating the image or labels) then a separate upgrade action call is done after the update.

“upgrade=true” in the UI URL does not necessarily mean an upgrade will be sent to the server. It and serviceId just differentiate between a new service vs edit existing vs clone existing as new.

Thanks for the clarification. The real question then is why is the updated PortRules not being applied when the call is made. Printing out the slice before we make the call we see the added rules. After, the added rules are gone.

In my use case, I don’t think an upgrade will ever be needed as the desire is to just manipulate the service rules. Keen to work out how to just do an update successfully, adding a service rule (portRule).

I’ve uploaded my project to github and so currently this code is living in https://github.com/flaccid/rancher-services-gateway/blob/master/discover/discover.go. The issue is not resolved and atm, I’m considering doing a raw http call as a workaround. Would love a solution and one which prevents upgrade, just update.

I am also struggling with the same issue. Did you guys ever figure out how to resolve this? Maybe one thing you guys could help me with, is there any better places to get documentation for this go-rancher SDK with actual explanations/descriptions/examples of what some of these methods do. I’ve been using godoc and gowalker + just reading through their code. As already mentioned knowing the difference between Update and Upgrade or ActionUpdate would be very helpful.

I did notice one thing while reading through the output of the existing port rules and the new one I am trying to create, is that the existing ones have Resource.Type: portRule. Where as the one I created (the same way you guys are) does not. On top of that, I did notice that they do have this Create method for a PortRuleClient https://godoc.org/github.com/rancher/go-rancher/v2#PortRuleClient.Create. I’ve been trying to play around with that. But I have not been successful yet. Here is an example, maybe you guys can point out something wrong with my approach.

portRuleClient := &rancher.PortRuleClient{}

// create port rule client
portRule := &rancher.PortRule{
	Protocol:   "https",
	Hostname:   "testing",
	Path:       "",
	Priority:   10,
	SourcePort: 443,
	TargetPort: 80,
	ServiceId:  "1s73",
}

log.Infof("PORT RULE: %+v", portRule)

// Trying to use the create method
rule, err := portRuleClient.Create(portRule)

// Can't get past here
if err != nil {
	panic(err)
} else {
	servicesRouter.LbConfig.PortRules = append(servicesRouter.LbConfig.PortRules, *rule)
	log.Infof("New service Rules: %+v", servicesRouter.LbConfig.PortRules)

	update, err := rancherClient.LoadBalancerService.ActionUpgrade(servicesRouter, strategy)
	if err != nil {
		panic(err)
	} else {
		log.Infof("update complete: +%v", update.LbConfig.PortRules)
	}
}

Though I keep getting a panic: runtime error: invalid memory address or nil pointer dereference on the Create method.

I just ended up doing a PUT with it all which is good enough but does obviously not use go-rancher for that update, see https://github.com/flaccid/rancher-services-gateway/blob/master/discover/discover.go.