5 Commits

Author SHA1 Message Date
mayuresh82
e32ff1d52c Merge pull request #8 from mayuresh82/vip_config
Add ability to specify vip parameters
2021-05-14 09:35:05 -07:00
Mayuresh Gaitonde
82152d030d Fix dockerfile 2021-05-13 23:54:52 -07:00
Mayuresh Gaitonde
5821c01a7b Add ability to specify vip parameters 2021-05-13 20:33:05 -07:00
mayuresh82
8ca38f4b77 Merge pull request #7 from mayuresh82/tests
Tests
2020-12-18 11:47:07 -08:00
mayuresh82
b8ec7a3391 Create LICENSE 2020-11-28 00:41:16 -08:00
12 changed files with 112 additions and 48 deletions

View File

@@ -3,18 +3,19 @@ RUN apk update && \
apk upgrade && \ apk upgrade && \
apk add --no-cache git && \ apk add --no-cache git && \
apk add make apk add make
RUN mkdir -p /opt/gocast
RUN mkdir -p /go/src/github.com/mayuresh82 RUN mkdir -p /go/src/github.com/mayuresh82/gocast
RUN cd /go/src/github.com/mayuresh82 && \
git clone https://github.com/mayuresh82/gocast COPY . /go/src/github.com/mayuresh82/gocast
WORKDIR /go/src/github.com/mayuresh82/gocast WORKDIR /go/src/github.com/mayuresh82/gocast
RUN make RUN make
RUN cp gocast /opt/gocast/
FROM alpine:latest FROM alpine:latest
RUN apk --no-cache add ca-certificates bash iptables netcat-openbsd sudo RUN apk --no-cache add ca-certificates bash iptables netcat-openbsd sudo
WORKDIR /root/ WORKDIR /root/
COPY --from=builder /opt/gocast/gocast . COPY --from=builder /go/src/github.com/mayuresh82/gocast .
EXPOSE 8080/tcp EXPOSE 8080/tcp

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 mayuresh82
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -24,4 +24,7 @@ bgp:
apps: apps:
- name: app1 - name: app1
vip: 1.1.1.1/32 vip: 1.1.1.1/32
vip_config:
# additional per VIP BGP communities
bgp_communities: [ aaaa:bbbb ]
monitor: port:tcp:5000 monitor: port:tcp:5000

View File

@@ -26,9 +26,16 @@ type BgpConfig struct {
Origin string Origin string
} }
type VipConfig struct {
// per VIP BGP communities to announce. This is in addition to the
// global config
BgpCommunities []string `yaml:"bgp_communities"`
}
type AppConfig struct { type AppConfig struct {
Name string Name string
Vip string Vip string
VipConfig VipConfig `yaml:"vip_config"`
Monitors []string Monitors []string
Nats []string Nats []string
} }

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/mayuresh82/gocast/config"
) )
type MonitorType int type MonitorType int
@@ -51,7 +52,8 @@ func (m Monitors) Contains(elem *Monitor) bool {
type App struct { type App struct {
Name string Name string
Vip *net.IPNet Vip *Route
VipConfig config.VipConfig
Monitors Monitors Monitors Monitors
Nats []string Nats []string
Source string Source string
@@ -66,10 +68,15 @@ func (a *App) Equal(other *App) bool {
return false return false
} }
} }
return a.Name == other.Name && a.Vip.String() == other.Vip.String() return a.Name == other.Name && a.Vip.Net.String() == other.Vip.Net.String()
} }
func NewApp(appName, vip string, monitors []string, nats []string, source string) (*App, error) { func (a *App) String() string {
return fmt.Sprintf("Name: %s, Vip: %s, VipConf: %v, Monitors: %v, Nats: %v, Source: %s",
a.Name, a.Vip.Net.String(), a.VipConfig, a.Monitors, a.Nats, a.Source)
}
func NewApp(appName, vip string, vipConfig config.VipConfig, monitors []string, nats []string, source string) (*App, error) {
if appName == "" { if appName == "" {
return nil, fmt.Errorf("Invalid app name") return nil, fmt.Errorf("Invalid app name")
} }
@@ -78,7 +85,8 @@ func NewApp(appName, vip string, monitors []string, nats []string, source string
if err != nil { if err != nil {
return nil, fmt.Errorf("Invalid VIP specified, need ip/mask") return nil, fmt.Errorf("Invalid VIP specified, need ip/mask")
} }
app.Vip = ipnet app.Vip = &Route{Net: ipnet, Communities: vipConfig.BgpCommunities}
app.VipConfig = vipConfig
for _, m := range monitors { for _, m := range monitors {
// valid monitor formats: // valid monitor formats:
// "port:tcp:123" , "exec:/local/check.sh", "consul" // "port:tcp:123" , "exec:/local/check.sh", "consul"

View File

@@ -3,32 +3,36 @@ package controller
import ( import (
"testing" "testing"
"github.com/mayuresh82/gocast/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestAppParsing(t *testing.T) { func TestAppParsing(t *testing.T) {
a := assert.New(t) a := assert.New(t)
app1, err := NewApp("app1", "1.1.1.1/32", []string{"port:tcp:123"}, []string{}, "") app1, err := NewApp("app1", "1.1.1.1/32", config.VipConfig{}, []string{"port:tcp:123"}, []string{}, "")
a.Nil(err) a.Nil(err)
app2, err := NewApp("app1", "1.1.1.1/32", []string{"port:tcp:123"}, []string{}, "") app2, err := NewApp("app1", "1.1.1.1/32", config.VipConfig{BgpCommunities: []string{"111:222"}}, []string{"port:tcp:123"}, []string{}, "")
a.Nil(err) a.Nil(err)
app3, err := NewApp("app3", "2.2.2.2/32", []string{"exec:/bin/testme"}, []string{}, "") app3, err := NewApp("app3", "2.2.2.2/32", config.VipConfig{}, []string{"exec:/bin/testme"}, []string{}, "")
a.Nil(err) a.Nil(err)
a.Equal("1.1.1.1/32", app1.Vip.String()) a.Equal("1.1.1.1/32", app1.Vip.Net.String())
a.Equal(Monitor_PORT, app1.Monitors[0].Type) a.Equal(Monitor_PORT, app1.Monitors[0].Type)
a.Equal("123", app1.Monitors[0].Port) a.Equal("123", app1.Monitors[0].Port)
a.Equal("tcp", app1.Monitors[0].Protocol) a.Equal("tcp", app1.Monitors[0].Protocol)
a.Equal(config.VipConfig{}, app1.VipConfig)
a.Equal(true, app1.Equal(app2)) a.Equal(true, app1.Equal(app2))
a.Equal("111:222", app2.Vip.Communities[0])
a.Equal(Monitor_EXEC, app3.Monitors[0].Type) a.Equal(Monitor_EXEC, app3.Monitors[0].Type)
a.Equal("/bin/testme", app3.Monitors[0].Cmd) a.Equal("/bin/testme", app3.Monitors[0].Cmd)
// test errors // test errors
_, err = NewApp("app4", "4.4.4.4", []string{}, []string{}, "") _, err = NewApp("app4", "4.4.4.4", config.VipConfig{}, []string{}, []string{}, "")
a.NotNil(err) a.NotNil(err)
_, err = NewApp("app4", "4.4.4.4/32", []string{"port:abcd::1023"}, []string{}, "") _, err = NewApp("app4", "4.4.4.4/32", config.VipConfig{}, []string{"port:abcd::1023"}, []string{}, "")
a.NotNil(err) a.NotNil(err)
} }

View File

@@ -14,6 +14,11 @@ import (
gobgp "github.com/osrg/gobgp/pkg/server" gobgp "github.com/osrg/gobgp/pkg/server"
) )
type Route struct {
Net *net.IPNet
Communities []string
}
type Controller struct { type Controller struct {
peerAS int peerAS int
localIP, peerIP net.IP localIP, peerIP net.IP
@@ -90,14 +95,14 @@ func (c *Controller) AddPeer(peer string) error {
return c.s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: n}) return c.s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: n})
} }
func (c *Controller) getApiPath(route *net.IPNet) *api.Path { func (c *Controller) getApiPath(route *Route) *api.Path {
afi := api.Family_AFI_IP afi := api.Family_AFI_IP
if route.IP.To4() == nil { if route.Net.IP.To4() == nil {
afi = api.Family_AFI_IP6 afi = api.Family_AFI_IP6
} }
prefixlen, _ := route.Mask.Size() prefixlen, _ := route.Net.Mask.Size()
nlri, _ := ptypes.MarshalAny(&api.IPAddressPrefix{ nlri, _ := ptypes.MarshalAny(&api.IPAddressPrefix{
Prefix: route.IP.String(), Prefix: route.Net.IP.String(),
PrefixLen: uint32(prefixlen), PrefixLen: uint32(prefixlen),
}) })
a1, _ := ptypes.MarshalAny(&api.OriginAttribute{ a1, _ := ptypes.MarshalAny(&api.OriginAttribute{
@@ -107,7 +112,7 @@ func (c *Controller) getApiPath(route *net.IPNet) *api.Path {
NextHop: c.localIP.String(), NextHop: c.localIP.String(),
}) })
var communities []uint32 var communities []uint32
for _, comm := range c.communities { for _, comm := range append(c.communities, route.Communities...) {
communities = append(communities, convertCommunity(comm)) communities = append(communities, convertCommunity(comm))
} }
a3, _ := ptypes.MarshalAny(&api.CommunitiesAttribute{ a3, _ := ptypes.MarshalAny(&api.CommunitiesAttribute{
@@ -121,7 +126,7 @@ func (c *Controller) getApiPath(route *net.IPNet) *api.Path {
} }
} }
func (c *Controller) Announce(route *net.IPNet) error { func (c *Controller) Announce(route *Route) error {
var found bool var found bool
err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) { err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
if p.Conf.NeighborAddress == c.peerIP.String() { if p.Conf.NeighborAddress == c.peerIP.String() {
@@ -140,7 +145,7 @@ func (c *Controller) Announce(route *net.IPNet) error {
return err return err
} }
func (c *Controller) Withdraw(route *net.IPNet) error { func (c *Controller) Withdraw(route *Route) error {
return c.s.DeletePath(context.Background(), &api.DeletePathRequest{Path: c.getApiPath(route)}) return c.s.DeletePath(context.Background(), &api.DeletePathRequest{Path: c.getApiPath(route)})
} }

View File

@@ -93,7 +93,8 @@ func TestBgpNew(t *testing.T) {
a.FailNow(err.Error()) a.FailNow(err.Error())
} }
_, ipnet, _ := net.ParseCIDR("20.30.40.0/24") _, ipnet, _ := net.ParseCIDR("20.30.40.0/24")
if err := ctrl.Announce(ipnet); err != nil { r := &Route{Net: ipnet}
if err := ctrl.Announce(r); err != nil {
a.FailNow(err.Error()) a.FailNow(err.Error())
} }

View File

@@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/mayuresh82/gocast/config"
) )
const ( const (
@@ -85,6 +86,7 @@ func (c *ConsulMon) queryServices() ([]*App, error) {
monitors []string monitors []string
nats []string nats []string
) )
var vipConf config.VipConfig
for _, tag := range service.Tags { for _, tag := range service.Tags {
// try to find the requires tags. Only vip is mandatory // try to find the requires tags. Only vip is mandatory
parts := strings.Split(tag, "=") parts := strings.Split(tag, "=")
@@ -94,6 +96,8 @@ func (c *ConsulMon) queryServices() ([]*App, error) {
switch parts[0] { switch parts[0] {
case "gocast_vip": case "gocast_vip":
vip = parts[1] vip = parts[1]
case "gocast_vip_communities":
vipConf.BgpCommunities = strings.Split(parts[1], ",")
case "gocast_monitor": case "gocast_monitor":
monitors = append(monitors, parts[1]) monitors = append(monitors, parts[1])
case "gocast_nat": case "gocast_nat":
@@ -104,7 +108,7 @@ func (c *ConsulMon) queryServices() ([]*App, error) {
glog.Errorf("No vip Tag found in matched service :%s", service.Service) glog.Errorf("No vip Tag found in matched service :%s", service.Service)
continue continue
} }
app, err := NewApp(service.Service, vip, monitors, nats, "consul") app, err := NewApp(service.Service, vip, vipConf, monitors, nats, "consul")
if err != nil { if err != nil {
glog.Errorf("Unable to add consul app: %v", err) glog.Errorf("Unable to add consul app: %v", err)
continue continue

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"testing" "testing"
"github.com/mayuresh82/gocast/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -15,7 +16,7 @@ var mockConsulData = map[string]string{
"ID": "test-app-1", "ID": "test-app-1",
"Service": "test-service", "Service": "test-service",
"Tags": [ "Tags": [
"enable_gocast", "gocast_vip=1.1.1.1/32", "gocast_monitor=consul" "enable_gocast", "gocast_vip=1.1.1.1/32", "gocast_monitor=consul", "gocast_vip_communities=111:222,333:444"
] ]
} }
}}`, }}`,
@@ -103,7 +104,9 @@ func TestQueryServices(t *testing.T) {
a.FailNow(err.Error()) a.FailNow(err.Error())
} }
a.Equal(1, len(apps)) a.Equal(1, len(apps))
app, _ := NewApp("test-service", "1.1.1.1/32", []string{"consul"}, []string{}, "consul") a.Equal([]string{"111:222", "333:444"}, apps[0].Vip.Communities)
app, _ := NewApp("test-service", "1.1.1.1/32", config.VipConfig{}, []string{"consul"}, []string{}, "consul")
a.True(app.Equal(apps[0])) a.True(app.Equal(apps[0]))
// test no match // test no match

View File

@@ -100,7 +100,7 @@ func NewMonitor(config *c.Config) *MonitorMgr {
mon.config = config mon.config = config
// add apps defined in config // add apps defined in config
for _, a := range config.Apps { for _, a := range config.Apps {
app, err := NewApp(a.Name, a.Vip, a.Monitors, a.Nats, "config") app, err := NewApp(a.Name, a.Vip, a.VipConfig, a.Monitors, a.Nats, "config")
if err != nil { if err != nil {
glog.Errorf("Failed to add configured app %s: %v", a.Name, err) glog.Errorf("Failed to add configured app %s: %v", a.Name, err)
continue continue
@@ -159,8 +159,8 @@ func (m *MonitorMgr) Add(app *App) {
glog.V(2).Infof("App %s already exists", app.Name) glog.V(2).Infof("App %s already exists", app.Name)
return return
} }
if appMon.app.Vip.String() == app.Vip.String() && appMon.app.Name != app.Name { if appMon.app.Vip.Net.String() == app.Vip.Net.String() && appMon.app.Name != app.Name {
glog.Errorf("Error: Vip %s is already being announced by app: %s", app.Vip.String(), appMon.app.Name) glog.Errorf("Error: Vip %s is already being announced by app: %s", app.Vip.Net.String(), appMon.app.Name)
return return
} }
} }
@@ -168,7 +168,7 @@ func (m *MonitorMgr) Add(app *App) {
appMon := &appMon{app: app, done: make(chan bool)} appMon := &appMon{app: app, done: make(chan bool)}
m.monitors[app.Name] = appMon m.monitors[app.Name] = appMon
go m.runLoop(appMon) go m.runLoop(appMon)
glog.Infof("Registered a new app: %v", app) glog.Infof("Registered a new app: %v", app.String())
} }
// Remove removes an app from monitor manager, stops BGP // Remove removes an app from monitor manager, stops BGP
@@ -183,7 +183,7 @@ func (m *MonitorMgr) Remove(appName string) {
glog.Errorf("Failed to withdraw route: %v", err) glog.Errorf("Failed to withdraw route: %v", err)
} }
} }
if err := deleteLoopback(a.app.Vip); err != nil { if err := deleteLoopback(a.app.Vip.Net); err != nil {
glog.Errorf("Failed to remove app: %s: %v", a.app.Name, err) glog.Errorf("Failed to remove app: %s: %v", a.app.Name, err)
} }
for _, nat := range a.app.Nats { for _, nat := range a.app.Nats {
@@ -191,7 +191,7 @@ func (m *MonitorMgr) Remove(appName string) {
if len(parts) != 2 { if len(parts) != 2 {
continue continue
} }
if err := natRule("D", a.app.Vip.IP, m.ctrl.localIP, parts[0], parts[1]); err != nil { if err := natRule("D", a.app.Vip.Net.IP, m.ctrl.localIP, parts[0], parts[1]); err != nil {
glog.Errorf("Failed to remove app: %s: %v", a.app.Name, err) glog.Errorf("Failed to remove app: %s: %v", a.app.Name, err)
} }
} }
@@ -228,7 +228,7 @@ func (m *MonitorMgr) checkCond(am *appMon) error {
if m.runMonitors(app) { if m.runMonitors(app) {
glog.V(2).Infof("All Monitors for app: %s succeeded", app.Name) glog.V(2).Infof("All Monitors for app: %s succeeded", app.Name)
if !am.announced { if !am.announced {
if err := addLoopback(app.Name, app.Vip); err != nil { if err := addLoopback(app.Name, app.Vip.Net); err != nil {
return err return err
} }
for _, nat := range app.Nats { for _, nat := range app.Nats {
@@ -236,7 +236,7 @@ func (m *MonitorMgr) checkCond(am *appMon) error {
if len(parts) != 2 { if len(parts) != 2 {
continue continue
} }
if err := natRule("A", app.Vip.IP, m.ctrl.localIP, parts[0], parts[1]); err != nil { if err := natRule("A", app.Vip.Net.IP, m.ctrl.localIP, parts[0], parts[1]); err != nil {
return err return err
} }
} }
@@ -294,13 +294,13 @@ func (m *MonitorMgr) CloseAll() {
if am.checkOn { if am.checkOn {
am.done <- true am.done <- true
} }
deleteLoopback(am.app.Vip) deleteLoopback(am.app.Vip.Net)
for _, nat := range am.app.Nats { for _, nat := range am.app.Nats {
parts := strings.Split(nat, ":") parts := strings.Split(nat, ":")
if len(parts) != 2 { if len(parts) != 2 {
continue continue
} }
natRule("D", am.app.Vip.IP, m.ctrl.localIP, parts[0], parts[1]) natRule("D", am.app.Vip.Net.IP, m.ctrl.localIP, parts[0], parts[1])
} }
} }
} }

View File

@@ -4,9 +4,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/golang/glog"
"github.com/mayuresh82/gocast/controller"
"net/http" "net/http"
"strings"
"github.com/golang/glog"
"github.com/mayuresh82/gocast/config"
"github.com/mayuresh82/gocast/controller"
) )
// Server is the main entrypoint into the app and serves app requests // Server is the main entrypoint into the app and serves app requests
@@ -46,7 +49,11 @@ func (s *Server) Serve(ctx context.Context) {
func (s *Server) registerHandler(w http.ResponseWriter, r *http.Request) { func (s *Server) registerHandler(w http.ResponseWriter, r *http.Request) {
queries := r.URL.Query() queries := r.URL.Query()
app, err := controller.NewApp(queries["name"][0], queries["vip"][0], queries["monitor"], queries["nat"], "http") var vipConf config.VipConfig
if vipComm, ok := queries["vip_communities"]; ok {
vipConf.BgpCommunities = strings.Split(vipComm[0], ",")
}
app, err := controller.NewApp(queries["name"][0], queries["vip"][0], vipConf, queries["monitor"], queries["nat"], "http")
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest) http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest)
return return